Ruby.CodeCompared.To/Haskell

An interactive executable cheatsheet for Rubyists learning Haskell

Ruby 4.0 Haskell 9.12
Output & Basics
Hello World
puts "Hello, World!"
main :: IO () main = putStrLn "Hello, World!"

Every Haskell program's entry point is main :: IO (). The IO () type signals that this function performs side effects. putStrLn prints a string followed by a newline β€” the direct counterpart of Ruby's puts.

print vs putStrLn
puts "no quotes" p "with quotes" p 42
main :: IO () main = do putStrLn "no quotes" print "with quotes" print 42

putStrLn outputs a String as-is, just like Ruby's puts. print is equivalent to Ruby's p β€” it calls show on the value (adding quotes around strings) and prints the result. Use putStrLn for user-facing output and print for debugging.

Multiple print statements
puts "First" puts "Second" puts "Third"
main :: IO () main = do putStrLn "First" putStrLn "Second" putStrLn "Third"

The do block sequences multiple IO actions. Each line is executed in order, just like a Ruby method body. Haskell normally evaluates lazily, but IO actions in a do block are performed strictly from top to bottom.

Values & Bindings
let bindings
greeting = "Hello" count = 42 puts greeting p count
main :: IO () main = do let greeting = "Hello" let count = 42 :: Int putStrLn greeting print count

Haskell's let inside a do block creates an immutable local binding β€” there is no way to reassign it. Ruby's local variables are mutable; Haskell's are not. The :: Int annotation tells the compiler which numeric type to use when it cannot infer one.

where clause
def greeting_message prefix = "Hello" target = "from helper" "#{prefix} #{target}" end puts greeting_message
main :: IO () main = putStrLn greetingMessage where greetingMessage = prefix ++ " " ++ target prefix = "Hello" target = "from where"

The where clause attaches private definitions to a function β€” like giving a Ruby method private helper variables that are only visible in that scope. In Haskell where bindings can be mutually recursive and can themselves have their own where clauses. Ruby has no direct equivalent; the closest idiom is local variables or private helper methods.

Type annotations
# Ruby infers types at runtime (dynamic typing) count = 42 ratio = 3.14 p count p ratio
main :: IO () main = do let count = 42 :: Int let ratio = 3.14 :: Double print count print ratio

Haskell is statically typed and infers most types automatically, but you can annotate with :: Type to be explicit or to resolve ambiguity. Ruby never requires type annotations β€” types are checked at runtime, not compile time.

Building strings
name = "World" puts "Hello, #{name}!"
main :: IO () main = do let name = "World" putStrLn ("Hello, " ++ name ++ "!")

Haskell has no string interpolation. You concatenate strings with the ++ operator, or use show to convert non-string values. The Data.Text.printf and Text.Printf.printf modules provide C-style formatting as an alternative. For Rubyists the lack of interpolation is one of the first friction points in Haskell.

Types
Basic types
number = 42 ratio = 3.14 flag = true letter = 'A' p number p ratio p flag p letter
main :: IO () main = do let number = 42 :: Int let ratio = 3.14 :: Double let flag = True :: Bool let letter = 'A' :: Char print number print ratio print flag print letter

Haskell's basic types β€” Int, Double, Bool, Char β€” are similar to Ruby's, but declared at compile time. A notable difference is that Haskell's Char uses single quotes exclusively, while String (which is [Char]) uses double quotes.

Type inference
# Ruby resolves types at runtime count = 42 message = "inferred as String" puts "#{count} and #{message}"
main :: IO () main = do let count = 42 :: Int let message = "inferred as String" putStrLn (show count ++ " and " ++ message)

Haskell's type inference (the Hindley–Milner algorithm) figures out the type of every expression at compile time without requiring annotations. The compiler catches type mismatches before the program ever runs β€” something Ruby only discovers at runtime.

Numeric conversions
count = 7 ratio = count.to_f / 2.0 p ratio p ratio.floor p ratio.ceil
main :: IO () main = do let count = 7 :: Int let ratio = fromIntegral count / 2.0 :: Double print ratio print (floor ratio :: Int) print (ceiling ratio :: Int)

Haskell never implicitly converts between numeric types β€” you must use fromIntegral to promote an Int to a Double. This is more verbose than Ruby's .to_f but prevents silent precision loss. floor and ceiling need a type annotation on the result because they are polymorphic.

Boolean operations
sunny = true warm = false p sunny && warm p sunny || warm p !sunny
main :: IO () main = do let sunny = True let warm = False print (sunny && warm) print (sunny || warm) print (not sunny)

Haskell uses && and || for boolean operators, just like Ruby. The difference is that Haskell uses the function not instead of the ! prefix operator. Haskell's Bool is a proper algebraic data type with two constructors: True and False (capitalized).

Strings
String concatenation
first = "Hello" second = " World" puts first + second
main :: IO () main = do let first = "Hello" let second = " World" putStrLn (first ++ second)

