puts "Hello, World!"printfn "Hello, World!"printfn prints a string followed by a newline — the direct equivalent of Ruby's puts. F# top-level expressions execute in order, just like a Ruby script.
puts "First"
puts "Second"
puts "Third"printfn "First"
printfn "Second"
printfn "Third"F# top-level code runs sequentially. Each printfn call is an IO action — the program executes them in order, like Ruby's procedural style.
print "no newline"
print "\n"
puts "with newline"printf "no newline"
printf "\n"
printfn "with newline"printf is like Ruby's print — no trailing newline. printfn is like puts — adds a newline. Both accept format specifiers like %s, %d, and %A.
name = "Alice"
age = 30
price = 9.99
puts "#{name}, age #{age}, paid $#{format('%.2f', price)}"let name = "Alice"
let age = 30
let price = 9.99
printfn "%s, age %d, paid $%.2f" name age priceF# inherits printf-style format specifiers: %s for strings, %d for integers, %f for floats, %b for booleans, and %A for any value using F#'s default pretty-printer.
name = "Alice"
age = 30
puts "Hello, #{name}! You are #{age} years old."let name = "Alice"
let age = 30
printfn $"Hello, {name}! You are {age} years old."F# 5+ supports string interpolation with $"..." syntax, equivalent to Ruby's #{}. Any expression is valid inside the braces. For format control, use $"%.2f{price}" or switch to format specifiers.
name = "Alice"
puts namelet name = "Alice"
printfn "%s" nameIn F#, let creates an immutable binding — name cannot be reassigned. All let bindings are immutable by default, unlike Ruby's local variables. Attempting name <- "Bob" is a compile error.
counter = 0
counter = 5
puts counterlet mutable counter = 0
counter <- 5
printfn "%d" counterF# requires the mutable keyword to opt in to mutability. Assignment to a mutable binding uses <- — not =. The = operator is always equality comparison in F#.
number = 42
greeting = "hello"
decimal = 3.14
flag = true
puts [number.class, greeting.class, decimal.class, flag.class].inspectlet number = 42 // inferred: int
let greeting = "hello" // inferred: string
let decimal = 3.14 // inferred: float
let flag = true // inferred: bool
printfn "%A" (number, greeting, decimal, flag)F#'s Hindley-Milner type inference deduces the type of every binding without annotations. Unlike Ruby's dynamic typing, these types are checked statically at compile time — number + greeting is a compile error, not a runtime one.
# Ruby has no built-in type annotations (Sorbet/RBS are external tools)
age = 30
greeting = "Hello"
puts "#{greeting}, age #{age}"let age: int = 30
let greeting: string = "Hello"
printfn "%s, age %d" greeting ageType annotations use a colon after the identifier name. They are optional — the compiler infers types — but useful for documentation or when the compiler needs a hint. F# annotations always follow the name, never precede it as in C# or Java.
# Ruby rebinds (mutates) the same variable
x = 1
x = x + 1
puts x// Shadowing works inside a function or let..in expression:
let compute () =
let number = 1
let number = number + 1 // shadows the first binding
number
printfn "%d" (compute ())F# allows a let binding to shadow a previous one with the same name within a function scope. This creates a new immutable binding — the original is hidden, not mutated. Shadowing is common in F# pipeline code as an alternative to mutable variables.
puts 42.class # Integer
puts 3.14.class # Float
puts 1_000_000.class # Integerlet integer = 42 // int (32-bit)
let large = 1_000_000L // int64 (64-bit, L suffix)
let floating = 3.14 // float (64-bit double)
let single = 3.14f // float32 (32-bit, f suffix)
printfn "%d %d %f %f" integer large floating singleF# has distinct numeric types: int (32-bit), int64 (64-bit, L suffix), float (64-bit double), and float32 (f suffix). Unlike Ruby, implicit coercion between them is not allowed — 1 + 1.0 is a type error.
puts 42.to_s
puts "42".to_i
puts 3.to_flet text = string 42
let number = int "42"
let decimal = float 3
printfn "%s %d %f" text number decimalF# uses conversion functions named after the target type: string, int, float, bool, etc. Unlike Ruby's .to_s method syntax, these are plain functions. Conversions that can fail (like int "abc") raise an exception at runtime.
puts true && false
puts true || false
puts !true
puts (1 == 1)
puts (1 != 2)printfn "%b" (true && false)
printfn "%b" (true || false)
printfn "%b" (not true)
printfn "%b" (1 = 1)
printfn "%b" (1 <> 2)F# uses &&, ||, and not (a function, not a prefix operator). Equality is =; inequality is <>. These match OCaml conventions and differ from C-family languages.
def do_something
puts "done"
# Ruby methods return nil when nothing is returned explicitly
end
result = do_something
puts result.inspectlet doSomething () =
printfn "done"
// Returns unit: ()
let result = doSomething ()
printfn "%A" resultF# has a unit type (written as ()) representing "no meaningful value." Functions that perform side effects and return nothing have return type unit. It is a real type — not a null or nil — and signals to callers that the function is called for its effect.
point = [3, 4]
x, y = point
puts "x=#{x}, y=#{y}"let point = (3, 4)
let (x, y) = point
printfn "x=%d, y=%d" x yF# tuples use parentheses with commas: (3, 4). They are destructured with let (x, y) = .... Tuples can hold mixed types: ("Alice", 30, true) has type string * int * bool. The * in the type is read as "and."
first = "Hello"
last = "World"
puts first + ", " + last + "!"let first = "Hello"
let last = "World"
printfn "%s" (first + ", " + last + "!")F# uses + for string concatenation, the same as Ruby. However, F# does not allow mixing + with non-string types — "Count: " + 5 is a compile error. Use string 5 to convert first, or use interpolation.
text = "hello, world"
puts text.length
puts text.upcase
puts text.include?("world")
puts text.gsub("world", "F#")let text = "hello, world"
printfn "%d" text.Length
printfn "%s" (text.ToUpper())
printfn "%b" (text.Contains("world"))
printfn "%s" (text.Replace("world", "F#"))F# strings are .NET strings, so all .NET string methods are available. Properties like Length use no parentheses; methods like ToUpper() do. The naming convention is PascalCase (unlike Ruby's snake_case).
sentence = "one two three"
words = sentence.split(" ")
puts words.inspect
puts words.join(", ")let sentence = "one two three"
let words = sentence.Split(" ")
printfn "%A" words
printfn "%s" (String.concat ", " words)Split (from .NET) splits into a string array. String.concat joins a sequence of strings with a separator — the F# equivalent of Ruby's Array#join. Note that Split returns a .NET array (string[]), not an F# list.
message = <<~TEXT
Line one
Line two
Line three
TEXT
puts messagelet message = """
Line one
Line two
Line three"""
printfn "%s" messageF# triple-quoted strings (""") span multiple lines without escape sequences. Backslashes and quotes inside are treated literally. They are similar to Ruby heredocs but without the indentation-stripping that <<~ provides.
text = " hello world "
puts text.strip
puts text.strip[0, 5]let text = " hello world "
let trimmed = text.Trim()
let sub = trimmed.Substring(0, 5)
printfn "%s" trimmed
printfn "%s" subTrim() removes leading and trailing whitespace, equivalent to Ruby's strip. Substring(startIndex, length) extracts a portion — note that the second argument is a length, not an end index, unlike Ruby's slicing syntax.
numbers = [1, 2, 3, 4, 5]
puts numbers.inspectlet numbers = [1; 2; 3; 4; 5]
printfn "%A" numbersF# lists use semicolons as element separators: [1; 2; 3]. This is the most common surprise for Rubyists. Writing [1, 2, 3] in F# creates a list of one element — a 3-tuple — not a three-element list. F# lists are immutable singly-linked lists.
rest = [2, 3, 4]
all = [1] + rest
puts all.inspectlet rest = [2; 3; 4]
let all = 1 :: rest
printfn "%A" allThe :: (cons) operator prepends a single element to a list. Unlike Ruby's +, cons is O(1) — it reuses the existing tail without copying it. F# lists are persistent: rest is unchanged after 1 :: rest.
numbers = [1, 2, 3, 4, 5]
doubled = numbers.map { |number| number * 2 }
puts doubled.inspectlet numbers = [1; 2; 3; 4; 5]
let doubled = List.map (fun number -> number * 2) numbers
printfn "%A" doubledList.map is F#'s equivalent of Ruby's map. The function argument comes first, then the list. Anonymous functions use fun parameter -> body syntax — F#'s equivalent of Ruby blocks. There are no method-chaining versions; use the pipe operator instead.
numbers = [1, 2, 3, 4, 5, 6]
evens = numbers.select { |number| number.even? }
puts evens.inspectlet numbers = [1; 2; 3; 4; 5; 6]
let evens = List.filter (fun number -> number % 2 = 0) numbers
printfn "%A" evensList.filter is the equivalent of Ruby's select. Note that = is the equality operator (not assignment), and % is modulo. F# has no equivalent of Ruby's Integer#even? — use explicit comparison instead.
numbers = [1, 2, 3, 4, 5]
total = numbers.inject(0) { |sum, number| sum + number }
puts totallet numbers = [1; 2; 3; 4; 5]
let total = List.fold (fun accumulator number -> accumulator + number) 0 numbers
printfn "%d" totalList.fold is the equivalent of Ruby's inject/reduce. The argument order is: function, initial value, list. List.foldBack folds from the right. The function receives the accumulator first, then the current element.
items = [1, 2, 3]
puts items[0]
puts items.lengthlet items = [|1; 2; 3|]
let first = items.[0]
let count = items.Length
printfn "%d %d" first countF# arrays use [|...|] syntax with semicolons. Unlike F# lists, arrays are mutable and have O(1) indexed access. Element access uses .[index] or just [index] in newer F#. F# arrays are identical to .NET arrays.
evens = (1..Float::INFINITY).lazy.select { |n| n.even? }.first(5)
puts evens.inspectlet allEvens = seq {
for number in 1..1000 do
if number % 2 = 0 then yield number
}
let firstFive = allEvens |> Seq.take 5 |> Seq.toList
printfn "%A" firstFiveF# sequences (seq { }) are lazy, like Ruby's Enumerator::Lazy. They compute elements on demand. Seq.take 5 evaluates only the first five elements. Most F# collection functions have Seq, List, and Array variants.
ages = { "Alice" => 30, "Bob" => 25 }
puts ages["Alice"]
puts ages.keys.inspectlet ages = Map.ofList [("Alice", 30); ("Bob", 25)]
printfn "%d" ages.["Alice"]
printfn "%A" (Map.keys ages |> Seq.toList)F# Map is an immutable ordered dictionary. It is built from a list of tuples using Map.ofList. Key-value pairs are tuples: (key, value) — not Ruby's key => value. Looking up a key that does not exist raises an exception; use Map.tryFind to get an Option instead.
score = 85
if score >= 90
puts "A"
elsif score >= 80
puts "B"
else
puts "C"
endlet score = 85
if score >= 90 then
printfn "A"
elif score >= 80 then
printfn "B"
else
printfn "C"F# uses elif (not elsif) and requires then after each condition. Unlike Ruby, F# if is an expression — it returns a value. Both branches must have the same type, otherwise the compiler reports a type error.
age = 20
status = age >= 18 ? "adult" : "minor"
puts statuslet age = 20
let status = if age >= 18 then "adult" else "minor"
printfn "%s" statusF# if/then/else is an expression that returns the value of whichever branch was taken. This replaces Ruby's ternary operator ?:. The else branch is required when the result is used — omitting it implies the value is unit.
(1..5).each { |i| puts i }for i in 1..5 do
printfn "%d" iF# for ... in ... do iterates over ranges and sequences. 1..5 is an inclusive range, identical to Ruby's 1..5. F# also has for i = 1 to 5 do for counted loops. The body is indented — F# uses significant whitespace.
count = 1
while count <= 5
puts count
count += 1
endlet mutable count = 1
while count <= 5 do
printfn "%d" count
count <- count + 1F# while loops require mutable state. Idiomatic F# prefers recursion or sequence operations over imperative loops, but while is available. Note that <- is the mutation operator and there is no += shorthand in F#.
day = "Monday"
case day
when "Saturday", "Sunday"
puts "Weekend"
else
puts "Weekday"
endlet day = "Monday"
match day with
| "Saturday" | "Sunday" -> printfn "Weekend"
| _ -> printfn "Weekday"F# match uses | for each arm and -> to separate the pattern from the body. The _ wildcard matches anything, like Ruby's else in a case. Multiple patterns on one arm use |.
number = 42
case number
when 0 then puts "zero"
when (..(-1)) then puts "negative"
when (101..) then puts "large"
else puts "normal"
endlet number = 42
match number with
| 0 -> printfn "zero"
| n when n < 0 -> printfn "negative"
| n when n > 100 -> printfn "large"
| _ -> printfn "normal"Match guards use when after a pattern to add a boolean condition. The bound name (n) is available in the guard expression. This is more explicit than Ruby's case/when with range patterns.
point = [3, -2]
case point
in [x, y] if x > 0 && y > 0 then puts "Q1"
in [x, y] if x < 0 && y > 0 then puts "Q2"
in [x, y] if x < 0 && y < 0 then puts "Q3"
else puts "Q4 or axis"
endlet point = (3, -2)
match point with
| (x, y) when x > 0 && y > 0 -> printfn "Q1"
| (x, y) when x < 0 && y > 0 -> printfn "Q2"
| (x, y) when x < 0 && y < 0 -> printfn "Q3"
| _ -> printfn "Q4 or axis"F# can match on tuples by destructuring them in the pattern. Each tuple element is bound to a name and can be used in guards or the body. Tuple matching in F# is the cleanest way to handle multi-dimensional case logic.
items = [1, 2, 3]
case items
in [] then puts "empty"
in [single] then puts "one: #{single}"
in [first, *rest] then puts "head: #{first}, #{rest.length} more"
endlet items = [1; 2; 3]
match items with
| [] -> printfn "empty"
| [single] -> printfn "one: %d" single
| head :: tail -> printfn "head: %d, %d more" head (List.length tail)F# list patterns use :: to match the head and tail. [] matches empty, [x] matches a single-element list, and head :: tail matches any non-empty list. This is exhaustive — the compiler warns if a case is missing.
score = 85
grade = case score
when 90..100 then "A"
when 80..89 then "B"
when 70..79 then "C"
else "F"
end
puts gradelet score = 85
let grade =
match score with
| s when s >= 90 -> "A"
| s when s >= 80 -> "B"
| s when s >= 70 -> "C"
| _ -> "F"
printfn "%s" gradematch is an expression in F# — it returns a value. All arms must return the same type. This allows match to appear in any expression context, including let bindings, function arguments, and inside pipelines.
def add(x, y)
x + y
end
puts add(3, 4)let add x y = x + y
printfn "%d" (add 3 4)F# function definitions use let. Parameters follow the function name separated by spaces — no parentheses, no commas, no return. The last expression is the return value. Function calls also use spaces: add 3 4, not add(3, 4).
multiply = ->(x, y) { x * y }
double = multiply.curry.(2)
puts double.(5)let multiply x y = x * y
let double = multiply 2
printfn "%d" (double 5)Every F# function is automatically curried — calling it with fewer arguments than it expects returns a new function. multiply 2 returns a function int -> int. There is no .curry method — partial application is built into the language.
double = ->(n) { n * 2 }
increment = ->(n) { n + 1 }
double_then_increment = double >> increment
puts double_then_increment.(5)let double number = number * 2
let increment number = number + 1
let doubleAndIncrement = double >> increment
printfn "%d" (doubleAndIncrement 5)The >> operator composes two functions left-to-right: f >> g is equivalent to fun x -> g (f x). The << operator composes right-to-left. Function composition is a first-class idiom in F# alongside the pipe operator.
numbers = [1, 2, 3, 4, 5]
squared = numbers.map { |number| number * number }
puts squared.inspectlet numbers = [1; 2; 3; 4; 5]
let squared = List.map (fun number -> number * number) numbers
printfn "%A" squaredF# anonymous functions use fun parameter -> body. Multiple parameters: fun x y -> x + y. Unlike Ruby's blocks, F# lambdas are ordinary values — they can be stored in let bindings, passed to functions, and returned from functions.
def factorial(n)
return 1 if n <= 1
n * factorial(n - 1)
end
puts factorial(10)let rec factorial n =
if n <= 1 then 1
else n * factorial (n - 1)
printfn "%d" (factorial 10)F# requires the rec keyword to define a recursive function. This makes recursion explicit — a function without rec cannot call itself. The compiler can optimize tail-recursive functions to avoid stack overflow.
def apply_twice(func, value)
func.call(func.call(value))
end
double = ->(n) { n * 2 }
puts apply_twice(double, 3)let applyTwice func value = func (func value)
let double number = number * 2
printfn "%d" (applyTwice double 3)Functions are first-class values in F#. A function that accepts another function as a parameter is a higher-order function. The applyTwice function has inferred type ('a -> 'a) -> 'a -> 'a — it works for any type, not just integers.
result = [1, 2, 3, 4, 5, 6]
.select { |n| n.even? }
.map { |n| n * 3 }
.sum
puts resultlet result =
[1; 2; 3; 4; 5; 6]
|> List.filter (fun number -> number % 2 = 0)
|> List.map (fun number -> number * 3)
|> List.sum
printfn "%d" resultThe |> pipe operator passes the value on the left as the last argument to the function on the right. It reads top-to-bottom like Ruby's method chaining, but works with any function — not just methods on an object.
words = ["hello", "world", "fsharp"]
# Method chaining reads left to right:
result = words.map(&:upcase).select { |w| w.length > 5 }.first
puts resultlet words = ["hello"; "world"; "fsharp"]
// With pipes (reads in execution order):
let withPipes =
words
|> List.map (fun word -> word.ToUpper())
|> List.filter (fun word -> word.Length > 5)
|> List.head
// Equivalent nested form (reads inside-out):
let nested = List.head (List.filter (fun (word: string) -> word.Length > 5) (List.map (fun (word: string) -> word.ToUpper()) words))
printfn "%s" withPipesWithout pipes, nested function calls must be read inside-out, which obscures the order of operations. Pipes make the data flow explicit and match the direction you read. This is the primary reason |> is so central to idiomatic F#.
def add_tax(price)
price * 1.1
end
def round_to_cents(amount)
(amount * 100).round / 100.0
end
total = 29.99
puts round_to_cents(add_tax(total))let addTax price = price * 1.1
let roundToCents amount = System.Math.Round(amount * 100.0) / 100.0
let total = 29.99
let result = total |> addTax |> roundToCents
printfn "%f" resultThe pipe operator works with any user-defined function, not just library functions. total |> addTax |> roundToCents is equivalent to roundToCents (addTax total). This style encourages designing functions that accept their primary data as the last argument.
# Ruby has no direct equivalent to >>
# These are both possible:
triple = ->(n) { n * 3 }
add_one = ->(n) { n + 1 }
# Composition:
triple_add = triple >> add_one
puts triple_add.(4)
# Chaining (requires a value):
puts add_one.(triple.(4))let triple number = number * 3
let addOne number = number + 1
// >> composes functions (no value needed yet):
let tripleAndAdd = triple >> addOne
printfn "%d" (tripleAndAdd 4)
// |> applies to a specific value:
let result = 4 |> triple |> addOne
printfn "%d" result>> composes two functions into a new function without a value — it is for defining reusable pipelines. |> applies functions to a specific value — it is for data transformation. Both produce the same result when given the same input; the choice depends on whether you need the composed function as a reusable value.
def find_user(id)
users = { 1 => "Alice", 2 => "Bob" }
users[id] # returns nil if not found
end
user = find_user(1)
puts user.nil? ? "not found" : "found: #{user}"let findUser identifier =
let users = Map.ofList [(1, "Alice"); (2, "Bob")]
Map.tryFind identifier users // returns Some "Alice" or None
match findUser 1 with
| Some name -> printfn "found: %s" name
| None -> printfn "not found"F# uses Some value and None instead of value-or-nil. Map.tryFind returns Some value on success and None on failure. The type system enforces handling both cases — Option<string> cannot be used where a string is expected.
# Ruby uses &. (safe navigation) to map over nil-or-value
name = "Alice"
length = name&.length
puts lengthlet name = Some "Alice"
let length = Option.map (fun (text: string) -> text.Length) name
printfn "%A" lengthOption.map applies a function to the value inside Some, leaving None unchanged. It is the typed equivalent of Ruby's &. safe navigation operator. The result is always an Option — Some "Alice" becomes Some 5.
def parse_int(text)
Integer(text) rescue nil
end
def double_if_positive(n)
n > 0 ? n * 2 : nil
end
result = parse_int("5")&.then { |n| double_if_positive(n) }
puts resultlet tryParseInt (text: string) =
match System.Int32.TryParse(text) with
| (true, value) -> Some value
| _ -> None
let doubleIfPositive number =
if number > 0 then Some (number * 2) else None
let result = tryParseInt "5" |> Option.bind doubleIfPositive
printfn "%A" resultOption.bind chains operations that each return an Option. If any step returns None, the chain short-circuits. This is the typed equivalent of Ruby's &.then { |x| ... } safe navigation chain, but tracked by the type system.
def divide(numerator, denominator)
return [:error, "division by zero"] if denominator == 0
[:ok, numerator / denominator]
end
case divide(10, 2)
in [:ok, value] then puts "Result: #{value}"
in [:error, msg] then puts "Error: #{msg}"
endlet divide numerator denominator =
if denominator = 0 then Error "division by zero"
else Ok (numerator / denominator)
match divide 10 2 with
| Ok value -> printfn "Result: %d" value
| Error message -> printfn "Error: %s" messageF# has a built-in Result<'T, 'E> type with Ok value and Error reason. It encodes expected failure in the type system, unlike Ruby's convention-based [:ok, value] tuples. The type forces callers to handle both outcomes.
def parse_number(text)
[true, Integer(text)]
rescue ArgumentError
[false, "not a number: #{text}"]
end
success, value = parse_number("42")
puts success ? value * 2 : valuelet tryParseNumber (text: string) =
match System.Int32.TryParse(text) with
| (true, value) -> Ok value
| _ -> Error $"not a number: {text}"
let result = tryParseNumber "42" |> Result.map (fun number -> number * 2)
printfn "%A" resultResult.map applies a function to the value inside Ok, leaving Error unchanged. Combined with the pipe operator, it enables a chain of transformations that propagates the first error and continues on success.
Person = Struct.new(:name, :age)
person = Person.new("Alice", 30)
puts person.name
puts person.agetype Person = { Name: string; Age: int }
let person = { Name = "Alice"; Age = 30 }
printfn "%s is %d" person.Name person.AgeF# records are immutable named data structures. Fields use FieldName: Type syntax. Record creation uses { FieldName = value; ... }. They are like Ruby's Struct but immutable by default and fully integrated with pattern matching and the type system.
person = { name: "Alice", age: 30 }
older = person.merge(age: 31)
puts oldertype Person = { Name: string; Age: int }
let alice = { Name = "Alice"; Age = 30 }
let olderAlice = { alice with Age = 31 }
printfn "%s is %d" olderAlice.Name olderAlice.AgeThe with keyword creates a copy of a record with specified fields changed. The original record is unchanged. This is the idiomatic way to "update" an immutable record — it is the F# equivalent of Ruby's Hash#merge for creating modified copies.
Person = Struct.new(:name, :age)
person = Person.new("Alice", 30)
case person
in { name:, age: (..17) } then puts "#{name} is a minor"
in { name:, age: } then puts "#{name} is an adult (#{age})"
endtype Person = { Name: string; Age: int }
let person = { Name = "Alice"; Age = 30 }
match person with
| { Name = name; Age = age } when age < 18 -> printfn "%s is a minor" name
| { Name = name; Age = age } -> printfn "%s is an adult (%d)" name ageRecords can be matched by field name. The { FieldName = binding } pattern extracts field values into local names. Partial matching is allowed — unmentioned fields are ignored. This is more structured than Ruby's hash pattern matching.
Address = Struct.new(:city, :country)
Person = Struct.new(:name, :address)
person = Person.new("Alice", Address.new("Paris", "France"))
puts person.address.citytype Address = { City: string; Country: string }
type Person = { Name: string; Address: Address }
let person = { Name = "Alice"; Address = { City = "Paris"; Country = "France" } }
printfn "%s" person.Address.CityRecords can nest other records. The with copy-update syntax handles nested updates, though it requires updating each level explicitly: { person with Address = { person.Address with City = "Berlin" } }. Lenses and optics libraries provide more ergonomic nested updates.
# Ruby uses symbols or constants for sum types
module Color
RED = :red
GREEN = :green
BLUE = :blue
end
color = Color::RED
puts colortype Color = Red | Green | Blue
let color = Red
printfn "%A" colorDiscriminated unions (DUs) are F#'s algebraic sum types — a value is exactly one of a fixed set of cases. Each case is a named constructor. Pattern matching on a DU is exhaustive: the compiler warns if you miss a case. Ruby has no direct equivalent; symbols or classes are the closest approximation.
class Shape
Circle = Struct.new(:radius)
Rectangle = Struct.new(:width, :height)
end
shape = Shape::Circle.new(5.0)
area = case shape
when Shape::Circle then Math::PI * shape.radius ** 2
when Shape::Rectangle then shape.width * shape.height
end
puts area.round(2)type Shape =
| Circle of radius: float
| Rectangle of width: float * height: float
let shape = Circle(radius = 5.0)
let area =
match shape with
| Circle(radius) -> System.Math.PI * radius * radius
| Rectangle(width, height) -> width * height
printfn "%.2f" areaDU cases carry typed data. Circle of radius: float means the Circle case holds a float value named radius. Naming the fields makes construction and pattern matching self-documenting. This replaces Ruby's class hierarchy with a single concise type declaration.
# A simple binary tree in Ruby using classes
class Tree
Leaf = Class.new
Node = Struct.new(:value, :left, :right)
end
tree = Tree::Node.new(1,
Tree::Node.new(2, Tree::Leaf.new, Tree::Leaf.new),
Tree::Leaf.new)
puts tree.valuetype Tree =
| Leaf
| Node of value: int * left: Tree * right: Tree
let tree = Node(value = 1,
left = Node(value = 2, left = Leaf, right = Leaf),
right = Leaf)
let rec depth tree =
match tree with
| Leaf -> 0
| Node(_, left, right) -> 1 + max (depth left) (depth right)
printfn "depth: %d" (depth tree)Discriminated unions can be recursive — a Tree case can contain Tree values. Recursive DUs are the natural way to represent trees, linked lists, expression trees, and ASTs in F#. The rec keyword is needed for functions that recurse over them.
# Ruby's nil and non-nil are implicit; F# makes it explicit
value = 42 # Some(42)
missing = nil # None
puts missing.nil?// Option<'T> is defined as:
// type Option<'T> = Some of 'T | None
let present: int option = Some 42
let absent: int option = None
printfn "%A" present
printfn "%A" absent
printfn "%b" (Option.isNone absent)Option<'T> is itself a discriminated union built into F#. The 'T is a generic type parameter. Understanding DUs explains why Some and None behave as they do — they are just case constructors of an ordinary DU.
class Animal
def initialize(name, sound)
@name = name
@sound = sound
end
def speak
puts "#{@name} says #{@sound}"
end
end
dog = Animal.new("Dog", "woof")
dog.speaktype Animal(name: string, sound: string) =
member _.Speak() = printfn "%s says %s" name sound
let dog = Animal("Dog", "woof")
dog.Speak()F# classes use a primary constructor directly in the type definition — constructor parameters are listed after the type name. member _.Method() defines an instance method; _ is the self reference (often written this). Constructor parameters are automatically in scope for all members.
class Person
attr_reader :name
attr_accessor :age
def initialize(name, age)
@name = name
@age = age
end
end
person = Person.new("Alice", 30)
puts person.name
person.age = 31
puts person.agetype Person(name: string, initialAge: int) =
let mutable age = initialAge
member _.Name = name
member _.Age
with get() = age
and set(value) = age <- value
let person = Person("Alice", 30)
printfn "%s" person.Name
person.Age <- 31
printfn "%d" person.AgeF# properties are defined with member. Read-only properties use member _.Name = value. Mutable properties require explicit get() and set(value) accessors. F# code typically prefers immutable records over mutable classes.
class Vehicle
def initialize(make)
@make = make
end
def describe = puts "Vehicle: #{@make}"
end
class Car < Vehicle
def initialize(make, model)
super(make)
@model = model
end
def describe = puts "Car: #{@make} #{@model}"
end
Car.new("Toyota", "Camry").describetype Vehicle(make: string) =
abstract member Describe: unit -> unit
default _.Describe() = printfn "Vehicle: %s" make
type Car(make: string, model: string) =
inherit Vehicle(make)
override _.Describe() = printfn "Car: %s %s" make model
let car = Car("Toyota", "Camry")
car.Describe()F# uses inherit for class inheritance. To override a method, the base class must declare it abstract with a default implementation. Derived classes use override. F# enforces this distinction explicitly — unlike Ruby, where any method can be overridden.
module Describable
def describe
raise NotImplementedError, "#{self.class} must implement describe"
end
end
class Product
include Describable
def initialize(name, price)
@name, @price = name, price
end
def describe = puts "#{@name}: $#{'%.2f' % @price}"
end
Product.new("Widget", 9.99).describetype IDescribable =
abstract member Describe: unit -> unit
type Product(name: string, price: float) =
interface IDescribable with
member _.Describe() = printfn "%s: $%.2f" name price
let product = Product("Widget", 9.99)
(product :> IDescribable).Describe()F# interfaces use abstract member declarations. Implementing them requires an explicit interface InterfaceName with block. Casting to an interface uses :> (upcast). F# modules and functions often replace interfaces for simpler cases.
pairs = (1..3).flat_map do |x|
(1..3).map { |y| [x, y] }
end.reject { |(x, y)| x == y }
puts pairs.inspectlet pairs = [
for x in 1..3 do
for y in 1..3 do
if x <> y then yield (x, y)
]
printfn "%A" pairsF# list comprehensions use [ for ... do ... yield ... ]. Multiple for loops create the cartesian product. <> is the not-equal operator. The yield keyword produces each element. This is more readable than Ruby's nested flat_map.
fibonacci = Enumerator.new do |yielder|
a, b = 0, 1
loop do
yielder << a
a, b = b, a + b
end
end
puts fibonacci.take(8).inspectlet fibonacci = seq {
let mutable a = 0
let mutable b = 1
while true do
yield a
let next = a + b
a <- b
b <- next
}
printfn "%A" (Seq.take 8 fibonacci |> Seq.toList)F# seq { } computation expressions create lazy sequences evaluated on demand, like Ruby's Enumerator. yield produces each value. Only as many elements as needed are computed — Seq.take 8 evaluates exactly 8 steps of the loop.
# Ruby uses threads or fibers for async patterns
result = Thread.new { 42 * 2 }.value
puts resultlet asyncComputation = async {
let value = 42
return value * 2
}
let result = Async.RunSynchronously asyncComputation
printfn "%d" resultF# async { } computation expressions define asynchronous workflows. let! binds the result of another async operation without blocking. Async.RunSynchronously runs the workflow to completion. F#'s native async predates C#'s async/await and served as its inspiration.
def try_parse(text)
Integer(text) rescue nil
end
def safe_divide(numerator, denominator)
denominator == 0 ? nil : numerator / denominator
end
result = try_parse("10")&.then { |n| safe_divide(n, 2) }
puts result// Using Option.bind to chain Option-returning functions:
let tryParse (text: string) =
match System.Int32.TryParse(text) with
| (true, value) -> Some value
| _ -> None
let safeDivide numerator denominator =
if denominator = 0 then None else Some (numerator / denominator)
let result =
tryParse "10"
|> Option.bind (fun number -> safeDivide number 2)
printfn "%A" resultOption.bind chains computations that return Option. When any step returns None, the entire chain short-circuits to None. This is equivalent to Ruby's safe navigation chain (&.then) but enforced by the type system at compile time.
module MathUtils
def self.square(number)
number * number
end
def self.cube(number)
number * number * number
end
end
puts MathUtils.square(4)
puts MathUtils.cube(3)module MathUtils =
let square number = number * number
let cube number = number * number * number
printfn "%d" (MathUtils.square 4)
printfn "%d" (MathUtils.cube 3)F# modules are namespaces for functions and types. They are defined with module ModuleName = followed by indented definitions. Module functions are called as ModuleName.functionName arg, equivalent to Ruby's module methods defined with self..
module Greeter
def self.greet(name)
puts "Hello, #{name}!"
end
end
include Greeter rescue nil
Greeter.greet("Alice")module Greeter =
let greet name = printfn "Hello, %s!" name
open Greeter
greet "Alice"open ModuleName makes all of a module's definitions available without qualification. This is like Ruby's include, but for functions rather than instance methods. Opening List, Seq, or Map is a common pattern in F# code that uses these modules heavily.
# In Ruby, top-level code runs directly as a script
name = "Alice"
puts "Hello, #{name}!"// F# top-level code (outside any module) runs as the entry point:
let name = "Alice"
printfn "Hello, %s!" nameF# top-level let bindings and expressions outside any module act as the program's entry point — no main function needed. This is similar to Ruby's script mode. Larger F# programs organize code into modules and use [<EntryPoint>] for the main function.
begin
result = 10 / 0
puts result
rescue ZeroDivisionError => error
puts "Error: #{error.message}"
endtry
let result = 10 / 0
printfn "%d" result
with
| :? System.DivideByZeroException as error ->
printfn "Error: %s" error.MessageF# uses try/with for exception handling. The :? operator performs a type test: :? ExceptionType as name is like Ruby's rescue ExceptionClass => variable. Multiple exception types can be matched with separate | arms.
begin
puts "doing work"
ensure
puts "cleanup always runs"
endtry
printfn "doing work"
finally
printfn "cleanup always runs"try/finally ensures the cleanup block runs whether or not an exception occurred — the equivalent of Ruby's ensure. In F#, try/with and try/finally cannot be combined in one expression; nest them when both are needed.
def validate_age(age)
raise ArgumentError, "Age must be positive" if age < 0
age
end
begin
validate_age(-5)
rescue ArgumentError => error
puts error.message
endlet validateAge age =
if age < 0 then
raise (System.ArgumentException("Age must be positive"))
age
try
validateAge -5 |> ignore
with
| :? System.ArgumentException as error ->
printfn "%s" error.Messageraise throws an exception in F#. .NET exception types are used — System.ArgumentException, System.InvalidOperationException, etc. The |> ignore discards a return value when only the side effect (the potential exception) is needed.
def parse_age(text)
age = Integer(text)
raise ArgumentError, "Age must be non-negative" if age < 0
age
rescue ArgumentError => error
puts "Error: #{error.message}"
nil
end
puts parse_age("thirty").inspectlet tryParseAge (text: string) =
match System.Int32.TryParse(text) with
| (true, age) when age >= 0 -> Ok age
| (true, _) -> Error "Age must be non-negative"
| _ -> Error $"'{text}' is not a valid number"
match tryParseAge "thirty" with
| Ok age -> printfn "age is %d" age
| Error message -> printfn "Error: %s" messageIdiomatic F# prefers Result<'T, 'E> over exceptions for expected failure paths — parsing, validation, and lookups. Exceptions are reserved for truly unexpected failures. Using Result makes the function's failure modes explicit in its type signature and forces callers to handle them.