PONY λ M2 Modula-2

Haskell.CodeCompared.To/Scheme

An interactive executable cheatsheet comparing Haskell and Scheme

GHC 9.12 BiwaScheme 0.8.3 (R7RS-ish)
Output & Basics
Hello, World
main :: IO () main = putStrLn "Hello, World!"
(display "Hello, World!") (newline)
Haskell requires a named main :: IO () entry point that the runtime calls once. Scheme has no entry-point requirement at all — top-level forms simply execute in the order they are read, closer to a scripting language. display prints without a trailing newline, so (newline) is a separate, explicit call.
Sequencing multiple outputs
main :: IO () main = do putStrLn "First" putStrLn "Second" putStrLn "Third"
(display "First") (newline) (display "Second") (newline) (display "Third") (newline)
Haskell needs do-notation to sequence IO actions, since it desugars to chained monadic binds. Scheme is strict and impure by default, so top-level expressions simply execute one after another in the order written — no monad or block syntax required for plain sequencing.
Printing a value for inspection
main :: IO () main = print (Just [1, 2, 3])
(write (list 1 2 3)) (newline)
Haskell's print uses the Show typeclass, deriving a textual representation for (almost) any value. Scheme has no typeclass system to derive anything from at all — write is a built-in primitive that renders any value in a re-readable form (strings get quoted, for instance), the closest Scheme equivalent, but a plain built-in rather than something types opt into.
Values & Bindings
define is Haskell-style single-assignment binding
main :: IO () main = let radius = 5.0 area = pi * radius * radius in print area
(define radius 5.0) (define area (* 3.14159 radius radius)) (display area) (newline)
Haskell's let ... in bindings and Scheme's top-level define both introduce a name bound to a value — define is not an assignment statement, it is a declaration, closer in spirit to Haskell's let than to a mutable variable.
Opting in to mutation with set!
import Data.IORef main :: IO () main = do ref <- newIORef 0 modifyIORef ref (+1) value <- readIORef ref print value
(define count 0) (set! count (+ count 1)) (display count) (newline)
Haskell reaches for IORef and threads it explicitly through IO with newIORef/modifyIORef/readIORef. Scheme's set! offers plain, in-place mutation of an existing binding directly — the trailing ! is a Scheme naming CONVENTION marking any procedure that mutates state, not special syntax, but set! itself is a genuine special form built into the language.
Static Types vs. Fully Dynamic
No type system whatsoever — a first for this site's Haskell targets
add :: Int -> Int -> Int add x y = x + y main :: IO () main = print (add 2 3)
(define (add x y) (+ x y)) (display (add 2 3)) (newline)
This is the deepest throughline of this page. Every other Haskell target on the site — F#, Roc, ReScript, Scala, Swift, Kotlin, Rust — is still statically typed, even when it lacks type classes. Scheme has no static type system at all: add has no declared or inferred type whatsoever, and (add "two" 3) would compile (there is no separate compile step at all) and only fail — or silently produce nonsense — the moment that exact call actually runs.
Type predicates check at runtime, not compile time
describe :: Int -> String describe n = "an Int: " ++ show n main :: IO () main = putStrLn (describe 42)
(define (describe value) (cond ((number? value) (string-append "a number: " (number->string value))) ((string? value) (string-append "a string: " value)) (else "something else"))) (display (describe 42)) (newline)
Haskell's Int annotation is checked once, at compile time, and never needs re-checking at runtime. Scheme has no such compile-time guarantee, so idiomatic Scheme code frequently checks a value's runtime type with predicates like number?, string?, and pair? — a defensive style Haskell almost never needs, since the type checker already ruled out the wrong-type case before the program ever runs.
Strings & Symbols
String concatenation
main :: IO () main = putStrLn ("Hello, " ++ "World!")
(display (string-append "Hello, " "World!")) (newline)
Haskell's ++ is the general list-concatenation operator, since a String is really [Char]. Scheme's string-append is a dedicated string procedure — Scheme strings are their own distinct type, not a list of characters, so a general list operator would not apply to them at all.
Symbols: a genuinely new data type for Haskell
-- Haskell has nothing structurally equivalent to a Scheme symbol — the -- closest analogue is a nullary data constructor, which must be -- declared up front in a "data" statement before it can be used: data Status = Ok | Failed deriving (Show, Eq) main :: IO () main = print (Ok == Ok)
(display (eq? 'ok 'ok)) (newline)
A Scheme symbol like 'ok is an interned, immutable atomic value — spring into existence at the point of use, compared by identity with eq?, with no declaration required anywhere. Haskell has no equivalent data type at all; the closest comparison is a nullary constructor of a data type, which — unlike a Scheme symbol — must be declared upfront and belongs to one specific, closed type rather than a shared, open universe of interned atoms.
Lists vs. Pairs & Lists
Lists and the cons pattern — one of the closest matches on the site
main :: IO () main = let (first:rest) = [1, 2, 3] in print (first, rest)
(define numbers (list 1 2 3)) (define first (car numbers)) (define rest (cdr numbers)) (display (list first rest)) (newline)
Both languages model a list as a chain of cons cells terminated by an empty list — genuinely the same underlying representation, unlike most other languages on this site. Haskell's (first:rest) pattern destructures directly; Scheme has no pattern-matching syntax for this, so car (first element) and cdr (the rest) are called explicitly — the very names car/cdr come from 1950s IBM 704 hardware registers, a historical artifact with no Haskell parallel.
map and filter
main :: IO () main = let doubled = map (* 2) [1, 2, 3, 4, 5] evens = filter even doubled in print evens
(define doubled (map (lambda (n) (* n 2)) (list 1 2 3 4 5))) (define evens (filter (lambda (n) (= (remainder n 2) 0)) doubled)) (display evens) (newline)
Both languages call these operations map and filter, with the same argument order (function, then list) — a genuinely close match, both being languages that treat lists as a foundational data structure rather than an afterthought.
Folding a list
main :: IO () main = print (foldl (+) 0 [1, 2, 3, 4, 5])
(display (fold-left + 0 (list 1 2 3 4 5))) (newline)
Haskell's foldl and Scheme's fold-left take the same pieces in the same order — initial accumulator, then the list — with nearly identical names, one of the closer standard-library vocabulary matches on the whole site.
Improper lists: a shape Haskell cannot even express
-- Haskell's list type is always a proper, nil-terminated chain — there -- is no way to construct a Haskell list whose final "cons" cell points -- to something other than another list. A pair of two arbitrary types, -- for comparison, would just be an ordinary tuple: main :: IO () main = print (1 :: Int, "not a list" :: String)
(define improper (cons 1 "not a list")) (display improper) (newline) (display (pair? improper)) (newline) (display (list? improper)) (newline)
Because Scheme lists are just chains of cons cells by convention, not by type-level guarantee, nothing stops the final cdr from being something other than another list or '() — an "improper list" like (1 . "not a list"), printed with a dot. This is a real shape Haskell's type system makes literally inexpressible: a Haskell [a] is always proper by construction, so the closest comparison is an ordinary tuple, a genuinely different type rather than a malformed list.
Higher-Order Functions
Anonymous functions (lambda)
main :: IO () main = print (map (\x -> x * x) [1, 2, 3, 4])
(display (map (lambda (x) (* x x)) (list 1 2 3 4))) (newline)
Haskell spells a lambda \x -> ... (the backslash evoking the Greek letter lambda). Scheme spells out the full word: (lambda (x) ...) — no abbreviation at all, the etymological origin every other language's shorthand ultimately traces back to.
Currying: automatic in Haskell, manual in Scheme
add :: Int -> Int -> Int add x y = x + y main :: IO () main = let addFive = add 5 in print (addFive 10)
(define (add x y) (+ x y)) (define (add-five y) (add 5 y)) (display (add-five 10)) (newline)
In Haskell, add 5 alone is already valid — every function is secretly a chain of one-argument functions. Scheme has no automatic currying at all; getting a one-argument version of a two-argument function means writing the wrapper explicitly, exactly as shown, much like Erlang or C would require.
Pattern Matching vs. cond/case
case with guards becomes cond
describe :: Int -> String describe n = case n of 0 -> "zero" n | n > 0 -> "positive" _ -> "negative" main :: IO () main = putStrLn (describe (-5))
(define (describe n) (cond ((= n 0) "zero") ((> n 0) "positive") (else "negative"))) (display (describe -5)) (newline)
Haskell's guarded case and Scheme's cond both try a sequence of conditions top to bottom, falling through to a final catch-all (_ in Haskell, else in Scheme). Scheme's cond is closer to a chained if/else if than to Haskell's value-DESTRUCTURING case, since Scheme has no structural pattern matching at all — only boolean conditions.
No exhaustiveness checking whatsoever
-- GHC warns here (with -Wincomplete-patterns) since the -- negative case is missing entirely: classify :: Int -> String classify 0 = "zero" classify n | n > 0 = "positive" main :: IO () main = putStrLn (classify 5)
(define (classify n) (cond ((= n 0) "zero") ((> n 0) "positive"))) ; Scheme's "cond" has no analogue to Haskell's exhaustiveness ; warning at all — if every clause fails to match, "cond" simply ; returns an unspecified value with no error, no warning, nothing (display (classify 5)) (newline)
Haskell's compiler statically flags a case that might not cover every input. Scheme has nothing resembling this check: a cond where every clause fails to match simply returns an unspecified value silently — no warning at compile time (Scheme barely has a separate compile phase to check anything in), and no error at runtime either. This is a genuine safety regression a Haskeller should watch for.
Scheme's own case: literal matching only, no destructuring
describe :: Int -> String describe n = case n of 0 -> "zero" 1 -> "one" 2 -> "two" _ -> "many" main :: IO () main = putStrLn (describe 2)
(define (describe n) (case n ((0) "zero") ((1) "one") ((2) "two") (else "many"))) (display (describe 2)) (newline)
Scheme does have a construct literally named case, and it reads close to Haskell's at a glance — but it only matches against literal values with eqv?, wrapped in a list of alternatives per clause; it cannot destructure a pair, a record, or any compound structure the way Haskell's case destructures a constructor's payload.
Records vs. define-record-type
Records versus define-record-type
data Point = Point { x :: Int, y :: Int } deriving (Show) main :: IO () main = let point = Point { x = 3, y = 4 } in print (x point, y point)
(define-record-type point (fields x y)) (define my-point (make-point 3 4)) (display (list (point-x my-point) (point-y my-point))) (newline)
Haskell's record syntax generates a constructor, a predicate is implicit in the type system, and one accessor per field, all from one data declaration. Scheme's define-record-type generates the same three things automatically from the field list — make-point (constructor), point? (predicate, since Scheme has no static types to check this at compile time), and point-x/point-y (accessors) — genuinely close in spirit, and the strict naming convention means there is no way to ask for camelCase instead.
No record-update syntax at all — rebuild by hand
data Point = Point { x :: Int, y :: Int } deriving (Show) main :: IO () main = let point = Point { x = 3, y = 4 } moved = point { y = 99 } in print moved
(define-record-type point (fields x y)) (define my-point (make-point 3 4)) (define moved (make-point (point-x my-point) 99)) (display (list (point-x moved) (point-y moved))) (newline)
Haskell's point { y = 99 } produces a new record with one field replaced, in one expression. Scheme's define-record-type has no update syntax whatsoever — "updating" a record always means calling the constructor again, passing through every unchanged field by hand, considerably more ceremony for records with many fields.
Recursion & Tail Calls
Factorial
factorial :: Integer -> Integer factorial 0 = 1 factorial n = n * factorial (n - 1) main :: IO () main = print (factorial 10)
(define (factorial n) (if (= n 0) 1 (* n (factorial (- n 1))))) (display (factorial 10)) (newline)
A base-case clause and a recursive clause — Haskell writes them as separate pattern-matched equations, Scheme writes the single equivalent branch inside one if. Both are considered idiomatic in their own language for exactly this shape of recursion.
Both guarantee proper tail-call optimization
sumList :: [Int] -> Int -> Int sumList [] accumulator = accumulator sumList (x:xs) accumulator = sumList xs (x + accumulator) main :: IO () main = print (sumList [1, 2, 3, 4, 5] 0)
(define (sum-list numbers accumulator) (if (null? numbers) accumulator (sum-list (cdr numbers) (+ (car numbers) accumulator)))) (display (sum-list (list 1 2 3 4 5) 0)) (newline)
This is a genuine, formal guarantee both languages share: the Scheme standard (R7RS) MANDATES proper tail-call optimization as a language requirement, not just a compiler optimization that might or might not fire — and GHC reliably optimizes exactly this accumulator-passing shape too. An accumulator-passing recursive function runs in constant stack space in both languages, one of the strongest shared guarantees on the whole site.
Laziness vs. delay/force
Infinite lists exist only because Haskell is lazy
main :: IO () main = print (take 5 [1..])
(define (take-n lst n) (if (or (= n 0) (null? lst)) '() (cons (car lst) (take-n (cdr lst) (- n 1))))) (display (take-n (list 1 2 3 4 5) 5)) (newline)
Haskell's [1..] is an ordinary list that only works because Haskell is lazy, generating each element on demand as take asks for it. Scheme is strict by default, like almost every other language on this site — a genuinely infinite Scheme list has no direct representation at all; a bounded literal list is simply how Scheme gets "the first five numbers" directly.
delay/force: Scheme's opt-in laziness, a rare capability outside Haskell
main :: IO () main = do let expensive = 2 ^ 20 -- not computed until actually demanded putStrLn "before use" print expensive
(define expensive (delay (begin (display "computing now") (newline) (expt 2 20)))) (display "before use") (newline) (display (force expensive)) ; "computing now" prints here, on first force (newline) (display (force expensive)) ; cached — does NOT print "computing now" again (newline)
In Haskell, every binding is lazy unless forced otherwise — expensive is not actually computed until print demands it, and the result is cached automatically. Scheme is unusual among this page's comparison languages in offering the identical guarantee, but opt-in: delay creates a "promise" that is not evaluated until forced, and — like a Haskell thunk — evaluating it twice only computes it once, caching the result for the second force.
Macros: a Genuinely New Capability
Defining a brand-new control-flow form — no Haskell equivalent
-- Haskell has nothing resembling this. Adding a genuinely new piece -- of syntax to the language — not a function, but a form that -- decides WHETHER and HOW its arguments even get evaluated — is not -- something ordinary Haskell code can do at all. Template Haskell can -- approximate it, but it is rare, heavyweight machinery most Haskell -- code never touches: main :: IO () main = if True then putStrLn "condition was true" else return ()
(define-macro (my-when test . body) `(if ,test (begin ,@body) #f)) (my-when (> 5 3) (display "condition was true") (newline))
This is a genuinely new capability for a Haskeller, not just different syntax for an old one: define-macro lets ordinary Scheme code write a form that controls whether and how its own arguments are evaluated, rewriting itself into different code at read time — a brand-new piece of syntax, indistinguishable in use from something built into the language. Haskell has no equivalent at this level of ease; Template Haskell exists but is heavyweight, rarely used machinery most Haskell code never touches.
Unhygienic macros: a real footgun with no Haskell parallel
-- Haskell has nothing analogous to macro hygiene to worry about, -- since Haskell simply has no macro system at this level at all — -- there is no category of bug called "variable capture" in ordinary -- Haskell code: main :: IO () main = putStrLn "no macro hygiene concerns apply here"
; define-macro is UNHYGIENIC — the macro writer must choose names ; carefully, or a macro can accidentally capture a variable from the ; caller's own scope: (define-macro (my-unless test . body) `(if (not ,test) (begin ,@body) #f)) (my-unless (< 5 3) (display "5 is not less than 3") (newline))
BiwaScheme's define-macro, unlike the standard-Scheme syntax-rules it lacks, is unhygienic: a macro can accidentally capture or shadow a variable name from the code that calls it, since the macro's own template variables are not automatically renamed to avoid collision. Haskell has no comparable risk category at all — there being no macro system at this level, "variable capture from a macro expansion" is simply not a bug that can happen in ordinary Haskell code.
Continuations: call/cc vs. Cont
call/cc as a structured early-return
-- Haskell has nothing resembling call/cc as a language primitive. -- The closest comparison, the "Cont" monad from Control.Monad.Cont, -- MODELS continuations purely as data, rather than offering a raw, -- always-available control-flow primitive the way Scheme does: main :: IO () main = putStrLn "no raw continuation primitive exists in ordinary Haskell"
(display (call/cc (lambda (return) (display "before escape") (newline) (return 42) (display "never reached") (newline)))) (newline)
call/cc (call-with-current-continuation) captures "the rest of the computation" as a callable value; invoking it abandons the current work and jumps directly back to where call/cc was called. Used this way, it is a structured non-local return — like Ruby's return from a block. Haskell has nothing resembling this as a raw, always-available primitive; the closest comparison, the Cont monad, models continuations purely as an ordinary data value threaded explicitly through do-notation, rather than a control-flow primitive baked into the language itself.
dynamic-wind versus Haskell's bracket
import Control.Exception (bracket_) main :: IO () main = bracket_ (putStrLn "before") (putStrLn "after") (putStrLn "body")
(dynamic-wind (lambda () (display "before") (newline)) (lambda () (display "body") (newline)) (lambda () (display "after") (newline)))
Scheme's dynamic-wind runs three thunks — a "before" thunk, the body, and an "after" thunk that is GUARANTEED to run even if a continuation jumps out of the body early — a close structural cousin of Haskell's bracket_ (acquire, use, release, with release guaranteed even on exception). Scheme's version is subtly more general: it also correctly re-runs the "before" thunk if a stashed continuation later jumps back INTO the body, a scenario Haskell's exception-based bracket_ has no equivalent for at all, since Haskell has no continuations to jump back in with.
Maps vs. Hashtables
Data.Map versus R6RS hashtables
import qualified Data.Map as Map main :: IO () main = let ages = Map.fromList [("Ada", 36), ("Alan", 41)] in print (Map.lookup "Ada" ages)
(define ages (make-hashtable string-hash string=?)) (hashtable-set! ages "Ada" 36) (hashtable-set! ages "Alan" 41) (display (hashtable-ref ages "Ada" #f)) (newline)
Haskell's Data.Map is a persistent, immutable balanced tree — every "insert" returns a new map, the old one untouched. Scheme's R6RS hashtable (make-hashtable with a hash function and equality predicate) is a genuinely mutable table updated in place with hashtable-set! — a real representational difference, not just naming: Haskell's map can be shared safely across time, Scheme's hashtable cannot.
A missing key needs an explicit default in Scheme
import qualified Data.Map as Map main :: IO () main = let ages = Map.fromList [("Ada", 36)] in print (Map.lookup "Alan" ages)
(define ages (make-hashtable string-hash string=?)) (hashtable-set! ages "Ada" 36) (display (hashtable-ref ages "Alan" #f)) (newline)
Haskell's Map.lookup returns Maybe Int, an explicit, compiler-enforced "value or absence" type — the caller cannot forget to handle a missing key without a type error. hashtable-ref takes the DEFAULT to return on a miss as an explicit third argument (here #f) — there is no Maybe-equivalent wrapper type in Scheme to enforce that the caller ever checks it.
Either vs. Sentinel Values
Either becomes an ordinary tagged pair — nothing enforces handling it
divide :: Int -> Int -> Either String Int divide _ 0 = Left "divide by zero" divide x y = Right (x `div` y) main :: IO () main = print (divide 10 0)
(define (divide x y) (if (= y 0) (cons 'error "divide by zero") (cons 'ok (quotient x y)))) (display (divide 10 0)) (newline)
Haskell's Either String Int is a real, compiler-checked type — the compiler statically knows every value is either Left or Right, and rejects code that treats it as a bare Int. The closest Scheme idiom, a tagged cons pair like ('error . "message") or ('ok . value), is pure convention: nothing in the language enforces that a caller ever checks the tag before using the payload, since Scheme has no type system to enforce it with.
Skipping the failure case: compile error versus silent bug
divide :: Int -> Int -> Either String Int divide _ 0 = Left "divide by zero" divide x y = Right (x `div` y) main :: IO () main = case divide 10 0 of Right value -> print value Left reason -> putStrLn ("Error: " ++ reason)
(define (divide x y) (if (= y 0) (cons 'error "divide by zero") (cons 'ok (quotient x y)))) (define result (divide 10 0)) (if (eq? (car result) 'ok) (begin (display (cdr result)) (newline)) (begin (display (string-append "Error: " (cdr result))) (newline)))
This is the sharpest practical consequence of Scheme's lack of a type system. Forgetting to check the tag in Haskell — treating an Either as if it were the bare success value — is a compile-time type error, caught before the program ever runs. Forgetting the equivalent check in Scheme is simply a runtime bug: (cdr result) on an error pair would silently return the error MESSAGE where a number was expected, with no error raised anywhere, until something downstream breaks in a confusing way.
Gotchas for Haskell Developers
Scheme is not pure — mutation is ordinary, not an opt-in special case
-- Haskell structurally cannot mutate a value in place outside of -- an explicit IORef/STRef/MVar — there is no escape hatch: main :: IO () main = print (list [1, 2, 3]) where list = id
(define numbers (list 1 2 3)) (set-car! numbers 99) ; mutates the pair IN PLACE — no wrapper needed (display numbers) (newline)
Haskell has no escape hatch into ordinary in-place mutation outside of explicit IORef/STRef/MVar machinery threaded through IO. Scheme is not a pure language at all: set-car!/set-cdr! mutate a cons cell in place directly, no wrapper type required, and this is ordinary, everyday Scheme — not a special case reserved for genuinely imperative code the way Haskell treats IORef.
Three different equality predicates, no Haskell parallel
-- Haskell has exactly one equality operator, ==, resolved by the -- Eq typeclass — there is no separate "identity" versus "structural" -- equality distinction baked into the language itself: main :: IO () main = print ([1, 2, 3] == [1, 2, 3])
(display (eq? (list 1 2 3) (list 1 2 3))) (newline) ; #f — different objects (display (eqv? 2 2)) (newline) ; #t — same number (display (equal? (list 1 2 3) (list 1 2 3))) (newline) ; #t — structurally equal
Haskell has exactly one equality operator, ==, dispatched through the Eq typeclass, and it always means structural equality. Scheme has three genuinely different equality predicates with no Haskell parallel: eq? (pointer/identity equality, unreliable for numbers), eqv? (identity plus a few built-in types like numbers and characters), and equal? (deep, structural equality — the one closest to Haskell's ==). Picking the wrong one is a genuine, common Scheme bug category with no equivalent in Haskell at all.