Haskell uses ++ for list concatenation, and since String is just [Char], ++ concatenates strings too. Ruby uses +. The ++ operator works on any list, making string and list operations consistent β€” though for performance-critical code the Data.Text type is preferred.

String length
message = "Hello, World!" p message.length
main :: IO () main = do let message = "Hello, World!" print (length message)

Because String is [Char], length is the standard list length function β€” it counts characters. Be aware that this counts Unicode code points, not grapheme clusters, which matters for international text. The Data.Text package handles text encoding more robustly for production use.

String/number conversion
number_str = "42" number = Integer(number_str) p number + 8 puts number.to_s
main :: IO () main = do let numberStr = "42" let number = read numberStr :: Int print (number + 8) putStrLn (show number)

read parses a string into any type that implements the Read typeclass β€” it is the inverse of show. read throws a runtime exception on invalid input, so production code uses reads or Text.Read.readMaybe for safe parsing. Ruby's Integer() similarly raises on invalid strings.

words and lines
sentence = "one two three" p sentence.split multiline = "line one line two line three" p multiline.lines.map(&:chomp)
main :: IO () main = do let sentence = "one two three" print (words sentence) let multiline = "line one\nline two\nline three" print (lines multiline)

words splits on whitespace (like Ruby's split with no arguments) and lines splits on newlines (like Ruby's lines). Their inverses are unwords and unlines, which join with spaces and newlines respectively β€” the same role Ruby's join plays.

Reverse a string
message = "Haskell" puts message.reverse
main :: IO () main = do let message = "Haskell" putStrLn (reverse message)

Because String is [Char], the polymorphic list function reverse works on strings without any special string methods. This is a recurring Haskell pattern: functions that operate on lists automatically work on strings too.

Lists
List literals
numbers = [1, 2, 3, 4, 5] p numbers p numbers.first p numbers[1..]
main :: IO () main = do let numbers = [1, 2, 3, 4, 5] :: [Int] print numbers print (head numbers) print (tail numbers)

Haskell lists use the same [...] syntax as Ruby, but they are singly-linked lists under the hood β€” not arrays. head and tail correspond roughly to Ruby's first and [1..] slice. Calling head or tail on an empty list raises a runtime exception, so idiomatic Haskell prefers pattern matching.

The cons operator (:)
rest = [2, 3, 4] numbers = [1] + rest p numbers
main :: IO () main = do let rest = [2, 3, 4] :: [Int] let numbers = 1 : rest print numbers

The : operator (pronounced "cons") prepends a single element to a list. Because Haskell lists are singly-linked, cons is O(1). Ruby's + allocates a new array; Haskell's : just adds a node. All list literals are syntactic sugar for cons chains: [1,2,3] is 1:(2:(3:[])).

Range notation
p (1..10).to_a p (1..10).step(2).to_a p ('a'..'e').to_a
main :: IO () main = do print [1..10 :: Int] print [1,3..10 :: Int] print ['a'..'e']

Haskell's [start..end] syntax mirrors Ruby's ranges. The two-element form [start,step..end] specifies a step β€” equivalent to Ruby's step. Ranges work on any type that implements the Enum typeclass, including characters and integers. Haskell also supports infinite ranges like [1..] thanks to lazy evaluation.

List comprehensions
squares = (1..5).map { |x| x * x } p squares evens = (1..10).select { |x| x.even? } p evens
main :: IO () main = do let squares = [x * x | x <- [1..5 :: Int]] print squares let evens = [x | x <- [1..10 :: Int], even x] print evens

List comprehensions combine a generator (x <- list) with optional guards (conditions after the comma) to produce a new list. The syntax mirrors mathematical set notation. Ruby achieves the same thing with map and select chained together; Haskell's comprehension syntax combines both in a single expression.

map and filter
numbers = (1..5).to_a p numbers.map { |x| x * 2 } p numbers.select { |x| x.even? }
main :: IO () main = do let numbers = [1..5] :: [Int] print (map (* 2) numbers) print (filter even numbers)

map and filter are the Haskell counterparts of Ruby's map and select. In Haskell they are standalone functions rather than methods β€” you pass the list as the last argument. (* 2) is an operator section: a partially-applied multiplication, equivalent to Ruby's { |x| x * 2 }.

foldr β€” reduce/inject
numbers = (1..5).to_a total = numbers.inject(0) { |sum, x| sum + x } product_val = numbers.inject(1) { |prod, x| prod * x } p total p product_val
main :: IO () main = do let numbers = [1..5] :: [Int] let total = foldr (+) 0 numbers let productValue = foldr (*) 1 numbers print total print productValue

foldr (fold right) is the Haskell equivalent of Ruby's inject/reduce. It takes a combining function, an initial value, and a list. foldl' (strict fold left, from Data.List) is more efficient for large lists because it does not build up a chain of thunks. foldr is preferred for lazy or infinite structures.

Common list functions
numbers = [3, 1, 4, 1, 5, 9, 2, 6] p numbers.length p numbers.sum p numbers.max p numbers.min p numbers.reverse
main :: IO () main = do let numbers = [3, 1, 4, 1, 5, 9, 2, 6] :: [Int] print (length numbers) print (sum numbers) print (maximum numbers) print (minimum numbers) print (reverse numbers)

