PONYλM2Modula-2

Swift.CodeCompared.To/Roc

An interactive executable cheatsheet comparing Swift and Roc

Swift 6.3 Roc nightly
Hello World & The Platform Model
Hello, World
A Roc program is a definition of main!, a function the platform calls with the command-line arguments — there is no top-level script mode. The ! at the end of a name means "this performs effects", and the compiler enforces it.
print("Hello, World!")
main! = |_args| { echo!("Hello, World!") Ok({}) }
The _args parameter is named with a leading underscore because it is required by the signature and unused — the same convention Swift uses for an ignored closure parameter. Ok({}) is the return value, where {} is the empty record: Roc's Void.
Where the standard library comes from
Swift already has a version of this idea: what you can import depends on which platform you target, and a package declares the platforms it supports. Roc makes it total — a platform is a host program in Rust or Zig that owns the entry point, the allocator and the complete list of effects an application may perform.
import Foundation // Foundation, Dispatch and the rest are there // because a platform (Darwin, or corelibs on // Linux) provides them. print("the runtime came with the platform")
main! = |_args| { # Nothing is ambient. This program was built # against a platform providing exactly echo!, # so echo! is the only effect it can perform. echo!("every effect comes from the platform") Ok({}) }
The difference is that the list is checked. A Roc application cannot reach for an effect its platform does not provide, so there is no equivalent of code that builds on Darwin and fails to link on Linux because Foundation is a different library there.
How a program reports failure
Swift signals failure by throwing, or by calling exit somewhere along the way. Roc's main! returns a Try: Ok for success and Err for failure, and the platform turns that into whatever the operating system wants.
import Foundation func run() -> Int32 { print("all good") return 0 } let status = run() if status != 0 { exit(status) }
main! = |_args| { echo!("all good") Ok({}) }
Because the exit status is the function's return value, the compiler type-checks it. There is no way to call exit from deep inside a library and skip every defer on the way out.
Comments
Comments start with #, and there is no block form and no documentation-comment syntax, so a multi-line comment is several # lines.
// A single-line comment let count = 42 // an inline comment /* A block comment, which can nest. */ print(count)
main! = |_args| { # A single-line comment count : I64 count = 42 # an inline comment # Roc has no block comment and no doc comment — # every comment line starts with its own #. echo!(count.to_str()) Ok({}) }
The count : I64 line is a type annotation on its own line above the definition, rather than after the name as in Swift. Writing it is optional, but it pins down which number type this is — and that decides what gets printed.
Types & Inference
Inference covers whole programs
Swift's inference is strong within a function and stops at its boundary: parameters always need types, and so does the return type of anything but a single-expression closure. Roc infers across the whole program.
// Swift infers locals and single-expression // closures, but never a function's parameters or // its declared return type. func double(_ number: Int) -> Int { number * 2 } let result = double(21) print(result)
# Roc infers the whole signature from the body and # the call sites, so the annotation is optional. double = |number| number * 2 main! = |_args| { result : I64 result = double(21) echo!(result.to_str()) Ok({}) }
Roc convention is to annotate top-level functions anyway, for the same readability reason Swift requires it — and the compiler checks the annotation against the inferred type rather than taking it as the definition. There are also no argument labels, so the call site is bare.
Records are structural, not nominal
A Roc record type is its set of fields, so a literal with the right fields already is one. Swift structs are nominal: two structs with identical stored properties are different types.
struct Point { let x: Int let y: Int } func describe(_ point: Point) -> String { "(\(point.x), \(point.y))" } print(describe(Point(x: 1, y: 2))) // An identically-shaped struct of another name // would NOT be accepted.
Point : { x : I64, y : I64 } describe : Point -> Str describe = |point| "(${point.x.to_str()}, ${point.y.to_str()})" main! = |_args| { echo!(describe({ x: 1, y: 2 })) Ok({}) }
Point here is an alias, not a struct — nothing is constructed and no name appears at the call site. When you want Swift's nominal behavior, := instead of : gives it to you; see the newtype row in Gotchas.
No Any, and no as? to need
Roc has no Any, no as? and no as!. A value's type is fixed and known, so where Swift would test and conditionally cast, Roc makes the possibilities explicit as a tag union.
func describe(_ value: Any) -> String { if let number = value as? Int { return "a number: \(number)" } if let text = value as? String { return "a string: \(text)" } return "something else" } print(describe(42)) print(describe("hi")) print(describe(2.5))
describe : [Number(I64), Text(Str)] -> Str describe = |value| match value { Number(number) => "a number: ${number.to_str()}" Text(text) => "a string: ${text}" } main! = |_args| { echo!(describe(Number(42))) echo!(describe(Text("hi"))) Ok({}) }
The Roc version has two cases and needs no fallback, because its type says there are exactly two. The Swift version needs a third branch for everything it was not designed for, and that branch is where a mistake lands silently.
Optionals vs No Optional At All
There is no Optional
Optionals are among Swift's best ideas, and Roc does not have them — because it has nothing for them to wrap. There is no nil, so absence is a tag in an ordinary union.
func findUser(_ userID: Int) -> String? { userID == 1 ? "Ada" : nil } if let name = findUser(1) { print("found \(name)") } else { print("missing") }
find_user : U32 -> [Found(Str), Missing] find_user = |user_id| { if user_id == 1 { Found("Ada") } else { Missing } } main! = |_args| { match find_user(1) { Found(name) => echo!("found ${name}") Missing => echo!("missing") } Ok({}) }
The practical difference is that a Roc "nothing" can say which nothing. Missing, NotYetLoaded and Refused are three distinct values in the same union, where Swift would have one nil for all three and a comment explaining which is meant.
There is no ! to force with
Swift's optional safety has a deliberate escape hatch: ! asserts that a value is present without a check, and traps at run time if it is not. Roc has no such operator, because it has nothing to assert about.
let text: String? = "42" // ! promises the compiler something it cannot // check. Get it wrong and the program traps. print(Int(text!)!)
main! = |_args| { # There is nothing to force. A Try must be # unwrapped by handling both cases, or by # supplying a default with ??. parsed = I64.from_str("42") ?? 0 echo!(parsed.to_str()) Ok({}) }
The implicitly-unwrapped optional (String!) is the same hatch widened, and it exists mostly for Objective-C interoperation and for outlets set after initialization. A language with neither of those does not need it.
Nil-coalescing is spelled ??, in both
The operator is the same character and the same idea: take the value out if it is there, otherwise use the fallback.
let numbers: [Int] = [] let first = numbers.first ?? 0 print(first) let settings = ["verbose": "true"] print(settings["retries"] ?? "3")
main! = |_args| { numbers : List(I64) numbers = [] first = numbers.first() ?? 0 echo!(first.to_str()) settings = Dict.empty().insert("verbose", "true") echo!(settings.get("retries") ?? "3") Ok({}) }
The difference is what it unwraps. Swift's ?? takes an Optional, which can only say "nothing"; Roc's takes a Try, whose Err side could have carried a reason that ?? chooses to discard. Everything else about the row is identical, which is why this is the easiest transition on the page.
No ?. — a field always exists
There is no optional chaining in Roc, because a record field always exists — asking whether it is there would have only one answer. Where the value might be absent, that is said in the type, with a tag.
struct Address { let city: String? } struct Person { let address: Address? } let person = Person(address: Address(city: "Cupertino")) print(person.address?.city ?? "unknown") let unknown = Person(address: nil) print(unknown.address?.city ?? "unknown")
Address : { city : [Known(Str), Unknown] } Person : { address : [Known(Address), Unknown] } city_of : Person -> Str city_of = |person| match person.address { Unknown => "unknown" Known(address) => match address.city { Unknown => "unknown" Known(city) => city } } main! = |_args| { echo!(city_of({ address: Known({ city: Known("Cupertino") }) })) echo!(city_of({ address: Unknown })) Ok({}) }
The Roc column is longer, and that is the honest trade: ?. chains are genuinely concise. What they hide is that each link silently short-circuits the whole expression, so a nil two levels down and a nil at the top are indistinguishable at the end — the match version has to say which.
let, var & Value Semantics
Value semantics, everywhere, with no opt-out
Swift's value semantics are the closest thing on this site to Roc's model — a struct is copied on assignment, and copy-on-write makes that cheap. The difference is that Swift also has class, and choosing between them is a decision on every type.
struct Counter { var value = 0 } class SharedCounter { var value = 0 } var first = Counter() var second = first // a copy second.value = 99 print(first.value) // 0 let third = SharedCounter() let fourth = third // a reference fourth.value = 99 print(third.value) // 99
main! = |_args| { # There is no class, so there is no reference to # be shared and no decision to make. first = { value: 0.I64 } second = { ..first, value: 99 } echo!(first.value.to_str()) echo!(second.value.to_str()) Ok({}) }
The Swift column shows the decision costing something: the same two lines mean opposite things depending on which keyword was used three lines earlier. In Roc there is one kind of value and no keyword to choose, and the compiler still avoids the copy whenever it can prove nobody else holds the original.
A name is bound once
Roc's = defines a name once. A second definition in the same scope is an error rather than a reassignment, and there is no var at the top level to opt out with.
var greeting = "hello" greeting = "rebound" print(greeting)
main! = |_args| { greeting = "hello" # greeting = "rebound" # ^ COMPILE ERROR: duplicate definition echo!(greeting) Ok({}) }
Shadowing is unavailable too, which is stricter than let in a nested scope. Each step of a calculation gets its own name, so a variable cannot quietly mean something different fifteen lines further down.
Opting in to mutation
Roc borrows the keyword and adds a sigil: var declares it and $ marks every use, so mutation is visible where you read it rather than at a declaration further up.
var total = 0 total = total + 5 total = total + 10 print(total)
main! = |_args| { var $total = 0.I64 $total = $total + 5 $total = $total + 10 echo!($total.to_str()) Ok({}) }
A var is local to its function and cannot escape it, so there is no stored property to mutate, no inout parameter, and no mutating func — the three places Swift lets mutation cross a boundary.
Destructuring
Tuple destructuring works the way Swift's does, including from a function that returns a tuple. Record destructuring has no Swift equivalent: naming the fields pulls them out by name.
func coordinates() -> (Int, Int) { (3, 4) } let (x, y) = coordinates() print("\(x), \(y)") struct Person { let name: String; let age: Int } let person = Person(name: "Grace", age: 85) let (name, age) = (person.name, person.age) print("\(name): \(age)")
coordinates : () -> (I64, I64) coordinates = || (3, 4) main! = |_args| { (x, y) = coordinates() echo!("${x.to_str()}, ${y.to_str()}") person = { name: "Grace", age: 85.I64 } { name, age } = person echo!("${name}: ${age.to_str()}") Ok({}) }
Swift has to name each property twice to unpack a struct, which is why struct-to-local unpacking is rare in Swift code. Matching by name also means reordering two fields cannot silently swap them, which positional destructuring permits.
Enums vs Tag Unions
An enum with associated values
This is the row where Swift is already there. An enum with associated values and an exhaustive switch is a tagged union with exhaustiveness checking, and both compilers reject an incomplete match.
enum Shape { case circle(Double) case rectangle(Double, Double) } func area(_ shape: Shape) -> Double { switch shape { case .circle(let radius): return 3.14159 * radius * radius case .rectangle(let width, let height): return width * height } } print(area(.circle(2))) print(area(.rectangle(3, 4)))
Shape := [Circle(Dec), Rectangle(Dec, Dec)] area : Shape -> Dec area = |shape| match shape { Circle(radius) => 3.14159 * radius * radius Rectangle(width, height) => width * height } main! = |_args| { echo!(area(Shape.Circle(2)).to_str()) echo!(area(Shape.Rectangle(3, 4)).to_str()) Ok({}) }
The syntax is a little denser in Roc — one line for the union, and no let inside the pattern — and the substance is the same. What Roc adds is in the next three rows: no declaration required, and unions that can stay open.
Tags that need no declaration at all
A tag can be used with no declaration anywhere. Morning is a value the moment you write it, and its type is inferred as the set of tags that can reach that position.
enum Period { case morning case afternoon } let hour = 14 let period = hour < 12 ? Period.morning : Period.afternoon let label: String switch period { case .morning: label = "AM" case .afternoon: label = "PM" } print(label)
main! = |_args| { hour : I64 hour = 14 period = if hour < 12 { Morning } else { Afternoon } label = match period { Morning => "AM" Afternoon => "PM" } echo!(label) Ok({}) }
Swift needs the enum declaration before either use. The exhaustiveness guarantee is identical — both compilers reject a missing case — but getting it without a declaration changes how readily you reach for a union at all, for a two-state value that would otherwise become a Bool.
Open unions, which an enum cannot be
The .. in the type means "and possibly other tags". The function handles two by name and everything else with a wildcard, and callers may pass tags that did not exist when it was written.
// A Swift enum is closed by definition — that is // what makes the switch exhaustive. "These two, or // anything else" means giving up the type. func describe(_ signal: Any) -> String { if let text = signal as? String { if text == "go" { return "go" } if text == "stop" { return "stop" } } return "something else" } print(describe("go")) print(describe(7))
describe : [Go, Stop, ..] -> Str describe = |signal| match signal { Go => "go" Stop => "stop" _ => "something else" } main! = |_args| { echo!(describe(Go)) echo!(describe(Custom(7.I64))) Ok({}) }
This has no Swift equivalent. An enum is exhaustive precisely because it is closed — the @unknown default case exists only for library-evolution enums you do not own — so "these two, plus whatever turns up" means Any and losing the check on the two you named.
Recursive types without indirect
A declared union may mention itself, and nothing has to be said about it. Swift needs the indirect keyword, which is the point where the boxing becomes the programmer's business.
indirect enum Tree { case leaf(Int) case node(Tree, Tree) } func sumTree(_ tree: Tree) -> Int { switch tree { case .leaf(let value): return value case .node(let left, let right): return sumTree(left) + sumTree(right) } } let tree = Tree.node(.leaf(1), .node(.leaf(2), .leaf(3))) print(sumTree(tree))
Tree := [Leaf(I64), Node(Tree, Tree)] sum_tree : Tree -> I64 sum_tree = |tree| match tree { Leaf(value) => value Node(left, right) => sum_tree(left) + sum_tree(right) } main! = |_args| { tree = Tree.Node(Tree.Leaf(1), Tree.Node(Tree.Leaf(2), Tree.Leaf(3))) echo!(sum_tree(tree).to_str()) Ok({}) }
Roc works out where a pointer is needed and inserts it. That is a small convenience here and a real one in a large type, where indirect has to be applied to the enum or to each recursive case and forgetting it is a compile error about infinite size.
Exhaustiveness, in both languages
Both compilers name the case you forgot. This is the strongest thing Swift has in common with Roc and the reason a Swift programmer takes to tag unions immediately.
enum Color { case red, green, blue } func toHex(_ color: Color) -> String { switch color { case .red: return "#FF0000" case .green: return "#00FF00" // Deleting .blue is a COMPILE ERROR: // "switch must be exhaustive" case .blue: return "#0000FF" } } print(toHex(.green)) print(toHex(.blue))
Color := [Red, Green, Blue] to_hex : Color -> Str to_hex = |color| match color { Red => "#FF0000" Green => "#00FF00" # Deleting the next line is a COMPILE ERROR # naming Blue as the case not handled. Blue => "#0000FF" } main! = |_args| { echo!(to_hex(Color.Green)) echo!(to_hex(Color.Blue)) Ok({}) }
The one asymmetry is what a match is. Every Roc match is an expression producing a value, so the arms must agree on a type; a Swift switch is a statement, which is why the enum row above needs a return per case or a pre-declared let.
Numbers
A familiar menu of sizes
Roc has I8 through I128, U8 through U128, F32 and F64 — Swift's menu, and neither language widens implicitly.
let byteValue: UInt8 = 255 let ratio: Double = 2.5 print("\(byteValue) \(ratio)")
main! = |_args| { byte : U8 byte = 255 ratio : F64 ratio = 2.5 echo!("${byte.to_str()} ${ratio.to_str()}") Ok({}) }
What Roc adds is Dec, a fixed-point decimal type with no Swift equivalent outside Foundation's Decimal. What it removes is Int and UInt, the word-sized defaults, so every integer in a Roc program has a width you can name.
Exact decimals, without Foundation
Dec is a fixed-point decimal type and an ordinary member of the number menu — same operators, same literals — and it is what an unannotated decimal literal becomes.
import Foundation print(0.1 + 0.2) print(Decimal(string: "0.1")! + Decimal(string: "0.2")!)
main! = |_args| { lossy : F64 lossy = 0.1 + 0.2 echo!(lossy.to_str()) precise : Dec precise = 0.1 + 0.2 echo!(precise.to_str()) Ok({}) }
Decimal is the Swift answer and it costs a Foundation import, a failable string initializer, and a decision at every arithmetic site. Here the difference between the two Roc lines is one annotation.
Integer division and remainder
Roc separates the two divisions into two operators: // floors and / divides. Swift uses / for both and decides which you meant from the operand types.
print(17 / 5) // 3: both operands are Int print(17 % 5) print(17.0 / 5.0)
main! = |_args| { quotient : I64 quotient = 17 // 5 echo!(quotient.to_str()) remainder : I64 remainder = 17 % 5 echo!(remainder.to_str()) exact : Dec exact = 17 / 5 echo!(exact.to_str()) Ok({}) }
That decision is the trap: 17 / 5 and 17.0 / 5.0 are different operations spelled almost identically, and changing a variable from Int to Double silently changes which one runs. Two operators cannot be confused that way.
Overflow is caught in both, differently
Swift already refuses to wrap silently — big + 1 traps, and &+ is the opt-in that wraps. Roc goes one step further when both operands are known at compile time, as here: the error arrives before the program runs.
let big = Int.max // big + 1 would TRAP at run time. let (sum, overflowed) = big.addingReportingOverflow(1) print("\(sum) overflowed=\(overflowed)")
main! = |_args| { big : I64 big = 9_223_372_036_854_775_807 # echo!((big + 1).to_str()) # ^ COMPILE ERROR: "Integer addition overflowed!" echo!(big.to_str()) Ok({}) }
The Swift column uses addingReportingOverflow so it can print rather than trap, which is the same shape as a Roc Try: a value plus a report, instead of a crash. This is one of the closest agreements between the two languages.
Strings
Interpolation, with different punctuation
Roc spells interpolation ${} where Swift spells it \(). The substantive difference is that Roc's takes a Str and will not convert a number for you.
let name = "Roc bird" let age = 10 print("\(name) is \(age)") let message = "\(name) turns \(age + 1)" print(message)
main! = |_args| { name = "Roc bird" age : I64 age = 10 echo!("${name} is ${age.to_str()}") message = "${name} turns ${(age + 1).to_str()}" echo!(message) Ok({}) }
Swift's interpolation accepts anything and calls its description or reflects over it, which is how a struct's default reflection output ends up in a user-facing message. "${age}" in Roc is a type error naming I64 where Str was expected.
Concatenation without +
Roc reserves + for numbers. Joining two strings is concat, either as a method on the left-hand string or as a plain function.
print("Fast " + "and friendly") // "count: " + 5 does NOT compile: Swift's + is // String + String only. print("count: " + String(5))
main! = |_args| { echo!("Fast ".concat("and friendly")) echo!(Str.concat("also", " works")) # "count: ".concat(5) does not compile: # concat takes two Str values. Ok({}) }
Swift already refuses to add a number to a string, so both languages make you convert. The difference is only which spelling: String(5) against 5.to_str(), and + against concat.
Everyday string methods
The same operations with shorter names and no import. Str.inspect turns a non-string value into something printable, which is the job Swift's print(Any) does implicitly.
import Foundation let padded = " systems " print(padded.trimmingCharacters(in: .whitespaces)) print(String(repeating: "ab", count: 3)) print("systems".hasPrefix("sys")) print("systems".contains("stem"))
main! = |_args| { padded = " systems " echo!(padded.trim()) echo!("ab".repeat(3)) echo!(Str.inspect("systems".starts_with("sys"))) echo!(Str.inspect("systems".contains("stem"))) Ok({}) }
Trimming is the one worth noticing: Swift's lives in Foundation and takes a character set, which is more general and more to type. Roc's trim removes whitespace and that is all it does.
Splitting and joining
Splitting is split_on and joining is Str.join_with, which takes the list first and the separator second.
let parts = "red,green,blue".split(separator: ",") print(parts.count) print(parts.joined(separator: " | "))
main! = |_args| { parts = "red,green,blue".split_on(",") echo!(parts.len().to_str()) echo!(Str.join_with(parts, " | ")) Ok({}) }
Swift's split returns [Substring], a view into the original string, which is efficient and occasionally surprising when the substring outlives its purpose. Roc's returns a list of real strings, and there is no Substring type to convert back from.
Bytes rather than grapheme clusters
Swift's String is a collection of extended grapheme clusters — the most linguistically correct model of any language here, and the reason count is O(n) and indices are opaque. A Roc Str is UTF-8 bytes with no character view at all.
print("rocket: \u{1F680}") print("héllo".count) // 5 characters print("héllo".utf8.count) // 6 bytes
main! = |_args| { echo!("rocket: \u(1F680)") echo!("héllo".count_utf8_bytes().to_str()) Ok({}) }
This is a place Swift is unambiguously better. Roc has no Character type, no grapheme awareness, and no way to ask how many user-perceived characters a string has — what it has is a byte count and operations that do not let you cut UTF-8 in half.
Arrays vs Lists
A list is a value, as an Array is
Same literal syntax, and the same underlying idea: a flat, contiguous array of one element type, held by value with copy-on-write behind it.
let numbers = [3, 1, 4, 1, 5] print(numbers.count) print(numbers)
main! = |_args| { numbers : List(I64) numbers = [3, 1, 4, 1, 5] echo!(numbers.len().to_str()) echo!(Str.inspect(numbers)) Ok({}) }
This is another close agreement. Swift's Array is a struct with copy-on-write, so assigning it copies only when someone writes — which is exactly what Roc's reference counting achieves, without a isKnownUniquelyReferenced check anyone has to write.
map and filter
The same chain in the same order, with keep_if in place of filter and a named parameter in place of $0.
let numbers = [1, 2, 3, 4, 5, 6] let doubledEvens = numbers .filter { $0 % 2 == 0 } .map { $0 * 2 } print(doubledEvens)
main! = |_args| { numbers : List(I64) numbers = [1, 2, 3, 4, 5, 6] doubled_evens = numbers .keep_if(|number| number % 2 == 0) .map(|number| number * 2) echo!(Str.inspect(doubled_evens)) Ok({}) }
Roc has no shorthand argument names and no trailing-closure syntax, so every parameter is written out. That costs a few characters and removes the question of what $0 refers to three levels into a chain. There is also no lazy, so each step builds a real list.
reduce becomes fold
fold is reduce with the same argument order: the starting value first, the function second.
let numbers = [1, 2, 3, 4] print(numbers.reduce(0, +)) print(numbers.reduce(0) { total, number in total + number })
main! = |_args| { numbers : List(I64) numbers = [1, 2, 3, 4] total = numbers.fold(0, |accumulator, number| accumulator + number) echo!(total.to_str()) echo!(numbers.sum().to_str()) Ok({}) }
Swift can pass an operator directly (reduce(0, +)) because operators are functions; Roc's are not first-class values, so the closure is written out. sum covers the common case in both.
Indexing cannot trap
Roc has one subscript operation and it returns a Try. Swift has a subscript that traps and no safe counterpart in the standard library — the bounds check has to be written separately.
let numbers = [10, 20, 30] // numbers[9] TRAPS: "Index out of range", so the // bounds check has to be written separately. print(numbers.indices.contains(9) ? "\(numbers[9])" : "out of bounds") print(numbers[1])
main! = |_args| { numbers : List(I64) numbers = [10, 20, 30] match numbers.get(9) { Ok(value) => echo!(value.to_str()) Err(_) => echo!("out of bounds") } fallback = numbers.get(1) ?? 0 echo!(fallback.to_str()) Ok({}) }
Swift's trap is a deliberate choice: an out-of-range index is a programmer error, so the program stops rather than continuing with a wrong value. Roc treats it as an ordinary outcome instead, which is why ?? keeps the safe version about as short as the trapping one.
Sorting returns a new list
sort returns a new list and orders by the element type. It is Swift's sorted(), and Roc has no counterpart to sort(), the in-place version.
let numbers = [3, 1, 2] print(numbers.sorted()) print(numbers.sorted(by: >)) print(numbers)
main! = |_args| { numbers : List(I64) numbers = [3, 1, 2] echo!(Str.inspect(numbers.sort())) echo!(Str.inspect(numbers.sort_reversed())) echo!(Str.inspect(numbers)) Ok({}) }
Swift already made the right default: sorted() returns a copy and only a var array gets sort(). The naming is the only thing to relearn — Roc's sort is Swift's sorted.
contains, allSatisfy and first(where:)
The same three operations under shorter names: any, all and find_first.
let numbers = [2, 4, 6, 7] print(numbers.contains { $0 % 2 == 1 }) print(numbers.allSatisfy { $0 > 0 }) if let found = numbers.first(where: { $0 > 5 }) { print("found \(found)") } else { print("none") }
main! = |_args| { numbers : List(I64) numbers = [2, 4, 6, 7] echo!(Str.inspect(numbers.any(|number| number % 2 == 1))) echo!(Str.inspect(numbers.all(|number| number > 0))) match numbers.find_first(|number| number > 5) { Ok(found) => echo!("found ${found.to_str()}") Err(_) => echo!("none") } Ok({}) }
Swift's contains(where:) is overloaded with contains(_:) for equatable elements, and first is both a property and a method taking a predicate. Roc keeps each one a separate name, which is more words and fewer overload rules.
Index and element together
Roc's map passes only the element, so the index needs a different method: map_with_index, which takes the element first and the index second.
let words = ["one", "two"] let labeled = words.enumerated().map { index, word in "\(index):\(word)" } print(labeled)
main! = |_args| { words = ["one", "two"] labeled = words.map_with_index(|word, index| "${index.to_str()}:${word}") echo!(Str.inspect(labeled)) Ok({}) }
Note the argument order is the reverse of enumerated(), which yields the index first. Getting it backwards still type-checks whenever both happen to be numbers, so it is worth reading twice the first few times.
Structs vs Records
Building from defaults
Roc's .. spread copies a record and overrides named fields in one expression, and it cannot introduce a field the record did not already have.
struct Config { var verbose: Bool var retries: Int var timeoutSeconds: Int } let defaults = Config(verbose: false, retries: 3, timeoutSeconds: 30) var custom = defaults // a copy, because it is a value custom.retries = 5 print(custom) print(defaults)
main! = |_args| { defaults = { verbose: Bool.False, retries: 3.I64, timeout_seconds: 30.I64 } custom = { ..defaults, retries: 5 } echo!(Str.inspect(custom)) echo!(Str.inspect(defaults)) Ok({}) }
Swift reaches the same place by a different route — assigning a struct copies it — but only through a var, which makes the copy mutable for the rest of its life. The spread produces a new immutable value and leaves nothing mutable behind.
Naming a shape
A one-line alias names a record shape. Nothing constructs an Employee — the literal already is one, because it has those fields with those types.
struct Employee { let name: String let department: String } func describe(_ employee: Employee) -> String { "\(employee.name) works in \(employee.department)" } print(describe(Employee(name: "Nia", department: "Compilers")))
Employee : { name : Str, department : Str } describe : Employee -> Str describe = |employee| "${employee.name} works in ${employee.department}" main! = |_args| { employee = { name: "Nia", department: "Compilers" } echo!(describe(employee)) Ok({}) }
Swift gives a struct a memberwise initializer for free, which is close; Roc gives every record equality and Str.inspect for free as well, where Swift needs : Equatable and : CustomStringConvertible. The cost is that Roc's version is structural, so any record with the same fields fits.
Dictionary becomes Dict
When the keys really are data, Roc has Dict. Every key shares one type and so does every value, exactly as in a Swift Dictionary.
var scores = ["math": 90, "art": 95] print(scores.count) print(scores["art"] ?? 0) print(scores["music"] ?? 0)
main! = |_args| { scores = Dict.empty() .insert("math", 90.I64) .insert("art", 95.I64) echo!(scores.len().to_str()) echo!((scores.get("art") ?? 0).to_str()) echo!((scores.get("music") ?? 0).to_str()) Ok({}) }
insert returns a new dict rather than changing the old one, which is why the calls chain — a Swift dictionary is also a value type, so the difference is only that Roc has no mutating subscript to reach for. get returns a Try where Swift's subscript returns an Optional.
Tuples, and what they can hold
Both languages have real tuples with a fixed length and mixed element types, indexed with a dot. Swift adds element labels; Roc does not.
func divide(_ numerator: Int, by denominator: Int) -> (Int, Int) { (numerator / denominator, numerator % denominator) } let (quotient, remainder) = divide(17, by: 5) print("\(quotient) remainder \(remainder)") // A Swift tuple cannot conform to a protocol, so // it cannot be Equatable, Hashable or Codable.
divide : I64, I64 -> (I64, I64) divide = |numerator, denominator| (numerator // denominator, numerator % denominator) main! = |_args| { (quotient, remainder) = divide(17, 5) echo!("${quotient.to_str()} remainder ${remainder.to_str()}") pairs = [divide(17, 5), divide(9, 2)] echo!(Str.inspect(pairs)) Ok({}) }
The difference is what a tuple can do. A Swift tuple cannot conform to a protocol, so it is not Equatable, Hashable or Codable — which is why so much Swift code turns a pair into a struct. A Roc tuple gets equality and Str.inspect like every other value, as the second line shows.
switch vs match
switch is a statement; match is an expression
Swift's switch is a statement, so each case assigns to a variable declared before it. Roc's match produces a value, so the whole thing sits on the right of one =.
let statusCode = 404 let message: String switch statusCode { case 200: message = "ok" case 404: message = "not found" default: message = "something else" } print(message)
main! = |_args| { status_code : I64 status_code = 404 message = match status_code { 200 => "ok" 404 => "not found" _ => "something else" } echo!(message) Ok({}) }
Swift already fixed fall-through, so that is not the difference. What is: every Roc arm must produce a value of the same type, so an arm that forgets to assign is a compile error rather than a definite-initialization error further down.
Guards
A guard is a condition attached to a pattern, spelled if in Roc and where in Swift. Both bind a name in the pattern and test it in the guard.
func describe(_ number: Int) -> String { switch number { case 0: return "zero" case let n where n < 0: return "negative" case let n where n % 2 == 0: return "positive even" default: return "positive odd" } } print(describe(0)) print(describe(-5)) print(describe(8))
describe : I64 -> Str describe = |number| match number { 0 => "zero" n if n < 0 => "negative" n if n % 2 == 0 => "positive even" _ => "positive odd" } main! = |_args| { echo!(describe(0)) echo!(describe(-5)) echo!(describe(8)) Ok({}) }
The first arm shows what makes them patterns rather than conditions: 0 is matched, not compared, and the compiler counts it toward exhaustiveness. A guarded arm never counts in either language, which is why the wildcard is still required.
Matching on a list's shape
A Roc pattern can describe a list's shape directly — empty, exactly one element, or a first element plus the rest — and bind the pieces in the same breath.
func describe(_ numbers: [Int]) -> String { switch numbers.count { case 0: return "empty" case 1: return "one: \(numbers[0])" default: return "first \(numbers[0]), \(numbers.count - 1) more" } } print(describe([])) print(describe([7])) print(describe([1, 2, 3]))
describe : List(I64) -> Str describe = |numbers| match numbers { [] => "empty" [single] => "one: ${single.to_str()}" [first, .. as rest] => "first ${first.to_str()}, ${rest.len().to_str()} more" } main! = |_args| { echo!(describe([])) echo!(describe([7])) echo!(describe([1, 2, 3])) Ok({}) }
Swift has tuple patterns and enum patterns but no array pattern, so the same logic switches on a count and then subscripts — and the compiler cannot connect the two, so numbers[0] in the count-1 case is bounds-checked at run time and could trap. In the Roc version the binding is the check.
Or-patterns
Alternatives within one branch are written with | rather than a comma.
func sizeClass(_ number: Int) -> String { switch number { case 1, 2, 3: return "small" default: return "big" } } print(sizeClass(2)) print(sizeClass(9))
size_class : I64 -> Str size_class = |number| match number { 1 | 2 | 3 => "small" _ => "big" } main! = |_args| { echo!(size_class(2)) echo!(size_class(9)) Ok({}) }
Swift also has range patterns (case 1...3) and ~= for custom matching, neither of which Roc has. The alternatives matter more in Roc because they can be tags carrying payloads, and the compiler checks that the bound names agree across them.
Try vs throws and Result
throws becomes the return type
Swift and Roc agree on most of this: failure is a value, there is no unwinding across an unmarked boundary, and the compiler makes the caller deal with it. The difference is the channel — Swift adds a second one with throws, and Roc puts failure in the return type.
import Foundation enum ParseError: Error { case badScore(String) } func parseScore(_ text: String) throws -> Int { guard let score = Int(text.trimmingCharacters(in: .whitespaces)) else { throw ParseError.badScore(text) } return score } for candidate in ["95", "not a number"] { do { print("score: \(try parseScore(candidate))") } catch ParseError.badScore(let bad) { print("bad score: \(bad)") } }
parse_score : Str -> Try(I64, [BadScore(Str)]) parse_score = |text| match I64.from_str(text.trim()) { Ok(score) => Ok(score) Err(_) => Err(BadScore(text)) } main! = |_args| { for candidate in ["95", "not a number"] { match parse_score(candidate) { Ok(score) => echo!("score: ${score.to_str()}") Err(BadScore(bad)) => echo!("bad score: ${bad}") } } Ok({}) }
One consequence is visible in the signatures. throws says a function can fail and, until typed throws arrived, said nothing about how — so the catch clause above is not checked for completeness and a second error type would fall through it silently. Try(I64, [BadScore(Str)]) names the failures, and a match that misses one does not compile.
Result is the same shape
Swift's Result<Success, Failure> is Roc's Try(ok, err) — two type parameters, two cases, and a switch that must handle both. The two columns here are almost line-for-line.
enum ParseError: Error { case badScore(String) } func parseScore(_ text: String) -> Result<Int, ParseError> { guard let score = Int(text) else { return .failure(.badScore(text)) } return .success(score) } switch parseScore("95") { case .success(let score): print("score: \(score)") case .failure(.badScore(let bad)): print("bad score: \(bad)") }
parse_score : Str -> Try(I64, [BadScore(Str)]) parse_score = |text| match I64.from_str(text.trim()) { Ok(score) => Ok(score) Err(_) => Err(BadScore(text)) } main! = |_args| { match parse_score("95") { Ok(score) => echo!("score: ${score.to_str()}") Err(BadScore(bad)) => echo!("bad score: ${bad}") } Ok({}) }
The difference is which one is the default. In Swift, Result is a library type that competes with throws, so a codebase ends up converting between them at every boundary; in Roc there is no second channel to convert from.
try becomes ?
Roc's ? is Swift's try, moved to the end of the expression: it unwraps a success and returns early from the enclosing function on a failure.
enum ListError: Error { case empty } func showFirst(_ numbers: [Int]) throws { guard let first = numbers.first else { throw ListError.empty } print("first: \(first * 2)") } try showFirst([5, 6, 7])
show_first! = |numbers| { first = numbers.first()? echo!("first: ${(first * 2).to_str()}") Ok({}) } main! = |_args| { numbers : List(I64) numbers = [5, 6, 7] show_first!(numbers) }
Both languages mark every call that can exit early, which is the property that makes the control flow readable. Swift also has try?, which discards the error into an optional, and try!, which traps — Roc's equivalents are ?? with a default, and nothing at all.
crash, and fatalError
There is exactly one way to stop a Roc program abruptly, and it is fatalError by another name: uncatchable, and reserved for states the program has already established cannot happen.
func divide(_ numerator: Int, by denominator: Int) -> Int { precondition(denominator != 0, "impossible: checked upstream") return numerator / denominator } print(divide(10, by: 2))
divide : I64, I64 -> I64 divide = |numerator, denominator| if denominator == 0 { # crash is not catchable. It is for states # the program has already established # cannot happen. crash "impossible: checked upstream" } else { numerator // denominator } main! = |_args| { echo!(divide(10, 2).to_str()) Ok({}) }
Swift agrees with Roc here more than any other language on this site. precondition and fatalError are also uncatchable — do/catch does not see them — so the distinction between "a failure a caller should handle" and "a bug" is already one you draw.
Functions & Closures
One function form, and no argument labels
Roc has one way to write a function, and it is the anonymous one. A named function is a name bound to a closure, so the top-level and local forms are identical.
func add(_ left: Int, _ right: Int) -> Int { left + right } let alsoAdd = { (left: Int, right: Int) in left + right } print(add(2, 3)) print(alsoAdd(2, 3))
add : I64, I64 -> I64 add = |left, right| left + right main! = |_args| { also_add = |left, right| left + right echo!(add(2, 3).to_str()) echo!(also_add(2.I64, 3.I64).to_str()) Ok({}) }
There are no argument labels, no _ to suppress them, and no external-versus-internal parameter names — so a Roc call site shows only the values. That is less self-documenting than divide(17, by: 5) and it is also one fewer thing in every signature.
Closures capture values, not variables
A Swift closure captures the variable by reference unless a capture list says otherwise, so a later assignment is visible inside it. A Roc closure captures the value, and the value cannot change.
var amount = 10 let addAmount = { (number: Int) in number + amount } amount = 1000 // the closure sees this print(addAmount(5))
main! = |_args| { amount : I64 amount = 10 add_amount = |number| number + amount # There is no second assignment to "amount", # so nothing can change under the closure. echo!(add_amount(5).to_str()) Ok({}) }
The Swift column prints 1005, not 15. Writing { [amount] number in … } captures the value instead — and that capture list also exists to write [weak self], which is the subject of the ARC section below.
No default or labeled arguments
Roc functions take a fixed number of positional arguments, with no defaults and no variadics. The pattern that replaces them is a record of options and a named set of defaults.
struct Options { var host: String var port: UInt16 = 8080 var verbose: Bool = false } func connect(_ options: Options) -> String { "\(options.host):\(options.port) verbose=\(options.verbose)" } print(connect(Options(host: "example.com"))) print(connect(Options(host: "example.com", verbose: true)))
Options : { host : Str, port : U16, verbose : Bool } connect : Options -> Str connect = |options| "${options.host}:${options.port.to_str()} verbose=${Str.inspect(options.verbose)}" main! = |_args| { defaults = { host: "example.com", port: 8080.U16, verbose: Bool.False } echo!(connect(defaults)) echo!(connect({ ..defaults, verbose: Bool.True })) Ok({}) }
Swift has default property values on a struct, so the two columns land in almost the same place — the difference is that Swift's defaults live on the type and Roc's live in a value you write once and spread from. What is genuinely lost is default arguments on the function itself.
Generic functions
A lowercase name in a Roc signature is a type variable, with no angle brackets to declare it in. a -> a says the function returns exactly the type it was given.
func identity<T>(_ value: T) -> T { value } print(identity("same")) print(identity(7))
identity : a -> a identity = |value| value main! = |_args| { echo!(identity("same")) echo!(identity(7.I64).to_str()) Ok({}) }
Both languages monomorphize, so the generic really is specialized per type rather than erased. Swift can also compile a generic once and dispatch through a witness table when it has to cross a module boundary; Roc has no such fallback, which is simpler and means whole-program compilation.
Control Flow
if is an expression
Roc has no if statement — if produces a value, so the multi-branch choice is written once and reads as a single expression.
let score = 85 let grade: String if score >= 90 { grade = "A" } else if score >= 80 { grade = "B" } else { grade = "C" } print(grade)
main! = |_args| { score : I64 score = 85 grade = if score >= 90 { "A" } else if score >= 80 { "B" } else { "C" } echo!(grade) Ok({}) }
Swift has the ternary for two branches and, since 5.9, if and switch expressions for a single-expression body. The Roc version cannot leave grade unassigned, because every branch must produce a value and an else is required when the result is used.
for ... in
The same keyword and the same shape. Roc's for is available only in effectful code, because a loop that produces no value has nothing to do in a pure function.
for word in ["alpha", "beta", "gamma"] { print(word) }
main! = |_args| { for word in ["alpha", "beta", "gamma"] { echo!(word) } Ok({}) }
There is no range syntax — no 1...10, no stride — and no where clause on the loop, so a filtered iteration is a keep_if first. The same loop can also be written as a method, words.for_each!(|word| echo!(word)).
while and break
Roc has a real while loop with break, which surprises people expecting a functional language to insist on recursion. It needs a var, since a loop over an unchanging condition would never end.
var count = 0 while count < 5 { count += 1 if count == 3 { break } } print(count)
main! = |_args| { var $count = 0.I64 while $count < 5 { $count = $count + 1 if $count == 3 { break } } echo!($count.to_str()) Ok({}) }
There are no compound assignment operators, so count += 1 is written out. There is no repeat ... while, no labeled break and no continue, so a loop that wants them is usually asking to be a fold.
Guard clauses and early return
return exists and does what you expect, so the guard-clause style transfers. The last expression of a block is its value, so the final line needs no return.
func clampPositive(_ number: Int) -> Int { guard number >= 0 else { return 0 } return number } print(clampPositive(-5)) print(clampPositive(9))
clamp_positive : I64 -> I64 clamp_positive = |number| { if number < 0 { return 0 } number } main! = |_args| { echo!(clamp_positive(-5).to_str()) echo!(clamp_positive(9).to_str()) Ok({}) }
There is no guard keyword, and with no optionals there is nothing for guard let to unwrap — the ? operator covers what guard let … else { return } is usually doing. What is lost is the compiler check that a guard's else branch really does leave the scope.
No defer, because there is nothing to release
defer exists to release something the function acquired: a file, a lock, a C allocation. A Roc application acquires none of those — the platform owns every resource, and memory is released by counts the compiler inserted.
func work() { defer { print("cleanup runs last") } print("doing the work") } work()
work! : {} => {} work! = |{}| { echo!("doing the work") # No defer. Nothing here owns a handle, a lock # or a buffer, so there is nothing to release — # the platform owns every resource. echo!("cleanup runs last") } main! = |_args| { work!({}) Ok({}) }
So the feature is missing and most of the need is missing with it. Swift also leans less on defer than C does, because ARC already releases objects at the end of scope — which is the same mechanism Roc uses, with the same consequence.
Recursion without growing the stack
A call in tail position — the last thing a function does — is compiled to a jump rather than a new stack frame, so a tail-recursive Roc function runs in constant stack space, guaranteed by the language.
func sumTo(_ limit: Int, _ accumulator: Int) -> Int { if limit <= 0 { return accumulator } return sumTo(limit - 1, accumulator + limit) } print(sumTo(100000, 0))
sum_to : I64, I64 -> I64 sum_to = |limit, accumulator| { if limit <= 0 { accumulator } else { sum_to(limit - 1, accumulator + limit) } } main! = |_args| { echo!(sum_to(100_000, 0).to_str()) Ok({}) }
Swift has no such guarantee: the optimizer often performs the same transformation in release builds, and nothing in the language promises it, so the identical function can overflow the stack in a debug build. A guarantee and an optimization are different things to rely on.
Protocols vs where Clauses
A protocol constraint becomes a where clause
A where clause states what the function needs from its type variable — here, a describe method with that signature. It is Swift's T: Describable bound, made structural.
protocol Describable { func describe() -> String } struct Celsius: Describable { let degrees: Double func describe() -> String { "\(degrees)°C" } } func announce<T: Describable>(_ value: T) { print(value.describe()) } announce(Celsius(degrees: 21.5))
Celsius := { degrees : Dec }.{ describe : Celsius -> Str describe = |celsius| "${celsius.degrees.to_str()}°C" } announce! : a => {} where [a.describe : a -> Str] announce! = |value| { echo!(value.describe()) } main! = |_args| { announce!(Celsius.{ degrees: 21.5 }) Ok({}) }
Nothing declares that Celsius satisfies the clause: it has the method, so it fits. There is no protocol to define, no conformance to write, and no retroactive-conformance problem for a type you do not own — but also no protocol extension providing a default implementation, which is what Swift builds most of its standard library on.
No existentials — no any Protocol
Roc has no existential types and no dynamic dispatch, so there is no such thing as "an array of anything that can be measured". A heterogeneous collection is a list of one tag union, and the dispatch is a match.
protocol Shape { func area() -> Int } struct Square: Shape { let side: Int func area() -> Int { side * side } } // A heterogeneous array of boxed conformances, // each carrying its own witness table. let shapes: [any Shape] = [Square(side: 2), Square(side: 3)] print(shapes.reduce(0) { $0 + $1.area() })
Shape := [Square(I64)] area : Shape -> I64 area = |shape| match shape { Square(side) => side * side } main! = |_args| { shapes = [Shape.Square(2), Shape.Square(3)] total = shapes.fold(0, |running, shape| running + area(shape)) echo!(total.to_str()) Ok({}) }
The trade runs both ways. Swift's version is open — a new shape in another module joins the array without touching this code — while Roc's is closed and therefore exhaustively checked. Choose the tag union when the set of cases should be complete; Swift's any Shape when it is genuinely open-ended.
Methods become a block on the type
The .{ } after a type definition is a block of functions associated with that type. There is no self — each function takes the value as an ordinary first parameter.
struct Counter { var value = 0 func increment() -> Counter { Counter(value: value + 1) } func describe() -> String { "count is \(value)" } } print(Counter().increment().increment().describe())
Counter := { value : I64 }.{ new : () -> Counter new = || { value: 0 } increment : Counter -> Counter increment = |{ value }| { value: value + 1 } describe : Counter -> Str describe = |counter| "count is ${counter.value.to_str()}" } main! = |_args| { counter = Counter.new().increment().increment() echo!(counter.describe()) Ok({}) }
Method syntax works because the compiler resolves counter.describe() from the type it already knows. There is no mutating modifier to reach for, because nothing can mutate, and no extension to add a method later.
async vs the ! Marker
async colors for time; ! colors for everything
You already know this mechanism. async colors a function so that only another async context may await it, and the color propagates up the call stack. Roc does exactly that with ! — for every side effect rather than only for suspension.
// An async function can only be awaited from // another async context — the same propagation // Roc applies to every effect. func describe(_ name: String) -> String { "hello \(name)" } func announce(_ name: String) { print(describe(name)) } announce("Roc")
# Pure: Str -> Str (thin arrow) describe : Str -> Str describe = |name| "hello ${name}" # Effectful: Str => {} (fat arrow, name ends in !) announce! : Str => {} announce! = |name| { echo!(describe(name)) } main! = |_args| { announce!("Roc") Ok({}) }
The consequence is the one async has: a pure function that gains a print has to change its signature, and so does every caller. Note that announce in the Swift column is not marked in any way despite printing — which is the gap the ! marker closes.
No async, no await, no actors
The coloring idea carries over; the concurrency does not. There is no async, no await, no Task, no actor and no Sendable — none of it exists in the language.
// Roc has no equivalent of any of this: // // let first = async let a = compute(1) // await withTaskGroup(of: Int.self) { … } // actor Counter { var value = 0 } // // Concurrency is not part of the language at all. let results = [1, 2].map { $0 * $0 } print(results)
main! = |_args| { # Whether work like this can be spread across # cores is the PLATFORM's decision, and the # application cannot express it. results = [1.I64, 2].map(|number| number * number) echo!(Str.inspect(results)) Ok({}) }
The reasoning is the platform model: a platform written in Rust may run an application's work across every core, and the application stays pure code that says what to compute rather than when. Next to Swift's structured concurrency and actor isolation, this is the largest single thing the page asks you to give up.
expect is part of the language
expect is a keyword rather than a library function, and it can appear at the top level of a file as well as inside one — which is how Roc writes unit tests without a testing framework.
let total = 2 + 2 assert(total == 4) print("the assertion held")
main! = |_args| { total : I64 total = 2 + 2 expect total == 4 echo!("the assertion held") Ok({}) }
A failing expect prints every value that fed the expression, not just the expression that was false. Swift's assert is also removed in release builds by default, where a Roc expect has no such mode.
ARC, Without the Retain Cycle
Reference counting, with no cycle to break
This is the row that matters most on this page. Swift and Roc use the same memory strategy — reference counting, no tracing collector — and Swift has to give you weak and unowned because of exactly what the fourth line here does.
class Peer { let name: String var peer: Peer? init(name: String) { self.name = name } } let first = Peer(name: "first") let second = Peer(name: "second") second.peer = first first.peer = second // a retain cycle: neither // will ever be deallocated print(second.peer!.name) print(first.peer!.name)
main! = |_args| { first = { name: "first" } second = { name: "second", peer: first } # first cannot be made to point back at second: # it was finished the moment it was defined, so # this program has no second line to print. echo!(second.peer.name) Ok({}) }
Two objects holding each other keep each other's count above zero forever, and no amount of scope exit will free them. Roc has no such case: nothing can be made to point back at a value that already exists, so counting alone is sufficient and there is no weak keyword, no [weak self] capture list, and no leak to find with Instruments.
Copy-on-write, without writing it
Swift's Array is copy-on-write: assigning shares the buffer, and the first write copies it. Roc does the same thing from the other direction — set builds a new list semantically, and mutates in place when the count proves nobody else is holding the old one.
var numbers = [1, 2, 3] let updated = numbers // no copy yet numbers[1] = 99 // the write triggers it print(numbers) print(updated)
main! = |_args| { numbers : List(I64) numbers = [1, 2, 3] updated = numbers.set(1, 99) ?? numbers echo!(Str.inspect(updated)) echo!(Str.inspect(numbers)) Ok({}) }
The outcome is identical and the effort is not. A Swift library type gets copy-on-write only if its author writes isKnownUniquelyReferenced by hand; in Roc the compiler applies the optimization to every value in the program, because immutability makes it always safe.
Top-level values are computed before the program starts
A top-level Roc definition is evaluated by the compiler. By the time the program runs, squared is the constant 100 baked into the binary.
let limit = 10 let squared = limit * limit // computed at startup print(squared)
limit : I64 limit = 10 squared : I64 squared = limit * limit main! = |_args| { echo!(squared.to_str()) Ok({}) }
A Swift global is lazily initialized on first access, with a swift_once check every time it is read, which is why globals are safe to use before main and why they cost slightly more than a constant. Roc has no such distinction — every top-level definition is a constant.
Gotchas for Swift Programmers
An untyped integer prints as a decimal
This is the first thing that will confuse you. An unconstrained number literal defaults to Dec, so a list that looks like integers prints as [1.0, 2.0, 3.0].
print([1, 2, 3]) print(1 + 2)
main! = |_args| { # No annotation: these literals become Dec, # and print with a decimal point. echo!(Str.inspect([1, 2, 3])) echo!(Str.inspect(1 + 2)) typed : List(I64) typed = [1, 2, 3] echo!(Str.inspect(typed)) Ok({}) }
The fix is an annotation or a suffix: typed : List(I64), or 42.I64 on the literal. Swift has the same machinery with a friendlier default — ExpressibleByIntegerLiteral resolves an untyped literal to Int — so the habit of asking which numeric type you have transfers directly.
A bare True is not a Bool
Bool is an ordinary tag union in Roc, and True and False written bare are just tags — not necessarily that union.
let ready = false print(!ready)
main! = |_args| { # Without the annotation, "False" is inferred as # a one-off structural tag rather than a Bool, # and ! would have nothing to negate. ready : Bool ready = Bool.False echo!(Str.inspect(!ready)) Ok({}) }
Annotating the binding, or writing Bool.True and Bool.False in full, pins it down. Note that ! is overloaded in Roc the way it is in Swift — negation as a prefix, and an effect marker as a name suffix — which reads oddly for about a day.
There is no working [i] on a list
Subscript syntax exists in the grammar and does not work, which is worse than not existing — the error it produces talks about type variables rather than about indexing.
let numbers = [10, 20, 30] print(numbers[0]) print(numbers.last!)
main! = |_args| { numbers : List(I64) numbers = [10, 20, 30] # numbers[0] parses in this build but does not # type-check into a usable value. echo!((numbers.get(0) ?? 0).to_str()) echo!((numbers.last() ?? 0).to_str()) Ok({}) }
Use get(index), and first() or last() for the ends. Roc's last() returns a Try where Swift's last returns an Optional, so the two are the same shape and the force-unwrap has no counterpart.
When you want a nominal type, use :=
Roc's : makes an alias and := makes a genuinely new type. Swift has no alias-versus-newtype distinction to learn — a struct is always nominal and typealias is always an alias.
struct UserID { let value: UInt64 } func greet(_ userID: UserID) -> String { "user #\(userID.value)" } print(greet(UserID(value: 42))) // greet(42) does not compile.
UserId := { value : U64 } greet : UserId -> Str greet = |user_id| "user #${user_id.value.to_str()}" main! = |_args| { user_id = UserId.{ value: 42 } echo!(greet(user_id)) # greet(42) does not compile. Ok({}) }
This is the one place a Swift programmer must choose deliberately, because the default (:) is the structural one. Both versions are zero-cost at run time, and both reject a bare number at the call site.
The standard library is still settling
Roc is pre-1.0 and its standard library is visibly incomplete. Methods Swift has had since 1.0 are simply absent, and which ones are absent changes between nightly builds.
import Foundation print("hello world".replacingOccurrences(of: " ", with: "_")) print("shout".uppercased())
main! = |_args| { # There is no Str.replace in this build, and no # case conversion — compose what exists: parts = "hello world".split_on(" ") echo!(Str.join_with(parts, "_")) echo!("shout") Ok({}) }
There is also no Codable, no regular expressions, no date and time handling, and no grapheme-aware string model. Swift's standard library is unusually well designed; this comparison is not a fair one and it is a real one.
The honest comparison
Of the eight languages Roc is pitched against, Swift makes the most similar bets: reference counting instead of a collector, value semantics with copy-on-write, sum types with exhaustive matching, and failure as a value rather than an exception.
// Swift: optionals, enums with associated values, // exhaustive switch, value semantics with // copy-on-write, ARC rather than a collector, and // a 1.0 in 2014. print("most of the same bets, a decade earlier")
main! = |_args| { # Roc: no optional to wrap, tags with no # declaration, open unions, effects in the type # system, and refcounting with no cycle # possible — so no weak, no unowned, no leak. echo!("the same bets, pushed one step further") Ok({}) }
So the honest summary is narrow, and it is memory-shaped. Roc removes the retain cycle rather than giving you tools to break it, removes the optional rather than making it safe, drops the declaration a union needs, and puts every effect in the type system. It gives up async/await, actors, protocol extensions, grapheme-aware strings and the entire Apple ecosystem to do it.

Thank you — anything else?