Hello World & Tooling
Hello, World
fn main() {
println!("Hello, World!");
} main :: IO ()
main = putStrLn "Hello, World!" Every Haskell program's entry point is
main :: IO (), the counterpart of Rust's fn main(). The type signature IO () tells the compiler this action performs side effects and produces no meaningful result — Rust expresses roughly the same idea with an implicit () return type. putStrLn is the direct analogue of println! for a plain string.Build tools
// Cargo (built into Rust):
// cargo new myproject — create new project
// cargo build — debug build → target/debug/
// cargo build --release — optimized build → target/release/
// cargo run — build + run
// cargo test — run tests
// cargo add text — add a dependency (Cargo.toml + Cargo.lock)
// cargo fmt — format code (rustfmt)
// cargo clippy — lint -- Stack (the most common Haskell build tool):
-- stack new myproject — create new project from a template
-- stack build — compile the project
-- stack run — build + run
-- stack test — run tests
-- stack ghci — start a REPL with the project loaded
--
-- Cabal (the package manager Stack is built on):
-- cabal build
-- cabal run
-- cabal test
--
-- Dependencies live in package.yaml (Stack) or a .cabal file (Cabal),
-- similar in spirit to Cargo.toml. Stack and Cabal fill the role Cargo plays for Rust — dependency management, building, testing, and running a project. Stack additionally pins an entire compiler-plus-package snapshot (an LTS resolver) so that a project builds identically on every machine, a stronger reproducibility guarantee than Cargo.lock alone provides. Most real Haskell projects use Stack or Cabal rather than invoking
ghc directly.Formatted output
fn main() {
let name = "Alice";
let score = 42;
let ratio = 0.75_f64;
println!("{name} scored {score} ({:.1}%)", ratio * 100.0);
eprintln!("debug: score={score}");
} import Text.Printf (printf)
import System.IO (hPutStrLn, stderr)
main :: IO ()
main = do
let name = "Alice"
let score = 42 :: Int
let ratio = 0.75 :: Double
printf "%s scored %d (%.1f%%)\n" name score (ratio * 100.0)
hPutStrLn stderr ("debug: score=" ++ show score) Haskell has no built-in string interpolation, so
Text.Printf.printf provides C-style formatting closer to Rust's {:.1} specifiers than plain concatenation would. hPutStrLn stderr writes to standard error, the counterpart of Rust's eprintln!. Notice that printf is polymorphic in its argument count and types — a very different mechanism from Rust's macro-based println!, but the call site looks familiar.Values & Immutability
let bindings — immutable by default in both
fn main() {
let greeting = "Hello"; // immutable
let mut counter = 0; // mutable, needs explicit "mut"
counter += 1;
println!("{greeting} {counter}");
} main :: IO ()
main = do
let greeting = "Hello"
let counter = 0 :: Int
-- There is no way to mutate "counter" in place. To simulate the
-- Rust example above, you compute a new value under a new name:
let updatedCounter = counter + 1
putStrLn (greeting ++ " " ++ show updatedCounter) Rust's
let is immutable by default but offers an escape hatch, let mut, for in-place mutation. Haskell offers no such escape hatch for ordinary let bindings — every binding is permanently immutable, and "changing" a value always means computing a new one under a new name. Genuine mutable state in Haskell exists only inside IO, via explicit reference types like IORef.Shadowing
fn main() {
let value = 5;
let value = value * 2; // shadows the previous binding
let value = value.to_string();
println!("{value}");
} main :: IO ()
main = do
let value = 5 :: Int
let doubled = value * 2
let stringValue = show doubled
putStrLn stringValue Rust allows repeatedly shadowing a name in the same scope, even changing its type each time, which is a common idiom for step-by-step transformations. Haskell technically allows shadowing too, but idiomatic style favors giving each transformation stage a distinct name (as shown here) because Haskell's pervasive laziness makes it easy to lose track of which binding a shadowed name actually refers to at the point it is evaluated.
where clauses — private helper bindings
fn greeting_message() -> String {
let prefix = "Hello";
let target = "from helper";
format!("{prefix} {target}")
}
fn main() {
println!("{}", greeting_message());
} greetingMessage :: String
greetingMessage = prefix ++ " " ++ target
where
prefix = "Hello"
target = "from where"
main :: IO ()
main = putStrLn greetingMessage Rust achieves private helper values with ordinary local
let bindings inside a function body. Haskell's where clause attaches helper bindings to the end of a function definition instead of the beginning, and — unlike a Rust local variable — those bindings can be mutually recursive with each other and can carry their own type signatures.Constants
const MAX_RETRIES: u32 = 5;
const GREETING: &str = "Welcome";
fn main() {
println!("{GREETING}, retries allowed: {MAX_RETRIES}");
} maxRetries :: Int
maxRetries = 5
greeting :: String
greeting = "Welcome"
main :: IO ()
main = putStrLn (greeting ++ ", retries allowed: " ++ show maxRetries) Rust distinguishes compile-time
const declarations from ordinary variables with a dedicated keyword. Haskell has no separate constant syntax — a top-level binding with no arguments, like maxRetries, is already a compile-time-known, permanently immutable value, so it plays the same role without any special keyword.The Type System
Both are statically typed — but inference differs
fn main() {
let count = 42; // inferred as i32
let ratio: f64 = 3.14; // explicit annotation
println!("{count} {ratio}");
} main :: IO ()
main = do
let count = 42 :: Int
let ratio = 3.14 :: Double
print count
print ratio Both languages catch type errors at compile time rather than at runtime, which is a large part of what makes each language "feel safe" compared to a dynamically typed one. The type inference engines differ in strength, though: Haskell's Hindley–Milner-based inference can infer the types of entire function definitions with no annotations at all, while Rust's local type inference generally still needs an explicit function signature.
Function type signatures
fn add(first: i32, second: i32) -> i32 {
first + second
}
fn main() {
println!("{}", add(3, 4));
} add :: Int -> Int -> Int
add first second = first + second
main :: IO ()
main = print (add 3 4) Rust's function signature groups all parameters in one parenthesized list followed by
-> ReturnType. Haskell writes a signature as a chain of types joined by ->, with no visual grouping of the parameters at all — Int -> Int -> Int literally reads "takes an Int, then takes another Int, and produces an Int." This is not cosmetic: it reflects the fact that Haskell functions are curried (see the Higher-Order Functions section), whereas Rust functions are not.Generics vs parametric polymorphism
fn largest<T: PartialOrd + Copy>(values: &[T]) -> T {
let mut maximum = values[0];
for &value in values {
if value > maximum {
maximum = value;
}
}
maximum
}
fn main() {
let numbers = [34, 50, 25, 100, 65];
println!("{}", largest(&numbers));
} largest :: Ord a => [a] -> a
largest values = foldr1 max values
main :: IO ()
main = do
let numbers = [34, 50, 25, 100, 65] :: [Int]
print (largest numbers) Rust generics are written
<T: Bound> before the parameter list. Haskell writes the equivalent constraint before the signature with a fat arrow: Ord a => [a] -> a means "for any type a that implements Ord." The underlying idea — a function that works uniformly over any type satisfying a constraint — is the same in both languages; only the surface syntax differs.Point-free type inference
fn double(number: i32) -> i32 {
number * 2
}
fn main() {
let numbers = vec![1, 2, 3];
let doubled: Vec<i32> = numbers.iter().map(|number| double(*number)).collect();
println!("{doubled:?}");
} double :: Int -> Int
double = (* 2)
main :: IO ()
main = do
let numbers = [1, 2, 3] :: [Int]
print (map double numbers) Notice
double = (* 2) has no explicit parameter at all — this is "point-free" style, and it type-checks because Haskell's inference engine can determine that double must be a function from the way it is later applied to a list of Int via map. Point-free definitions like this are idiomatic in Haskell; Rust's type system does not support omitting the parameter list in this way.Option/Result vs Maybe/Either
Option<T> ↔ Maybe a — a direct parallel
fn safe_head(values: &[i32]) -> Option<i32> {
values.first().copied()
}
fn main() {
println!("{:?}", safe_head(&[1, 2, 3]));
println!("{:?}", safe_head(&[]));
} safeHead :: [Int] -> Maybe Int
safeHead [] = Nothing
safeHead (x:_) = Just x
main :: IO ()
main = do
print (safeHead [1, 2, 3])
print (safeHead []) This is one of the closest one-to-one mappings between the languages: Rust's
Some(x)/None correspond exactly to Haskell's Just x/Nothing, and both types exist for the same reason — making the absence of a value part of the type system so the compiler forces you to handle it. A Rust programmer who already avoids null in favor of Option will find Maybe immediately familiar.Result<T, E> ↔ Either e a — a direct parallel
fn safe_divide(numerator: i32, denominator: i32) -> Result<i32, String> {
if denominator == 0 {
Err(String::from("division by zero"))
} else {
Ok(numerator / denominator)
}
}
fn main() {
println!("{:?}", safe_divide(10, 2));
println!("{:?}", 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) Rust's
Ok(value)/Err(error) correspond to Haskell's Right value/Left error — note the reversed order in the name Either e a (error type first, success type second), which trips up many newcomers. The mnemonic "right is right" (correct) helps: Right always holds the success case, matching Rust's Ok.unwrap_or ↔ fromMaybe
fn main() {
let present: Option<i32> = Some(42);
let absent: Option<i32> = None;
println!("{}", present.unwrap_or(0));
println!("{}", absent.unwrap_or(0));
} import Data.Maybe (fromMaybe)
main :: IO ()
main = do
let present = Just 42 :: Maybe Int
let absent = Nothing :: Maybe Int
print (fromMaybe 0 present)
print (fromMaybe 0 absent) Rust's
option.unwrap_or(default) and Haskell's fromMaybe default maybeValue do exactly the same job — unwrap the value if present, otherwise substitute a default — just with the argument order reversed and the receiver/argument relationship flipped, since Haskell has no method-call syntax..map() on Option ↔ fmap on Maybe
fn main() {
let score: Option<i32> = Some(85);
let grade = score.map(|value| if value >= 90 { "A" } else { "B" });
println!("{grade:?}");
let nothing: Option<i32> = None;
println!("{:?}", nothing.map(|value| value * 2));
} main :: IO ()
main = do
let score = Just 85 :: Maybe Int
let grade = fmap (\value -> if value >= 90 then "A" else "B") score
print grade
let nothing = Nothing :: Maybe Int
print (fmap (* 2) nothing) Rust's
Option::map and Haskell's fmap (or the Functor operator <$>) both apply a function to the contained value only if one is present, otherwise passing the absence through unchanged. The difference is scope: Rust's .map() is a method defined specifically on Option and Result, while Haskell's fmap is one function that works identically across Maybe, lists, Either, IO, and any other type that implements the Functor typeclass.and_then ↔ >>= (bind)
fn half_if_even(number: i32) -> Option<i32> {
if number % 2 == 0 { Some(number / 2) } else { None }
}
fn main() {
let start: Option<i32> = Some(20);
let result = start.and_then(half_if_even).and_then(half_if_even);
println!("{result:?}");
} halfIfEven :: Int -> Maybe Int
halfIfEven number
| even number = Just (number `div` 2)
| otherwise = Nothing
main :: IO ()
main = do
let start = Just 20 :: Maybe Int
let result = start >>= halfIfEven >>= halfIfEven
print result Rust's
Option::and_then chains fallible operations, short-circuiting to None as soon as any step fails — this is exactly what Haskell's bind operator >>= does for Maybe. This is not a coincidence: and_then is Rust's name for what the rest of the functional programming world calls monadic bind, and Haskell's >>= is the same operation spelled out as a first-class operator that works across every monad, not just Maybe.Pattern Matching
match ↔ case — Rust programmers will feel at home
fn describe(day: i32) -> &'static str {
match day {
1 => "Monday",
2 => "Tuesday",
3 => "Wednesday",
_ => "Other",
}
}
fn main() {
println!("{}", describe(3));
} describe :: Int -> String
describe day = case day of
1 -> "Monday"
2 -> "Tuesday"
3 -> "Wednesday"
_ -> "Other"
main :: IO ()
main = putStrLn (describe 3) The structure is almost identical: both use
_ as the catch-all wildcard, and both compilers enforce exhaustiveness — the compiler warns (or errors, with the right flags) if a case is missing. The main syntax differences are => versus -> for each arm and the absence of commas separating Haskell's cases.Function-clause pattern matching — Haskell-only style
fn describe_list(values: &[i32]) -> &'static str {
match values {
[] => "empty",
[_] => "one element",
[_, _] => "two elements",
_ => "many elements",
}
}
fn main() {
println!("{}", describe_list(&[]));
println!("{}", 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, 2, 3]) Rust matches on the parameter's value inside a
match expression in the function body. Haskell instead lets you write one equation per pattern, directly at the top level of the function definition — the runtime tries each equation top-to-bottom and uses the first whose pattern matches. There is no equivalent syntax in Rust; the closest analogy is Rust's slice patterns inside a single match, shown here for comparison.Destructuring tuples and structs
struct Point { x: f64, y: f64 }
fn main() {
let point = Point { x: 3.0, y: 4.0 };
let Point { x, y } = point;
println!("x={x} y={y}");
let pair = (1, "one");
let (number, name) = pair;
println!("{number} {name}");
} data Point = Point { pointX :: Double, pointY :: Double }
main :: IO ()
main = do
let point = Point { pointX = 3.0, pointY = 4.0 }
let Point { pointX = x, pointY = y } = point
putStrLn ("x=" ++ show x ++ " y=" ++ show y)
let pair = (1 :: Int, "one")
let (number, name) = pair
putStrLn (show number ++ " " ++ name) Both languages let you destructure a struct/record or tuple directly in a
let binding, pulling fields out into individually named bindings in one step. The syntax is nearly identical — Rust's let Point { x, y } = point; and Haskell's let Point { pointX = x, pointY = y } = point express the same idea, though Haskell requires explicitly renaming each field since it has no shorthand field-punning by default outside of an extension.Guards in patterns
fn classify(number: i32) -> &'static str {
match number {
n if n < 0 => "negative",
0 => "zero",
n if n < 10 => "small",
_ => "large",
}
}
fn main() {
println!("{}", classify(-5));
println!("{}", classify(7));
} 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 7) Rust attaches a guard condition to a
match arm with pattern if condition => .... Haskell's guards live directly in a function definition, introduced by |, with otherwise (which is just True) playing the role of the catch-all — a style that reads almost like a mathematical piecewise definition.Matching head and tail (cons pattern)
fn first_two(values: &[i32]) -> String {
match values {
[] => "Empty".to_string(),
[only] => format!("Only: {only}"),
[first, second, ..] => format!("First: {first}, Second: {second}"),
}
}
fn main() {
println!("{}", first_two(&[1, 2, 3, 4]));
println!("{}", first_two(&[42]));
} firstTwo :: [Int] -> String
firstTwo (first:second:_) = "First: " ++ show first ++ ", Second: " ++ show second
firstTwo [only] = "Only: " ++ show only
firstTwo [] = "Empty"
main :: IO ()
main = do
putStrLn (firstTwo [1, 2, 3, 4])
putStrLn (firstTwo [42]) Rust's slice patterns, like
[first, second, ..], cover the same ground as Haskell's cons pattern (first:second:_): both split a list into named leading elements plus "everything else." The underlying representations differ, though — a Rust slice is a contiguous, indexable array in memory, while a Haskell list is a chain of cons cells, so the pattern (x:rest) is really peeling one link off that chain rather than indexing into a buffer.No Ownership — Garbage Collection
No borrow checker at all
fn main() {
let owner = String::from("hello");
let borrowed = &owner; // borrow — compiler tracks lifetime
println!("{borrowed}");
// owner is still valid here because the borrow ended above
println!("{owner}");
} main :: IO ()
main = do
let owner = "hello"
-- There is no borrowing, no lifetimes, and no ownership transfer here.
-- "reference" below is simply another name bound to the same immutable
-- value; both names remain valid for as long as anything needs them,
-- and the garbage collector reclaims the value once nothing does.
let reference = owner
putStrLn reference
putStrLn owner This is the single biggest structural difference between the two languages. Rust's compiler enforces ownership and borrowing rules so that memory is freed deterministically with no garbage collector; Haskell has none of that machinery. Every Haskell value is managed by a tracing garbage collector, so there is no concept of "moving" a value, no lifetimes to annotate, and no possibility of a use-after-move or dangling-reference compile error — those bug categories simply do not exist in Haskell, at the cost of GC pauses and less predictable memory reclamation timing.
Sharing values freely — no Rc/Arc needed
use std::rc::Rc;
fn main() {
let shared = Rc::new(vec![1, 2, 3]);
let clone_one = Rc::clone(&shared);
let clone_two = Rc::clone(&shared);
println!("{:?} {:?} {:?}", shared, clone_one, clone_two);
println!("reference count: {}", Rc::strong_count(&shared));
} main :: IO ()
main = do
let shared = [1, 2, 3] :: [Int]
let cloneOne = shared
let cloneTwo = shared
print shared
print cloneOne
print cloneTwo
-- There is no reference count to inspect: the garbage collector
-- decides independently when the underlying memory can be reclaimed. Rust needs explicit reference-counted wrapper types like
Rc<T> (single-threaded) or Arc<T> (thread-safe) whenever a value must be shared among multiple owners, because its ownership model otherwise only permits one owner at a time. Haskell requires no such wrapper — any value can be bound to as many names as you like, because the garbage collector (not an ownership graph) decides when memory is reclaimed.Mutable state — IORef instead of &mut
fn main() {
let mut counter = 0;
counter += 1;
counter += 1;
println!("{counter}");
} import Data.IORef
main :: IO ()
main = do
counter <- newIORef (0 :: Int)
modifyIORef counter (+ 1)
modifyIORef counter (+ 1)
finalValue <- readIORef counter
print finalValue Rust's
let mut plus &mut references give you compiler-checked, in-place mutation of ordinary variables. Haskell keeps ordinary bindings permanently immutable and instead offers IORef — an explicit, boxed mutable cell that can only be read and written inside IO. Reaching for IORef is the exception in Haskell, not the default, whereas mut is a routine, lightweight choice in Rust.No lifetime annotations
fn longest<'a>(first: &'a str, second: &'a str) -> &'a str {
if first.len() > second.len() { first } else { second }
}
fn main() {
let greeting = String::from("hello");
let name = String::from("world");
println!("{}", longest(&greeting, &name));
} longest :: String -> String -> String
longest first second
| length first > length second = first
| otherwise = second
main :: IO ()
main = do
let greeting = "hello"
let name = "world"
putStrLn (longest greeting name) Rust's explicit lifetime parameter
'a tells the borrow checker that the returned reference must not outlive either input — a piece of ceremony that exists purely to make borrowing safe without a garbage collector. Haskell's equivalent function needs no such annotation at all: strings are ordinary garbage-collected values, so there is nothing for a lifetime to describe.Laziness vs Strict Evaluation
Rust evaluates eagerly, Haskell lazily
fn expensive() -> i32 {
println!("computing...");
42
}
fn main() {
let _unused = expensive(); // runs immediately, prints "computing..."
println!("done");
} expensive :: Int
expensive = trace' 42
where trace' value = value -- (side-effect-free stand-in for demonstration)
main :: IO ()
main = do
let unused = expensive -- nothing is computed yet — "unused" is a thunk
putStrLn "done" -- "expensive" is never forced, so it never runs This is one of the largest behavioral differences a Rust programmer will meet in Haskell. Rust evaluates every expression as soon as it is bound, exactly where it appears in the code, which is why calling
expensive() in the left-hand example immediately prints its message. Haskell instead builds an unevaluated "thunk" for expensive and only forces it — actually runs the computation — the first time its value is genuinely needed; here it never is, so the computation never happens at all.Infinite lists work in Haskell
fn main() {
// Rust has no infinite iterator literal, but (1..) is an unbounded
// Range you can lazily .take() from, which is the closest analogue.
let first_five: Vec<i32> = (1..).take(5).collect();
println!("{first_five:?}");
} main :: IO ()
main = do
-- [1..] is an honest infinite list — Haskell never tries to build it
-- all at once; "take 5" only forces the first five cons cells.
print (take 5 [1..] :: [Int])
let squares = map (^ 2) [1..]
print (take 5 squares) Rust's unbounded
Range (1..) is the closest built-in analogue to a Haskell infinite list, and both rely on laziness to stay finite in practice. The difference is how pervasive the laziness is: Rust's Range is a special-cased lazy iterator, while in Haskell every list — including a plain [1..] literal with no special iterator type — is lazy by construction, so infinite lists fall out naturally rather than requiring a dedicated abstraction.Forcing strictness when it matters
fn sum_of_squares(values: &[i32]) -> i32 {
// Rust's iterator adapters are also lazy until a consuming call like
// .sum() runs them, so this is a reasonable point of comparison —
// but the underlying values in "values" were already fully evaluated
// before this function was ever called.
values.iter().map(|value| value * value).sum()
}
fn main() {
println!("{}", sum_of_squares(&[1, 2, 3, 4]));
} import Data.List (foldl')
sumOfSquares :: [Int] -> Int
-- foldl' is the STRICT left fold — it forces each intermediate
-- accumulator immediately, avoiding a large chain of unevaluated
-- thunks that plain "foldr" or lazy "foldl" would otherwise build up.
sumOfSquares = foldl' (\accumulator value -> accumulator + value * value) 0
main :: IO ()
main = print (sumOfSquares [1, 2, 3, 4]) Laziness is usually a benefit, but it has a well-known failure mode: repeatedly deferring computation can build up a long chain of unevaluated thunks that eventually overflows the stack or wastes memory — a problem Rust's eager evaluation never has.
Data.List.foldl' (the trailing apostrophe signals "strict") forces each step immediately, sidestepping the thunk build-up; reaching for it over plain foldl in accumulation-heavy code is a well-known Haskell idiom.IO actions still run in order
fn main() {
println!("first");
println!("second");
println!("third");
} main :: IO ()
main = do
putStrLn "first"
putStrLn "second"
putStrLn "third" Laziness governs pure computation, not the sequencing of side effects. Inside a
do block, IO actions are always performed strictly, top to bottom, exactly like Rust's sequential statements — this is an important exception for a Rust programmer to internalize early, since it is easy to over-generalize "Haskell is lazy" into assuming output order is unpredictable, when in fact IO sequencing is completely deterministic.Algebraic Data Types
enum ↔ data — sum types
#[derive(Debug)]
enum Direction {
North,
South,
East,
West,
}
fn describe(direction: &Direction) -> &'static str {
match direction {
Direction::North => "heading north",
Direction::South => "heading south",
Direction::East => "heading east",
Direction::West => "heading west",
}
}
fn main() {
println!("{}", describe(&Direction::North));
} data Direction = North | South | East | West deriving (Show)
describe :: Direction -> String
describe North = "heading north"
describe South = "heading south"
describe East = "heading east"
describe West = "heading west"
main :: IO ()
main = putStrLn (describe North) Rust's
enum and Haskell's data both define a sum type — a value that is exactly one of a fixed set of alternatives. The syntax reads almost identically, and both compilers enforce exhaustive matching over the alternatives. deriving (Show) is Haskell's equivalent of Rust's #[derive(Debug)], automatically generating a string representation.Enum variants carrying data
#[derive(Debug)]
enum Shape {
Circle(f64),
Rectangle(f64, f64),
}
fn area(shape: &Shape) -> f64 {
match shape {
Shape::Circle(radius) => std::f64::consts::PI * radius * radius,
Shape::Rectangle(width, height) => width * height,
}
}
fn main() {
println!("{:.2}", area(&Shape::Circle(5.0)));
println!("{:.2}", area(&Shape::Rectangle(4.0, 3.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
print (area (Circle 5.0))
print (area (Rectangle 4.0 3.0)) Each Rust enum variant can carry an inline tuple or struct of data, exactly as each Haskell constructor can carry positional fields.
Shape::Circle(f64) and Circle Double are the same idea, and both languages let you destructure the payload directly in a pattern-matching arm — Shape::Circle(radius) versus Circle radius.struct ↔ record syntax — product types
#[derive(Debug)]
struct Person {
name: String,
age: u32,
}
fn main() {
let person = Person { name: String::from("Alice"), age: 30 };
println!("{} is {}", person.name, person.age);
} data Person = Person
{ personName :: String
, personAge :: Int
} deriving (Show)
main :: IO ()
main = do
let person = Person { personName = "Alice", personAge = 30 }
putStrLn (personName person ++ " is " ++ show (personAge person)) Rust structs are always product types with named fields, roughly equivalent to Haskell's record syntax,
data Person = Person { ... }. One naming quirk: Haskell field accessors live in a single flat namespace shared across the whole module, so real code typically prefixes each field name (personName, personAge) to avoid collisions with a similarly named field on a different record — Rust has no such restriction, since person.name is always scoped to Person.Immutable record update
#[derive(Debug, Clone)]
struct Person {
name: String,
age: u32,
}
fn main() {
let alice = Person { name: String::from("Alice"), age: 30 };
let older_alice = Person { age: alice.age + 1, ..alice.clone() };
println!("{older_alice:?}");
} data Person = Person
{ personName :: String
, personAge :: Int
} deriving (Show)
main :: IO ()
main = do
let alice = Person { personName = "Alice", personAge = 30 }
let olderAlice = alice { personAge = personAge alice + 1 }
print olderAlice Both languages support "functional update" — building a new value that copies every field from an existing one except the fields you name explicitly. Rust's
..alice.clone() spread requires an explicit clone because the original alice would otherwise be moved; Haskell's alice { personAge = ... } needs no such clone, since every value is already immutable and freely shareable.Newtype wrappers for type safety
struct UserId(u32);
struct ProductId(u32);
fn describe_user(id: &UserId) -> String {
format!("user #{}", id.0)
}
fn main() {
let user = UserId(42);
println!("{}", describe_user(&user));
} newtype UserId = UserId Int deriving (Show)
newtype ProductId = ProductId Int deriving (Show)
describeUser :: UserId -> String
describeUser (UserId identifier) = "user #" ++ show identifier
main :: IO ()
main = putStrLn (describeUser (UserId 42)) Rust's tuple-struct newtype pattern,
struct UserId(u32), and Haskell's dedicated newtype keyword solve the same problem: preventing a raw u32/Int from being passed where a semantically distinct identifier is expected. Haskell's newtype is guaranteed to compile away entirely, adding zero runtime overhead compared to a plain Int — a stronger, compiler-enforced version of the guarantee Rust's equivalent single-field tuple struct also aims for.Traits vs Typeclasses
trait ↔ class — a close parallel
trait Describable {
fn describe(&self) -> String;
}
struct Season(String);
impl Describable for Season {
fn describe(&self) -> String {
format!("The season is {}", self.0)
}
}
fn main() {
let spring = Season(String::from("spring"));
println!("{}", spring.describe());
} class Describable a where
describe :: a -> String
data Season = Season String
instance Describable Season where
describe (Season name) = "The season is " ++ name
main :: IO ()
main = putStrLn (describe (Season "spring")) This is another very close parallel between the languages. Rust's
trait defines an interface; Haskell's class keyword does the same thing (confusingly named — it has nothing to do with object-oriented classes). Rust implements a trait for a type with impl Trait for Type; Haskell implements a typeclass for a type with instance Class Type where. Both compilers require every method of the interface to be provided (unless a default is supplied) and both reject calling a typeclass/trait method on a type that has no instance/impl for it.Default method implementations
trait Greet {
fn name(&self) -> String;
fn greeting(&self) -> String {
format!("Hello, {}!", self.name())
}
}
struct Person(String);
impl Greet for Person {
fn name(&self) -> String {
self.0.clone()
}
}
fn main() {
let alice = Person(String::from("Alice"));
println!("{}", alice.greeting());
} class Greet a where
name :: a -> String
greeting :: a -> String
greeting value = "Hello, " ++ name value ++ "!" -- default implementation
data Person = Person String
instance Greet Person where
name (Person personName) = personName
-- greeting is inherited from the default above
main :: IO ()
main = putStrLn (greeting (Person "Alice")) Both traits and typeclasses can supply a default implementation for a method, which any implementing type inherits automatically unless it chooses to override it. Rust writes the default directly in the
trait body; Haskell does the same directly in the class body — structurally identical, just spelled differently.Trait bounds ↔ typeclass constraints
fn print_twice<T: std::fmt::Display>(value: T) {
println!("{value}");
println!("{value}");
}
fn main() {
print_twice(42);
print_twice("hello");
} printTwice :: Show a => a -> IO ()
printTwice value = do
print value
print value
main :: IO ()
main = do
printTwice (42 :: Int)
printTwice "hello" Rust constrains a generic parameter with a trait bound, written
<T: Display>. Haskell constrains a type variable with a typeclass constraint, written before the signature with a fat arrow: Show a =>. Both mechanisms restrict a polymorphic function to only the types that support the required operations, and both are checked at compile time.#[derive] ↔ deriving
#[derive(Debug, Clone, PartialEq)]
struct Point {
x: f64,
y: f64,
}
fn main() {
let first = Point { x: 1.0, y: 2.0 };
let second = first.clone();
println!("{}", first == second);
println!("{first:?}");
} data Point = Point { pointX :: Double, pointY :: Double }
deriving (Show, Eq)
main :: IO ()
main = do
let first = Point { pointX = 1.0, pointY = 2.0 }
let second = first
print (first == second)
print first Rust's
#[derive(...)] attribute and Haskell's deriving (...) clause both ask the compiler to generate boilerplate typeclass/trait implementations automatically — equality, ordering, string conversion, and so on — rather than writing them by hand. The concept transfers directly; only the placement differs, with Rust's derive attached above the type and Haskell's deriving clause attached after it.Dynamic dispatch: dyn Trait vs existentials
trait Animal {
fn sound(&self) -> &str;
}
struct Dog;
struct Cat;
impl Animal for Dog { fn sound(&self) -> &str { "woof" } }
impl Animal for Cat { fn sound(&self) -> &str { "meow" } }
fn main() {
let animals: Vec<Box<dyn Animal>> = vec![Box::new(Dog), Box::new(Cat)];
for animal in &animals {
println!("{}", animal.sound());
}
} {-# LANGUAGE ExistentialQuantification #-}
class Animal a where
sound :: a -> String
data Dog = Dog
data Cat = Cat
instance Animal Dog where sound _ = "woof"
instance Animal Cat where sound _ = "meow"
data AnyAnimal = forall a. Animal a => AnyAnimal a
main :: IO ()
main = do
let animals = [AnyAnimal Dog, AnyAnimal Cat]
mapM_ (\(AnyAnimal animal) -> putStrLn (sound animal)) animals Rust's
Box<dyn Trait> lets a single collection hold multiple concrete types that all implement the same trait, dispatched at runtime through a vtable. Plain Haskell has no built-in equivalent — reaching the same result requires the ExistentialQuantification language extension to wrap heterogeneous values in a single existential type like AnyAnimal above. This is meaningfully more ceremony than Rust's dyn, and is one of the few places where idiomatic Rust is actually the more concise option.Higher-Order Functions & Currying
Closures ↔ lambda expressions
fn main() {
let double = |number: i32| number * 2;
let add = |first: i32, second: i32| first + second;
println!("{}", double(5));
println!("{}", add(3, 4));
} main :: IO ()
main = do
let double = \number -> number * 2
let add = \first second -> first + second
print (double (5 :: Int))
print (add (3 :: Int) 4) Rust closures use pipe syntax,
|number| number * 2; Haskell lambdas use a backslash (evoking the Greek letter λ) followed by ->, \\number -> number * 2. Both create anonymous functions that can capture bindings from the surrounding scope. The deeper difference, covered next, is that Haskell's multi-argument lambda is secretly a chain of single-argument functions, while Rust's is not.Currying — Haskell curries every function, Rust does not
fn add(first: i32, second: i32) -> i32 {
first + second
}
fn main() {
// Rust has no built-in partial application of a plain fn — you
// must write a closure to fix one argument in advance:
let add_five = |value: i32| add(5, value);
println!("{}", add_five(10));
} add :: Int -> Int -> Int
add first second = first + second
main :: IO ()
main = do
-- "add" is really a function Int -> (Int -> Int). Applying it to
-- one argument returns a new, fully usable function.
let addFive = add 5
print (addFive 10) This is a fundamental difference in how the two languages model multi-parameter functions. A Rust function like
fn add(first: i32, second: i32) -> i32 genuinely takes two arguments at once — partial application requires manually writing a wrapping closure. Every Haskell function of "two arguments" is secretly a function of one argument that returns another function of one argument; add 5 is a perfectly ordinary, complete expression that evaluates to a new Int -> Int function, with no closure syntax required.Passing functions as arguments
fn apply_twice(operation: impl Fn(i32) -> i32, value: i32) -> i32 {
operation(operation(value))
}
fn main() {
println!("{}", apply_twice(|x| x * 2, 3));
println!("{}", 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) Both languages treat functions as first-class values that can be passed as arguments. Rust spells the parameter type with
impl Fn(i32) -> i32 (or the more general Box<dyn Fn(i32) -> i32> for dynamic dispatch); Haskell spells it simply as (Int -> Int), a function type written the same way as any other type. (* 2) and (+ 10) here are operator sections — partially applied operators — one of the more distinctively terse pieces of Haskell syntax.map / filter / fold — iterator adapters vs list functions
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6];
let result: i32 = numbers.iter()
.filter(|&&value| value % 2 == 0)
.map(|&value| value * value)
.sum();
println!("{result}");
} main :: IO ()
main = do
let numbers = [1, 2, 3, 4, 5, 6] :: [Int]
let result = sum (map (^ 2) (filter even numbers))
print result Rust's iterator adapters (
.filter(), .map(), .sum()) are chained as methods and are lazy until a terminal call like .sum() or .collect() runs them. Haskell's filter, map, and sum are ordinary top-level functions rather than methods, composed by nesting or with . — and because Haskell lists are lazy by construction (see the Laziness section), no explicit terminal call is needed to trigger evaluation; the outer print forces the whole chain.IO & Do Notation
println!/Result-based IO vs do-notation
use std::io::{self, Write};
fn main() -> io::Result<()> {
println!("What is your name?");
let mut name = String::new();
io::stdin().read_line(&mut name)?;
println!("Hello, {}!", name.trim());
Ok(())
} main :: IO ()
main = do
putStrLn "What is your name?"
name <- getLine
putStrLn ("Hello, " ++ name ++ "!") Rust performs IO with ordinary synchronous function calls, using
Result and the ? operator to propagate failures such as a broken stdin stream. Haskell instead requires that any function performing IO carry IO in its type signature, and sequences those IO actions inside a do block using <- to bind results — a difference that is more about making side effects visible in the type system than about any change in what the program actually does. This example is marked non-runnable because it reads from standard input, which the in-browser Compiler Explorer runner cannot supply interactively.IO in the type signature marks effects
fn pure_double(number: i32) -> i32 {
number * 2 // no side effects — nothing in the signature says so explicitly
}
fn print_and_double(number: i32) -> i32 {
println!("doubling {number}"); // side effect, but the signature hides it
number * 2
}
fn main() {
println!("{}", pure_double(21));
println!("{}", print_and_double(21));
} pureDouble :: Int -> Int
pureDouble number = number * 2 -- guaranteed no side effects by the type
printAndDouble :: Int -> IO Int
printAndDouble number = do
putStrLn ("doubling " ++ show number)
return (number * 2)
main :: IO ()
main = do
print (pureDouble 21)
doubled <- printAndDouble 21
print doubled This is a distinction Rust's type system does not make: a Rust function's signature says nothing about whether it prints, mutates a file, or is otherwise "impure" —
pure_double and print_and_double have signatures that look equally innocent. In Haskell, any function that performs a side effect must say so by returning something wrapped in IO; a function with no IO in its type, like pureDouble :: Int -> Int, is guaranteed by the compiler to be a pure computation with no observable side effects at all.return does not exit — a common trap
fn always_forty_two() -> i32 {
return 42; // exits the function immediately with 42
}
fn main() {
println!("{}", always_forty_two());
} alwaysFortyTwo :: IO Int
alwaysFortyTwo = return 42
-- "return" here does NOT exit anything early — it simply wraps the
-- pure value 42 in an IO action. It is unrelated to Rust's "return".
main :: IO ()
main = do
number <- alwaysFortyTwo
print number In Rust,
return is a control-flow keyword that immediately exits the enclosing function with the given value. Haskell reuses the word return for something completely unrelated: it takes an ordinary pure value and wraps it in a monad — most commonly IO — with no effect on control flow whatsoever. A Rust programmer's intuition about return will actively mislead them here; modern Haskell style even prefers the identical function pure over return specifically to avoid this false association.Looping over a collection with side effects
fn main() {
let fruits = ["apple", "banana", "cherry"];
for fruit in fruits {
println!("{fruit}");
}
} main :: IO ()
main = do
let fruits = ["apple", "banana", "cherry"]
mapM_ putStrLn fruits Rust's
for fruit in fruits loop and Haskell's mapM_ putStrLn fruits both perform an IO action once per element, in order, discarding any results. There is no dedicated loop syntax in Haskell for this — mapM_ is an ordinary function, and the trailing underscore is a naming convention (shared with several other Haskell functions) meaning "run this for effect and throw away the results."Monads & the ? Operator
The ? operator — Rust's closest brush with monadic chaining
fn parse_and_double(text: &str) -> Result<i32, std::num::ParseIntError> {
let number = text.parse::<i32>()?; // early-returns Err on failure
Ok(number * 2)
}
fn parse_and_double_twice(first: &str, second: &str) -> Result<i32, std::num::ParseIntError> {
let doubled_first = parse_and_double(first)?;
let doubled_second = parse_and_double(second)?;
Ok(doubled_first + doubled_second)
}
fn main() {
println!("{:?}", parse_and_double_twice("3", "4"));
println!("{:?}", parse_and_double_twice("3", "not a number"));
} import Text.Read (readMaybe)
parseAndDouble :: String -> Maybe Int
parseAndDouble text = do
number <- readMaybe text
return (number * 2)
parseAndDoubleTwice :: String -> String -> Maybe Int
parseAndDoubleTwice first second = do
doubledFirst <- parseAndDouble first
doubledSecond <- parseAndDouble second
return (doubledFirst + doubledSecond)
main :: IO ()
main = do
print (parseAndDoubleTwice "3" "4")
print (parseAndDoubleTwice "3" "not a number") Rust's
? operator on Result (and Option) is Rust's closest brush with monadic chaining: it threads a computation forward and short-circuits at the first failure, unwrapping the success case automatically at each step. Haskell's do-notation over Maybe does precisely the same job — each <- line is a bind that short-circuits to Nothing the moment any step fails. Recognizing this parallel is the single most useful bridge for a Rust programmer trying to build intuition for what a "monad" actually is: it is the general pattern that ? only implements for Result/Option specifically.Monad is just another typeclass
// Rust has no single "Monad" trait in the standard library — the
// short-circuiting behavior of ? is built into the language for
// Result and Option specifically, rather than being a general,
// user-extensible interface the way Haskell's Monad is.
fn main() {
let chained = Some(3).and_then(|x| Some(x * 2)).and_then(|y| Some(y + 1));
println!("{chained:?}");
} main :: IO ()
main = do
-- (>>=) and return/pure come from the Monad typeclass in the standard
-- Prelude, implemented for Maybe, Either e, lists, IO, and many more.
-- Any type with a Monad instance can be chained with >>= or do-notation.
let chained = Just 3 >>= (\x -> Just (x * 2)) >>= (\y -> Just (y + 1))
print chained Rust bakes short-circuiting behavior directly into the language for
Result and Option via ?, but there is no general, user-extensible "Monad trait" in the standard library that other types can opt into. Haskell instead defines Monad as an ordinary typeclass — the same mechanism used for Show, Eq, and Ord — with (>>=) and return/pure as its core operations, so Maybe, Either e, lists, IO, and countless user-defined types can all share this one interface and inherit do-notation for free.Chaining Result/Either with early exit
fn half(number: i32) -> Result<i32, String> {
if number % 2 == 0 {
Ok(number / 2)
} else {
Err(format!("{number} is odd"))
}
}
fn halve_three_times(start: i32) -> Result<i32, String> {
let first = half(start)?;
let second = half(first)?;
let third = half(second)?;
Ok(third)
}
fn main() {
println!("{:?}", halve_three_times(40));
println!("{:?}", halve_three_times(24));
} half :: Int -> Either String Int
half number
| even number = Right (number `div` 2)
| otherwise = Left (show number ++ " is odd")
halveThreeTimes :: Int -> Either String Int
halveThreeTimes start = do
first <- half start
second <- half first
third <- half second
return third
main :: IO ()
main = do
print (halveThreeTimes 40)
print (halveThreeTimes 24) This example makes the
?-to-do-notation parallel concrete over three chained steps. Rust's let first = half(start)?;, repeated three times, and Haskell's first <- half start, also repeated three times, both stop at the first failure and propagate its error unchanged — Rust returns from the function early, and Haskell's Either monad short-circuits the rest of the do block to the same Left value.List Comprehensions
List comprehensions vs iterator chains
fn main() {
let squares: Vec<i32> = (1..=5).map(|x| x * x).collect();
println!("{squares:?}");
} main :: IO ()
main = do
let squares = [x * x | x <- [1..5 :: Int]]
print squares Rust has no dedicated comprehension syntax — building a transformed list always means chaining iterator adapters like
.map() and finishing with .collect(). Haskell's list comprehension, [x * x | x <- [1..5]], reads close to mathematical set-builder notation: the part before | is the output expression, and x <- [1..5] is the generator supplying values of x.Comprehensions with a filtering guard
fn main() {
let even_squares: Vec<i32> = (1..=10)
.filter(|x| x % 2 == 0)
.map(|x| x * x)
.collect();
println!("{even_squares:?}");
} main :: IO ()
main = do
let evenSquares = [x * x | x <- [1..10 :: Int], even x]
print evenSquares Adding a condition after the comma in a Haskell comprehension —
, even x — acts as a guard that filters the generator, combining what Rust would express as a separate .filter() call chained before .map(). A single Haskell comprehension can combine multiple generators and multiple guards in one expression, which often reads more compactly than the equivalent chain of iterator adapters.Nested comprehensions — Cartesian products
fn main() {
let mut pairs = Vec::new();
for first in 1..=3 {
for second in 1..=3 {
if first != second {
pairs.push((first, second));
}
}
}
println!("{pairs:?}");
} main :: IO ()
main = do
let pairs = [(first, second) | first <- [1..3 :: Int], second <- [1..3 :: Int], first /= second]
print pairs Rust expresses a Cartesian product with nested
for loops that push into a mutable vector. Haskell expresses the same computation with multiple generators in a single comprehension, first <- [1..3], second <- [1..3], which iterates every combination — no explicit nesting, no mutable accumulator, and no separate loop bodies required.Function Composition
Function composition — the . operator
fn main() {
let double = |x: i32| x * 2;
let negate = |x: i32| -x;
// Rust has no built-in composition operator — chain calls manually:
let double_then_negate = |x: i32| negate(double(x));
println!("{}", double_then_negate(5));
} main :: IO ()
main = do
let double = (* 2) :: Int -> Int
let negateValue = negate :: Int -> Int
-- (.) composes right-to-left: (f . g) x = f (g x)
let doubleThenNegate = negateValue . double
print (doubleThenNegate 5) Rust has no built-in operator for composing two functions into a new one — the idiomatic approach is a closure that manually calls one function on the result of the other, as shown here. Haskell's
. operator composes functions directly: negateValue . double builds a brand-new function without ever naming its argument, reading right-to-left just like mathematical function composition notation, f ∘ g.Building pipelines with composition
fn main() {
let words = "the quick brown fox";
// Rust chains method calls left-to-right to build a pipeline:
let result: Vec<String> = words
.split_whitespace()
.rev()
.map(|word| word.to_uppercase())
.collect();
println!("{result:?}");
} import Data.List (words)
import Data.Char (toUpper)
main :: IO ()
main = do
let sentence = "the quick brown fox"
-- Composed right-to-left: reverse the word list, then uppercase each.
let pipeline = map (map toUpper) . reverse . words
print (pipeline sentence) Rust builds a processing pipeline by chaining methods left-to-right in the order each step happens: split, then reverse, then uppercase. Haskell's
.-composed pipeline, map (map toUpper) . reverse . words, reads in the opposite order — rightmost first — because . mirrors mathematical composition. This right-to-left reading is one of the most common early sources of confusion for a Rust programmer new to Haskell, precisely because it inverts the left-to-right method-chain intuition Rust builds.The $ operator — avoiding parentheses
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
// Rust relies on method chaining rather than nested parentheses,
// so it rarely needs a dedicated "avoid parens" operator:
let total: i32 = numbers.iter().map(|x| x * 2).sum();
println!("{total}");
} main :: IO ()
main = do
let numbers = [1, 2, 3, 4, 5] :: [Int]
-- Without $, this would need extra parentheses:
-- print (sum (map (* 2) numbers))
print $ sum $ map (* 2) numbers The
$ operator is simply function application with the lowest possible precedence, so f $ g $ x means exactly f (g x) but without the nested parentheses. Rust rarely needs an equivalent because its dot-chained method calls already avoid nested-parenthesis pileups; Haskell's prefix function-call style means $ earns its keep specifically to keep deeply nested calls readable.Gotchas for Rust Programmers
Gotcha: whitespace is significant, no semicolons
fn main() {
let first = 1;
let second = 2;
// Braces and semicolons define structure — indentation is
// stylistic only and never changes what the code means.
if first < second {
println!("first is smaller");
}
} main :: IO ()
main = do
let first = 1 :: Int
let second = 2 :: Int
-- Indentation IS the structure here: every line of this do block
-- must align to the same column, or the parser sees a different
-- (and usually broken) program. There are no semicolons or braces.
if first < second
then putStrLn "first is smaller"
else putStrLn "first is not smaller" Rust uses braces and semicolons to delimit blocks and statements, so indentation is purely a style convention enforced by
rustfmt, not the compiler. Haskell's "layout rule" makes indentation itself part of the grammar: every binding inside a do block, where clause, or let group must line up at the same column, and shifting a line's indentation can silently change — or break — what the code parses as. This is a frequent early source of confusing compile errors for newcomers.Gotcha: :: means type annotation, not path separator
fn main() {
// In Rust, :: separates a module/type path from an item:
let numbers = Vec::<i32>::new();
let maximum = std::cmp::max(3, 7);
println!("{numbers:?} {maximum}");
} main :: IO ()
main = do
-- In Haskell, :: means "has type" — it is a type annotation, not a
-- path separator. Module-qualified names use a single dot instead,
-- e.g. "Data.List.sort" or "Map.lookup" after a qualified import.
let numbers = [] :: [Int]
let maximum = max (3 :: Int) 7
print numbers
print maximum This symbol overlap is a classic trap: Rust's
:: separates path segments, as in std::cmp::max or Vec::<i32>::new(). Haskell's :: means something entirely different — "has type," used to annotate an expression's type, as in numbers :: [Int]. Haskell's module-qualification instead uses a single dot, Data.List.sort, which is itself easy to confuse with Rust's field-access dot at first glance.Gotcha: immutability is the norm, not the exception
fn main() {
let mut total = 0;
for value in 1..=5 {
total += value; // ordinary, routine mutation
}
println!("{total}");
} main :: IO ()
main = do
-- There is no direct translation of "total += value" here: ordinary
-- Haskell bindings simply cannot be mutated in place. The idiomatic
-- rewrite replaces the loop with a fold that produces a new value.
let total = foldr (+) 0 [1..5 :: Int]
print total Rust makes mutability opt-in but common — reaching for
mut to accumulate a running total in a loop is completely ordinary, idiomatic Rust. Haskell makes immutability the unyielding default for all pure code, with no lightweight equivalent of mut at all; accumulating a value always means transforming an old value into a new one, typically via a fold, recursion, or a comprehension, rather than mutating a variable in place. A Rust programmer's instinct to reach for a mutable accumulator needs to be retrained toward "what fold produces this value" instead.Gotcha: laziness can cause surprising memory or evaluation-order behavior
fn main() {
let mut total: i64 = 0;
for value in 1..=1_000_000_i64 {
total += value; // each addition happens immediately — no buildup
}
println!("{total}");
} import Data.List (foldl')
main :: IO ()
main = do
-- Plain foldl (not shown running here) can build a huge chain of
-- unevaluated thunks over a million additions and blow the stack.
-- foldl' forces each intermediate sum immediately, avoiding that trap.
let total = foldl' (+) 0 [1..1000000 :: Integer]
print total Because Rust evaluates eagerly, a running total computed in a loop never accumulates unevaluated work — each addition simply happens. Because Haskell evaluates lazily, the naive equivalent (a plain, non-strict
foldl) can silently build up a million-deep chain of unevaluated additions instead of a single number, and only try to evaluate all of them at once at the very end — sometimes overflowing the stack. Reaching for the strict foldl' (or otherwise forcing intermediate values) is a necessary habit for a Rust programmer to build when writing accumulation-heavy Haskell code.Gotcha: monomorphism restriction surprises
fn double_it(value: i32) -> i32 {
value * 2
}
fn main() {
// Rust requires an explicit numeric type somewhere; there is no
// ambiguity to resolve once the type is written down.
println!("{}", double_it(21));
} main :: IO ()
main = do
-- Without a type annotation, a top-level "let" binding like
-- total = 21 + 21
-- can be defaulted by the compiler to a specific numeric type
-- (Integer) even though the expression itself looks fully generic.
-- This surprises newcomers who expect it to stay polymorphic.
let total = (21 :: Int) + 21
print total Rust requires numeric literals to eventually resolve to a concrete type, and once you have written that type down, there is no further ambiguity to reason about. Haskell's "monomorphism restriction" is a subtler trap: a top-level or
let-bound value with no type signature and no arguments can be silently defaulted to a specific type (usually Integer or Double) rather than staying as general as its usage would otherwise allow, which can produce confusing type errors when that binding is later used somewhere expecting a different numeric type. Adding an explicit type signature to top-level bindings, as this codebase does throughout, avoids the surprise entirely.