Haskell's standard list functions length, sum, maximum, minimum, and reverse map directly onto their Ruby counterparts. They are ordinary functions rather than methods, so they work on any list β€” including strings, since String is [Char].

Infinite lists (lazy evaluation)
naturals = (1..).lazy p naturals.first(5) squares = (1..).lazy.map { |n| n ** 2 } p squares.first(5)
main :: IO () main = do print (take 5 [1..] :: [Int]) print (take 5 (map (^2) [1..]) :: [Int])

Haskell evaluates lists lazily by default β€” elements are only computed when needed. This allows infinite lists like [1..] to exist as first-class values; take 5 forces only the first five. Ruby requires explicit .lazy enumerators to achieve the same effect. Lazy evaluation is one of Haskell's most distinctive and powerful features.

Tuples
Tuple creation and access
point = [3, 4] p point p point[0] p point[1]
main :: IO () main = do let point = (3, 4) :: (Int, Int) print point print (fst point) print (snd point)

Haskell tuples are fixed-size, heterogeneous containers whose size and types are part of the type signature β€” (Int, String) and (String, Int) are different types. Ruby uses arrays where Haskell would use tuples. fst and snd extract the first and second elements of a pair; for larger tuples you use pattern matching.

Tuples with multiple values
person = ["Alice", 30, true] name, age, active = person puts name p age p active
main :: IO () main = do let person = ("Alice", 30, True) :: (String, Int, Bool) let (name, age, active) = person putStrLn name print age print active

Destructuring a tuple with let (a, b, c) = tuple works like Ruby's multiple assignment. Haskell's type system makes the structure explicit β€” (String, Int, Bool) is a distinct type from (Int, String, Bool). This lets the compiler catch mistakes that would only surface as runtime errors in Ruby.

zip β€” pairing lists
names = ["Alice", "Bob", "Carol"] scores = [95, 87, 92] pairs = names.zip(scores) p pairs p pairs.map { |name, _score| name }
main :: IO () main = do let names = ["Alice", "Bob", "Carol"] let scores = [95, 87, 92] :: [Int] let pairs = zip names scores print pairs print (map fst pairs)

zip combines two lists into a list of pairs, stopping at the shorter list β€” just like Ruby's zip. In Haskell, map fst pairs extracts the first element from each pair (equivalent to map(&:first) in Ruby). The combination of zip and map fst/snd is idiomatic for working with paired data.

Control Flow
if/then/else expression
temperature = 22 weather = temperature > 20 ? "warm" : "cool" puts weather
main :: IO () main = do let temperature = 22 :: Int let weather = if temperature > 20 then "warm" else "cool" putStrLn weather

In Haskell if/then/else is an expression, not a statement β€” it always produces a value, and the else branch is mandatory. This is similar to Ruby's ternary operator ? : rather than Ruby's if/end statement. Both branches must have the same type.

Guards
def classify(number) if number < 0 then "negative" elsif number == 0 then "zero" elsif number < 10 then "small" else "large" end end puts classify(-5) puts classify(0) puts classify(7) puts classify(42)
classify :: Int -> String classify number | number < 0 = "negative" | number == 0 = "zero" | number < 10 = "small" | otherwise = "large" main :: IO () main = do putStrLn (classify (-5)) putStrLn (classify 0) putStrLn (classify 7) putStrLn (classify 42)

Guards (|) are Haskell's idiomatic multi-branch conditional β€” cleaner than chained if/else. Each guard is a boolean expression; otherwise is just True and acts as the catch-all. Guards can appear in function definitions, case expressions, and list comprehensions. They map naturally onto Ruby's if/elsif/else chains.

case expression
day = 3 name = case day when 1 then "Monday" when 2 then "Tuesday" when 3 then "Wednesday" else "Other" end puts name
main :: IO () main = do let day = 3 :: Int let name = case day of 1 -> "Monday" 2 -> "Tuesday" 3 -> "Wednesday" _ -> "Other" putStrLn name

Haskell's case expression matches on values like Ruby's case/when, but _ is the wildcard rather than else. Crucially, Haskell's case is exhaustiveness-checked β€” the compiler warns (or errors) if you leave out a case. Ruby's case silently returns nil for unmatched values.

let...in expression
result = begin base = 10 multiplier = 3 base * multiplier end p result
main :: IO () main = do let result = let base = 10 :: Int multiplier = 3 in base * multiplier print result

The let...in form is the pure-expression version of let in a do block. It introduces local bindings that are visible only in the in body and evaluates to the body's value. Ruby's begin...end serves a similar purpose for grouping expressions, though without name-scoping.

Pattern Matching
Matching list shapes
def describe_list(list) case list in [] then "empty" in [_] then "one element" in [_, _] then "two elements" else "many elements" end end puts describe_list([]) puts describe_list([1]) puts describe_list([1, 2]) puts describe_list([1, 2, 3])
describeList :: [Int] -> String describeList [] = "empty" describeList [_] = "one element" describeList [_, _] = "two elements" describeList _ = "many elements" main :: IO () main = do putStrLn (describeList []) putStrLn (describeList [1]) putStrLn (describeList [1, 2]) putStrLn (describeList [1, 2, 3])

Haskell functions can have multiple equations, each matching a different pattern. The runtime tries each equation top-to-bottom and uses the first that matches. This is more expressive than Ruby's traditional case/when (which only tests equality) because patterns can match on structure. Ruby 3+ case/in is the closest Ruby equivalent. The compiler warns if your patterns are non-exhaustive.

Matching head and tail
def first_two(list) case list in [] then "Empty" in [x] then "Only: #{x}" in [x, y, *] then "First: #{x}, Second: #{y}" end end puts first_two([1, 2, 3, 4]) puts first_two([42]) puts first_two([])
firstTwo :: [Int] -> String firstTwo (x:y:_) = "First: " ++ show x ++ ", Second: " ++ show y firstTwo (x:_) = "Only: " ++ show x firstTwo [] = "Empty" main :: IO () main = do putStrLn (firstTwo [1, 2, 3, 4]) putStrLn (firstTwo [42]) putStrLn (firstTwo [])

The cons pattern (x:rest) binds the head element to x and the tail list to rest. Nesting it as (x:y:_) matches at least two elements. This mirrors Ruby 3's [x, y, *] pattern in case/in, but in Haskell it is the primary way to process lists recursively.

Matching tuples
def add_pair(pair) left, right = pair left + right end p add_pair([3, 4]) pairs = [[1, 2], [3, 4], [5, 6]] p pairs.map { |pair| add_pair(pair) }
addPair :: (Int, Int) -> Int addPair (left, right) = left + right main :: IO () main = do print (addPair (3, 4)) let pairs = [(1, 2), (3, 4), (5, 6)] :: [(Int, Int)] print (map addPair pairs)

Tuple patterns destructure the tuple directly in the function parameter list β€” no separate destructuring statement needed. This is similar to Ruby's multiple assignment left, right = pair, but Haskell's compiler verifies that the tuple has exactly the right number of elements at compile time.

Matching on literal values
def greet(name) case name when "Alice" then "Hello, Alice! Special greeting for you." when "Bob" then "Hey Bob!" else "Hi, #{name}!" end end puts greet("Alice") puts greet("Bob") puts greet("Carol")
greet :: String -> String greet "Alice" = "Hello, Alice! Special greeting for you." greet "Bob" = "Hey Bob!" greet name = "Hi, " ++ name ++ "!" main :: IO () main = do putStrLn (greet "Alice") putStrLn (greet "Bob") putStrLn (greet "Carol")

Haskell function equations can match on literal string, integer, or character values. The last equation with a variable name (name) acts as the catch-all, binding the unmatched argument. This is more readable than a sequence of if checks and directly maps to Ruby's case/when with string matches.

As-patterns (@)
def first_and_all(list) return "empty" if list.empty? "First: #{list.first}, All: #{list.inspect}" end puts first_and_all([1, 2, 3]) puts first_and_all([42])
firstAndAll :: [Int] -> String firstAndAll [] = "empty" firstAndAll list@(first:_) = "First: " ++ show first ++ ", All: " ++ show list main :: IO () main = do putStrLn (firstAndAll [1, 2, 3]) putStrLn (firstAndAll [42])

The @ pattern (pronounced "as") binds the whole matched value to a name while also destructuring it. list@(first:_) binds the whole list to list and the first element to first simultaneously. This avoids the need to reconstruct the original value after destructuring β€” a convenience with no direct Ruby equivalent.

Functions
Function definitions
def double(number) = number * 2 def square(number) = number * number p double(7) p square(5)
double :: Int -> Int double number = number * 2 square :: Int -> Int square number = number * number main :: IO () main = do print (double 7) print (square 5)

Haskell function definitions use the form name argument = body with no parentheses around arguments and no return keyword β€” the body expression is the return value. Function application is written with a space: double 7, not double(7). Type signatures like double :: Int -> Int are optional but strongly recommended.

Lambda expressions
double = ->(number) { number * 2 } add = ->(x, y) { x + y } p double.call(5) p add.call(3, 4)
main :: IO () main = do let double = (\number -> number * 2) :: Int -> Int let add = (\x y -> x + y) :: Int -> Int -> Int print (double 5) print (add 3 4)

Haskell lambdas use a backslash (resembling Ξ») followed by parameters and -> to the body. A multi-parameter lambda \x y -> ... is syntactic sugar for a nested single-parameter lambda \x -> (\y -> ...). Ruby's ->(x, y) {} packs multiple parameters into a single closure; Haskell's currying keeps them separate.

Function composition (.)
negate_fn = ->(x) { -x } double_fn = ->(x) { x * 2 } double_then_negate = negate_fn << double_fn p double_then_negate.call(5) reverse_words = ->(sentence) { sentence.split.reverse } p reverse_words.call("hello world foo")
main :: IO () main = do let doubleAndNegate = negate . (* 2) print (doubleAndNegate (5 :: Int)) let reverseWords = reverse . words print (reverseWords "hello world foo")

The . operator composes two functions: (f . g) x is f (g x). It reads right-to-left β€” negate . (* 2) first doubles, then negates. Ruby 2.6+ has << and >> for proc composition, but Haskell's . is deeply idiomatic and used throughout real code.

The $ operator
# Ruby uses parentheses; no $ operator needed p (1..5).map { |x| x * 2 }.sum puts "Result: #{42}"
main :: IO () main = do -- Without $: print (sum (map (* 2) [1..5])) print $ sum $ map (* 2) [1..5 :: Int] putStrLn $ "Result: " ++ show (42 :: Int)

The $ operator is just function application with the lowest possible precedence. f $ g $ x is f (g x) β€” it eliminates nested parentheses by associating to the right. Ruby's method chaining achieves the same readability goal but reads left-to-right, while $ chains read right-to-left.

Partial application and sections
add_five = ->(number) { number + 5 } double = ->(number) { number * 2 } p [1, 2, 3].map { |n| add_five.call(n) } p [1, 2, 3].map { |n| double.call(n) }
main :: IO () main = do let addFive = (+ 5) :: Int -> Int let double = (* 2) :: Int -> Int print (map addFive [1, 2, 3]) print (map double [1, 2, 3])

Every Haskell function is automatically curried β€” applying it to fewer arguments than it expects produces a new function. Operator sections like (+ 5) and (* 2) partially apply the operator, creating a new one-argument function. Ruby achieves the same effect with lambdas or method(:name).curry, but in Haskell it is effortless and pervasive.

Functions as arguments
def apply_twice(operation, value) operation.call(operation.call(value)) end p apply_twice(->(x) { x * 2 }, 3) p apply_twice(->(x) { x + 10 }, 5)
applyTwice :: (Int -> Int) -> Int -> Int applyTwice operation value = operation (operation value) main :: IO () main = do print (applyTwice (* 2) 3) print (applyTwice (+ 10) 5)

Functions that take other functions as arguments are first-class in Haskell. The type signature (Int -> Int) -> Int -> Int explicitly shows that the first argument is a function. Ruby requires lambdas or procs to pass callables; in Haskell any function β€” including operator sections β€” can be passed directly.

Higher-Order Functions
zipWith β€” combine two lists
prices = [10, 20, 30] quantities = [3, 2, 4] totals = prices.zip(quantities).map { |price, quantity| price * quantity } p totals p totals.sum
main :: IO () main = do let prices = [10, 20, 30] :: [Int] let quantities = [3, 2, 4] :: [Int] let totals = zipWith (*) prices quantities print totals print (sum totals)

zipWith combines two lists element-by-element using a function β€” it is zip followed by map in one step. Ruby needs zip(...).map { } to achieve the same result. zipWith (+) adds lists element-wise, zipWith max gives element-wise maxima, and so on.

any and all
numbers = [1, 3, 5, 8, 9] p numbers.any?(&:even?) p numbers.all?(&:odd?) p numbers.any? { |n| n > 7 } p numbers.all? { |n| n > 0 }
main :: IO () main = do let numbers = [1, 3, 5, 8, 9] :: [Int] print (any even numbers) print (all odd numbers) print (any (> 7) numbers) print (all (> 0) numbers)

any and all are Haskell's counterparts of Ruby's any? and all?. They take a predicate function rather than a block. Haskell's lazy evaluation makes them short-circuit: any stops at the first True, all stops at the first False β€” just like Ruby.

flip β€” reverse argument order
halves = [10, 20, 30, 40].map { |number| number / 2 } p halves subtract_from_ten = ->(number) { 10 - number } p subtract_from_ten.call(3)
main :: IO () main = do let halves = map (flip div 2) [10, 20, 30, 40] :: [Int] print halves let subtractFromTen = flip (-) (3 :: Int) print (subtractFromTen 10)

flip f x y calls f y x β€” it reverses the order of the first two arguments. This is useful when partial application needs the "wrong" argument fixed first. For example, flip div 2 creates a function that divides its argument by 2, even though div's first parameter is the dividend. Ruby handles this with lambdas or curry.

concatMap β€” flat_map
sentences = ["hello world", "foo bar baz"] all_words = sentences.flat_map { |sentence| sentence.split } p all_words numbers = (1..4).to_a expanded = numbers.flat_map { |n| [n, n * n] } p expanded
main :: IO () main = do let sentences = ["hello world", "foo bar baz"] let allWords = concatMap words sentences print allWords let numbers = [1..4] :: [Int] let expanded = concatMap (\n -> [n, n * n]) numbers print expanded

concatMap f list maps f over the list and then concatenates the results β€” it is the Haskell name for what Ruby calls flat_map. It is also the implementation of the list monad's bind operation: concatMap is (>>=) for lists, making it a fundamental building block for list comprehensions.

iterate and takeWhile
powers = [] current = 1 while current <= 100 powers << current current *= 2 end p powers countdown = 10.downto(6).to_a p countdown
main :: IO () main = do let powers = takeWhile (<= 100) (iterate (* 2) (1 :: Int)) print powers let countdown = take 5 (iterate (subtract 1) (10 :: Int)) print countdown

iterate f x produces the infinite list [x, f x, f (f x), ...]. Combined with takeWhile or take, it replaces imperative loops that accumulate values. This is idiomatic Haskell: express the series you want, then take what you need β€” no mutable loop variable required.

Maybe & Either
Maybe β€” safe nullable values
def safe_head(list) return nil if list.empty? list.first end p safe_head([1, 2, 3]) p safe_head([])
safeHead :: [Int] -> Maybe Int safeHead [] = Nothing safeHead (x:_) = Just x main :: IO () main = do print (safeHead [1, 2, 3]) print (safeHead [])

Maybe a is Haskell's type-safe alternative to nil. A value of Maybe Int is either Just n (a present integer) or Nothing (absent). Unlike Ruby's nil, the type system forces you to handle both cases β€” you cannot call + 1 on a Maybe Int without unwrapping it first.

fromMaybe β€” default values
def safe_head(list) list.first # returns nil for empty arrays end result1 = safe_head([42, 1, 2]) || 0 result2 = safe_head([]) || 0 p result1 p result2
import Data.Maybe (fromMaybe) safeHead :: [Int] -> Maybe Int safeHead [] = Nothing safeHead (x:_) = Just x main :: IO () main = do let result1 = fromMaybe 0 (safeHead [42, 1, 2]) let result2 = fromMaybe 0 (safeHead []) print result1 print result2

fromMaybe defaultValue maybeValue unwraps Just x to x, or returns the default if Nothing. It is the Haskell equivalent of Ruby's || default idiom. The key difference is that the compiler tracks whether a value is Maybe β€” you cannot accidentally forget to handle the missing case.

Pattern matching on Maybe
def describe_result(value) case value when nil then "No result" else "Got: #{value}" end end puts describe_result(42) puts describe_result(nil)
describeResult :: Maybe Int -> String describeResult Nothing = "No result" describeResult (Just n) = "Got: " ++ show n main :: IO () main = do putStrLn (describeResult (Just 42)) putStrLn (describeResult Nothing)

Pattern matching on Maybe is the idiomatic way to handle both cases. The compiler enforces exhaustiveness β€” if you forget to handle Nothing, you get a warning. Ruby's case/when nil is conventional but nothing prevents forgetting the nil branch.

Either β€” typed errors
def safe_divide(numerator, denominator) return [:error, "Division by zero"] if denominator == 0 [:ok, numerator / denominator] end p safe_divide(10, 2) p safe_divide(10, 0)
safeDivide :: Int -> Int -> Either String Int safeDivide _ 0 = Left "Division by zero" safeDivide numerator denominator = Right (numerator `div` denominator) main :: IO () main = do print (safeDivide 10 2) print (safeDivide 10 0)

Either String Int holds either a Left String (an error message) or a Right Int (a success value). It is Haskell's typed alternative to exceptions β€” the error path is visible in the type signature. Ruby conventionally uses tagged arrays [:ok, value] or raises exceptions; Haskell makes the two outcomes part of the type.

fmap over Maybe
maybe_number = 5 doubled = maybe_number&.*(2) p doubled nothing = nil p nothing&.*(2)
main :: IO () main = do let maybeNumber = Just (5 :: Int) let doubled = fmap (* 2) maybeNumber print doubled let nothing = Nothing :: Maybe Int print (fmap (* 2) nothing)

fmap f (Just x) returns Just (f x); fmap f Nothing returns Nothing. This is the same as Ruby's safe navigation operator &. β€” the function is applied only when the value is present. The power is that fmap is defined by the Functor typeclass and works on lists, Either, IO, and any other container.

Algebraic Data Types
Sum types (enumerations)
def describe(color) case color when :red then "warm red" when :green then "cool green" when :blue then "calm blue" end end p :red puts describe(:green)
data Color = Red | Green | Blue deriving (Show) describe :: Color -> String describe Red = "warm red" describe Green = "cool green" describe Blue = "calm blue" main :: IO () main = do print Red putStrLn (describe Green)

Haskell's data keyword defines an algebraic data type. A sum type like Color enumerates all possible values β€” similar to Ruby symbols, but typed. deriving (Show) automatically generates a string representation. The compiler exhaustiveness-checks case and pattern matches against the type's constructors.

Constructors with data
Circle = Data.define(:radius) Rectangle = Data.define(:width, :height) def area(shape) case shape in Circle[radius:] then Math::PI * radius ** 2 in Rectangle[width:, height:] then width * height end end p area(Circle.new(radius: 5.0)) p area(Rectangle.new(width: 4.0, height: 6.0))
data Shape = Circle Double | Rectangle Double Double deriving (Show) area :: Shape -> Double area (Circle radius) = pi * radius * radius area (Rectangle width height) = width * height main :: IO () main = do let circle = Circle 5.0 let rectangle = Rectangle 4.0 6.0 print (area circle) print (area rectangle)

Constructors can carry data β€” Circle Double wraps a radius, Rectangle Double Double wraps width and height. Pattern matching in function equations unpacks the values. This is Haskell's equivalent of Ruby's Data.define value objects, but the type is a single coherent union rather than separate classes.

Record syntax
Person = Data.define(:name, :age) person = Person.new(name: "Alice", age: 30) puts person.name p person.age p person
data Person = Person { personName :: String , personAge :: Int } deriving (Show) main :: IO () main = do let person = Person { personName = "Alice", personAge = 30 } putStrLn (personName person) print (personAge person) print person

Record syntax adds named field accessors to a data type. personName person works like Ruby's person.name. Haskell also generates a record update syntax: person { personAge = 31 } creates a new Person with only the age changed β€” immutable update that mirrors Ruby's Data#with.

newtype wrappers
Name = Data.define(:value) Age = Data.define(:value) def greet(name, age) "Hello, #{name.value}! You are #{age.value} years old." end puts greet(Name.new(value: "Alice"), Age.new(value: 30))
newtype Name = Name String deriving (Show) newtype Age = Age Int deriving (Show) greet :: Name -> Age -> String greet (Name name) (Age age) = "Hello, " ++ name ++ "! You are " ++ show age ++ " years old." main :: IO () main = do putStrLn (greet (Name "Alice") (Age 30))

newtype creates a distinct type from an existing one with zero runtime overhead β€” the wrapper is erased after type-checking. It prevents accidentally passing a raw String where a Name is expected. Ruby achieves a similar effect with thin wrapper classes or Data.define, but the guarantee is conventional rather than compiler-enforced.

Type Classes
Show and Read
number = 42 as_string = number.to_s puts as_string back_to_number = as_string.to_i p back_to_number + 1
main :: IO () main = do let number = 42 :: Int let asString = show number putStrLn asString let backToNumber = read asString :: Int print (backToNumber + 1)

Show and Read are typeclasses that define how a type converts to and from a string representation. show is like Ruby's to_s and inspect; read is like Ruby's Integer() but generic over any readable type. deriving (Show, Read) generates these instances automatically for data types.

Eq and Ord
numbers = [3, 1, 4, 1, 5, 9, 2, 6] p numbers.min p numbers.max p 3 == 3 p 3 <=> 5
main :: IO () main = do let numbers = [3, 1, 4, 1, 5, 9, 2, 6] :: [Int] print (minimum numbers) print (maximum numbers) print (3 == (3 :: Int)) print (compare (3 :: Int) 5)

The Eq typeclass provides == and /= (not-equal); Ord provides <, >, compare, minimum, and maximum. compare returns an Ordering value: LT, EQ, or GT β€” the equivalent of Ruby's <=> spaceship operator but typed.

Defining a typeclass
module Describable def describe = raise NotImplementedError end class Season include Describable DESCRIPTIONS = { spring: "flowers bloom", summer: "sun shines", autumn: "leaves fall", winter: "snow falls" } def initialize(name) = @name = name def describe = DESCRIPTIONS[@name] end puts Season.new(:spring).describe puts Season.new(:winter).describe
class Describable a where describe :: a -> String data Season = Spring | Summer | Autumn | Winter instance Describable Season where describe Spring = "flowers bloom" describe Summer = "sun shines" describe Autumn = "leaves fall" describe Winter = "snow falls" main :: IO () main = do putStrLn (describe Spring) putStrLn (describe Winter)

Typeclasses define an interface that types can implement β€” like Ruby's modules used as mixins, but enforced by the type system. class Describable a where declares the interface; instance Describable Season where implements it. The compiler verifies every instance provides all required methods and rejects uses that mix incompatible types.

Functor β€” fmap over containers
numbers = [1, 2, 3, 4, 5] doubled = numbers.map { |n| n * 2 } p doubled maybe_value = 10 tripled = maybe_value.then { |n| n * 3 } p tripled
main :: IO () main = do let numbers = [1, 2, 3, 4, 5] :: [Int] let doubled = fmap (* 2) numbers print doubled let maybeValue = Just (10 :: Int) let tripled = fmap (* 3) maybeValue print tripled

The Functor typeclass defines fmap :: (a -> b) -> f a -> f b β€” the ability to apply a function inside a container without unwrapping it. Lists, Maybe, Either, and IO are all functors. Ruby's map on arrays and then/yield_self on objects serve similar roles, but they are not unified under a single abstraction.

Do Notation & IO
Sequential IO with do
greeting = "Hello" target = "Ruby" message = "#{greeting}, #{target}!" puts message print "Count: " p message.length
main :: IO () main = do let greeting = "Hello" let target = "Haskell" let message = greeting ++ ", " ++ target ++ "!" putStrLn message putStr "Count: " print (length message)

The do notation sequences IO actions, making Haskell's otherwise pure, lazy code execute steps in order. putStr prints without a newline (like Ruby's print); putStrLn adds one (like Ruby's puts). Under the hood, do is syntactic sugar for >>= (bind) chains.

Binding IO results with <-
def get_doubled(number) number * 2 end doubled = get_doubled(21) puts "Doubled: #{doubled}" tripled = 21 * 3 puts "Tripled: #{tripled}"
getDoubled :: Int -> IO Int getDoubled number = return (number * 2) main :: IO () main = do doubled <- getDoubled 21 putStrLn ("Doubled: " ++ show doubled) tripled <- return (21 * 3 :: Int) putStrLn ("Tripled: " ++ show tripled)

The <- arrow inside a do block "extracts" a value from an IO action and binds it to a name. doubled <- getDoubled 21 runs the IO action and gives the resulting Int the name doubled. Ruby's assignment result = method_call is the equivalent; the difference is that <- only works inside monadic context.

Combining do, let, and where
def compute_stats(numbers) total = numbers.sum count = numbers.length average = total / count [total, count, average] end numbers = [10, 20, 30, 40, 50] total, count, average = compute_stats(numbers) puts "Total: #{total}" puts "Count: #{count}" puts "Average: #{average}"
computeStats :: [Int] -> (Int, Int, Int) computeStats numbers = (total, count, average) where total = sum numbers count = length numbers average = total `div` count main :: IO () main = do let numbers = [10, 20, 30, 40, 50] :: [Int] let (total, count, average) = computeStats numbers putStrLn ("Total: " ++ show total) putStrLn ("Count: " ++ show count) putStrLn ("Average: " ++ show average)

The backtick syntax x `div` y turns any two-argument function into an infix operator β€” total `div` count is the same as div total count. This mirrors the readability goal of Ruby's method chaining. Returning a tuple of computed values is Haskell's lightweight alternative to returning a struct or hash.

return in IO
def always_forty_two 42 end number = always_forty_two p number puts "The answer is #{number}"
alwaysFortyTwo :: IO Int alwaysFortyTwo = return 42 main :: IO () main = do number <- alwaysFortyTwo print number putStrLn ("The answer is " ++ show number)

return in Haskell wraps a pure value in an IO context β€” it does not exit the function early as in Ruby. return 42 creates an IO Int action that, when executed, produces 42 without any side effects. This is often surprising for Ruby developers who expect return to control flow.

mapM_ β€” IO over a list
fruits = ["apple", "banana", "cherry"] fruits.each { |fruit| puts fruit }
main :: IO () main = do let fruits = ["apple", "banana", "cherry"] mapM_ putStrLn fruits

mapM_ applies an IO-returning function to each element of a list and sequences the actions in order, discarding the results. The trailing _ signals that return values are ignored β€” the same convention as Ruby's each versus map. mapM (without the underscore) collects results into a list of IO actions.

Modules
Importing from Data.List
# Ruby's Array has sort, uniq, join built-in numbers = [3, 1, 4, 1, 5, 9, 2, 6, 5] p numbers.sort p numbers.uniq items = ["one", "two", "three"] puts items.join(", ")
import Data.List (sort, nub, intercalate) main :: IO () main = do let numbers = [3, 1, 4, 1, 5, 9, 2, 6, 5] :: [Int] print (sort numbers) print (nub numbers) let items = ["one", "two", "three"] putStrLn (intercalate ", " items)

Data.List provides sort (like Ruby's sort), nub (remove duplicates, like Ruby's uniq), and intercalate (join with a separator, like Ruby's join). Imports are explicit in Haskell β€” listing names in parentheses like (sort, nub) prevents name collisions and makes dependencies obvious.

Qualified imports β€” Data.Map
scores = { "Alice" => 95, "Bob" => 87, "Carol" => 92 } p scores p scores["Alice"] p scores.size
import qualified Data.Map.Strict as Map main :: IO () main = do let scores = Map.fromList [("Alice", 95), ("Bob", 87), ("Carol", 92)] :: Map.Map String Int print scores print (Map.lookup "Alice" scores) print (Map.size scores)

import qualified ... as Map imports the module but requires the Map. prefix on every function β€” preventing clashes with Prelude functions like lookup and null. Data.Map.Strict is a balanced binary search tree (O(log n) operations), equivalent to Ruby's Hash for key-value storage. Map.lookup returns Maybe, not nil.

Importing from Data.Char
message = "Hello, World! 123" puts message.upcase puts message.chars.select { |char| char.match?(/[a-zA-Z]/) }.join puts message.chars.select { |char| char.match?(/d/) }.join
import Data.Char (toUpper, isAlpha, isDigit) main :: IO () main = do let message = "Hello, World! 123" putStrLn (map toUpper message) putStrLn (filter isAlpha message) putStrLn (filter isDigit message)

Data.Char provides character predicates and transformers. Because String is [Char], applying map toUpper to a string works exactly like filter isAlpha on a list β€” list functions and string functions are the same functions. Ruby needs regex or explicit character class checks; Haskell uses composable predicates.