1# Roc Language Reference
2
3## REPL
4
5Run with `roc repl`. Online: [roc-lang.org/repl](https://www.roc-lang.org/repl)
6
7```roc
8greeting = "Hi"
9audience = "World"
10```
11
12Arithmetic: `1 + 2 * (3 - 4)` follows order of operations.
13
14## Calling Functions
15
16```roc
17Str.concat("Hi ", "there.") # "Hi there."
18```
19
20`Str` is a module, `concat` is a function in that module.
21
22### String Interpolation
23
24```roc
25"${greeting} there, ${audience}."
26"Two plus three is: ${Num.to_str(2 + 3)}"
27```
28
29## Building an Application
30
31```roc
32app [main!] { pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br" }
33
34import pf.Stdout
35
36main! = |_args|
37 Stdout.line!("Hi there, from inside a Roc app. 🎉")
38```
39
40Run: `roc main.roc`
41
42### Defs
43
44```roc
45birds = 3
46iguanas = 2
47total = Num.to_str(birds + iguanas)
48
49main! = |_args|
50 Stdout.line!("There are ${total} animals.")
51```
52
53Defs are constant—cannot be reassigned.
54
55### Defining Functions
56
57```roc
58add_and_stringify = |num1, num2|
59 Num.to_str(num1 + num2)
60```
61
62### if-then-else
63
64```roc
65add_and_stringify = |num1, num2|
66 sum = num1 + num2
67 if sum == 0 then
68 ""
69 else if sum < 0 then
70 "negative"
71 else
72 Num.to_str(sum)
73```
74
75Every `if` must have both `then` and `else`.
76
77### Comments
78
79```roc
80# Single line comment
81## Doc comment (included in generated docs)
82## x = 2 # Code block in doc (5 spaces after ##)
83```
84
85### Records
86
87```roc
88add_and_stringify = |counts|
89 Num.to_str(counts.birds + counts.iguanas)
90
91total = add_and_stringify({ birds: 5, iguanas: 7 })
92```
93
94Functions accept records with extra fields:
95
96```roc
97total_with_note = add_and_stringify({ birds: 4, iguanas: 3, note: "Whee!" })
98```
99
100### Record Shorthands
101
102```roc
103return_foo = .foo # Same as |record| record.foo
104{ x: x, y: y } # Same as { x, y }
105```
106
107### Record Destructuring
108
109```roc
110add_and_stringify = |{ birds, iguanas }|
111 Num.to_str(birds + iguanas)
112
113add_and_stringify = |{ birds, iguanas: lizards }| # Rename field
114 Num.to_str(birds + lizards)
115
116{ x, y } = { x: 5, y: 10 } # Destructure in defs
117```
118
119### Record Update
120
121```roc
122original = { birds: 5, zebras: 2, iguanas: 7, goats: 1 }
123from_original = { original & birds: 4, iguanas: 3 }
124```
125
126`&` cannot introduce new fields or change types.
127
128### Debugging with dbg
129
130```roc
131pluralize = |singular, plural, count|
132 dbg count # Prints: [pluralize.roc 6:8] 5
133 if count == 1 then singular else plural
134
135dbg Str.concat(singular, plural) # Any expression
136inc = |n| 1 + dbg n # As function in expression
137```
138
139### Tuples
140
141```roc
142tuple = ("hello", 42, ["list"])
143first = tuple.0 # "hello"
144second = tuple.1 # 42
145
146(first, second, third) = ("hello", 42, ["list"]) # Destructuring
147```
148
149## Pattern Matching
150
151### Tags
152
153```roc
154stoplight_color =
155 if something > 0 then Red
156 else if something == 0 then Yellow
157 else Green
158```
159
160Tags are capitalized literals (like numbers/strings, no definition needed).
161
162### when/is
163
164```roc
165stoplight_str =
166 when stoplight_color is
167 Red -> "red"
168 Green -> "green"
169 Yellow -> "yellow"
170```
171
172Catch-all with `_`:
173
174```roc
175when stoplight_color is
176 Red -> "red"
177 _ -> "not red"
178```
179
180Multiple tags with `|`:
181
182```roc
183when stoplight_color is
184 Red -> "red"
185 Green | Yellow -> "not red"
186```
187
188Guards with `if`:
189
190```roc
191when stoplight_color is
192 Red -> "red"
193 Green | Yellow if contrast > 75 -> "high contrast"
194 Green | Yellow -> "not red"
195```
196
197### Tags with Payloads
198
199```roc
200stoplight_color =
201 if something > 100 then Red
202 else if something > 0 then Yellow
203 else Custom("some other color")
204
205stoplight_str =
206 when stoplight_color is
207 Red -> "red"
208 Green | Yellow -> "not red"
209 Custom(description) -> description
210```
211
212Multi-value payloads: `Custom(40, 60, 80)` destructured as `Custom(r, g, b) ->`
213
214### Booleans
215
216`Bool.true` and `Bool.false` (not keywords). Prefer tags for data modeling:
217
218```roc
219{ name: "Richard", role: Admin } # Better than is_admin: Bool.true
220```
221
222## Lists
223
224```roc
225names = ["Sam", "Lee", "Ari"]
226List.append(names, "Jess") # Returns new list (immutable)
227```
228
229### List.map
230
231```roc
232List.map([1, 2, 3], |num| num * 2) # [2, 4, 6]
233List.map([1, 2, 3], Num.is_odd) # [Bool.true, Bool.false, Bool.true]
234```
235
236All list elements must share a type. Use tags for mixed types:
237
238```roc
239List.map([StrElem "A", NumElem 1], |elem|
240 when elem is
241 NumElem(num) -> Num.is_negative(num)
242 StrElem(str) -> Str.starts_with(str, "A")
243)
244```
245
246### Using Tags as Functions
247
248```roc
249List.map(["a", "b", "c"], Foo) # Same as |str| Foo(str)
250```
251
252### List.any / List.all
253
254```roc
255List.any([1, 2, 3], Num.is_odd) # Bool.true
256List.all([1, 2, 3], Num.is_positive) # Bool.true
257```
258
259### Removing Elements
260
261```roc
262List.drop_at(["Sam", "Lee", "Ari"], 1) # ["Sam", "Ari"]
263List.keep_if([1, 2, 3, 4, 5], Num.is_even) # [2, 4]
264List.drop_if([1, 2, 3, 4, 5], Num.is_even) # [1, 3, 5]
265```
266
267### Getting Elements
268
269```roc
270List.get(["a", "b", "c"], 1) # Ok "b"
271List.get(["a", "b", "c"], 100) # Err(OutOfBounds)
272List.first(list) # Err(ListWasEmpty) if empty
273List.last(list) # Err(ListWasEmpty) if empty
274```
275
276## Error Handling
277
278```roc
279Result.with_default(List.get(["a", "b", "c"], 100), "") # ""
280Result.isOk(List.get(["a", "b", "c"], 1)) # Bool.true
281```
282
283### ? Postfix Operator
284
285Unwraps `Ok`, returns early on `Err`:
286
287```roc
288get_letter : Str -> Result Str [OutOfBounds, InvalidNumStr]
289get_letter = |index_str|
290 index = Str.to_u64(index_str)?
291 List.get(["a", "b", "c", "d"], index)
292```
293
294### ?? Infix Operator (Default on Error)
295
296```roc
297List.get(["a", "b", "c"], 100) ?? "" # ""
298```
299
300### List.walk
301
302```roc
303List.walk([1, 2, 3, 4, 5], { evens: [], odds: [] }, |state, elem|
304 if Num.is_even(elem) then
305 { state & evens: List.append(state.evens, elem) }
306 else
307 { state & odds: List.append(state.odds, elem) }
308)
309# { evens: [2, 4], odds: [1, 3, 5] }
310```
311
312Arguments: list, initial state, function `(state, elem) -> state`
313
314### Pattern Matching on Lists
315
316```roc
317when my_list is
318 [] -> 0 # empty
319 [Foo, ..] -> 1 # starts with Foo
320 [_, ..] -> 2 # at least one element
321 [Foo, Bar, Baz] -> 3 # exactly 3 elements
322 [Foo, a, ..] -> 4 # second element named `a`
323 [Ok a, ..] -> 5 # first is Ok with payload
324 [.., Foo] -> 6 # ends with Foo
325 [A, B, .., C, D] -> 7 # specific start and end
326 [head, .. as tail] -> 8 # head and rest
327```
328
329Only one `..` (rest pattern) per pattern.
330
331### Pipe Operator
332
333```roc
334["a", "b", "c"] |> List.get(1) |> Result.with_default("")
335# Same as: Result.with_default(List.get(["a", "b", "c"], 1), "")
336```
337
338## Types
339
340### Type Annotations
341
342```roc
343full_name : Str, Str -> Str
344full_name = |first_name, last_name|
345 "${first_name} ${last_name}"
346
347first_name : Str
348first_name = "Amy"
349
350amy : { first_name : Str, last_name : Str }
351amy = { first_name: "Amy", last_name: "Lee" }
352```
353
354### Type Aliases
355
356```roc
357Musician : { first_name : Str, last_name : Str }
358
359amy : Musician
360amy = { first_name: "Amy", last_name: "Lee" }
361```
362
363### Type Parameters
364
365```roc
366names : List Str
367names = ["Amy", "Simone", "Tarja"]
368```
369
370### Wildcard Type (*)
371
372```roc
373is_empty : List * -> Bool # Works on any list type
374```
375
376Empty list `[]` has type `List *`.
377
378### Type Variables
379
380```roc
381reverse : List elem -> List elem # Same element type in and out
382```
383
384Lowercase names (`elem`, `a`, `value`) are type variables.
385
386### Tag Union Types
387
388```roc
389color_from_str : Str -> [Red, Green, Yellow]
390color_from_str = |string|
391 when string is
392 "red" -> Red
393 "green" -> Green
394 _ -> Yellow
395```
396
397Tag unions accumulate:
398
399```roc
400|str|
401 if Str.is_empty(str) then Ok "it was empty"
402 else Err ["it was not empty"]
403# Type: Str -> [Ok Str, Err (List Str)]
404```
405
406`Result ok err` is alias for `[Ok ok, Err err]`.
407
408### Opaque Types
409
410```roc
411Username := Str
412
413from_str : Str -> Username
414from_str = |str| @Username(str)
415
416to_str : Username -> Str
417to_str = |@Username(str)| str
418```
419
420`@Username` only usable in defining module.
421
422### Integers
423
424| Type | Range |
425|------|-------|
426| U8 | 0 to 255 |
427| I8 | -128 to 127 |
428| U16 | 0 to 65,535 |
429| I16 | -32,768 to 32,767 |
430| U32 | 0 to 4,294,967,295 |
431| I32 | -2,147,483,648 to 2,147,483,647 |
432| U64 | 0 to 18+ quintillion |
433| I64 | -9+ to 9+ quintillion |
434| U128 | 0 to 340+ undecillion |
435| I128 | ±170+ undecillion |
436
437Overflow crashes the program.
438
439### Fractions
440
441| Type | Description |
442|------|-------------|
443| F32 | 32-bit floating-point |
444| F64 | 64-bit floating-point |
445| Dec | 128-bit decimal fixed-point (18 decimal places) |
446
447`Dec` is best for currency/base-10. `F32`/`F64` have precision loss with decimals.
448
449### Num, Int, Frac
450
451```roc
452abs : Num a -> Num a # Any number
453bitwise_xor : Int a, Int a -> Int a # Integers only
454cos : Frac a -> Frac a # Fractions only
455```
456
457Number literals have type `Num *` (or `Frac *` with decimal point).
458
459### Number Literal Suffixes
460
461```roc
4621u8 # U8
4635dec # Dec
4640xfe # Hex (254)
4650b1000 # Binary (8)
466```
467
468Suffixes: `u8`, `i8`, `u16`, `i16`, `u32`, `i32`, `u64`, `i64`, `u128`, `i128`, `f32`, `f64`, `dec`
469
470### Default-Value Record Fields
471
472```roc
473table : { height : U64, width : U64, title ?? Str, description ?? Str } -> Table
474table = |{ height, width, title ?? "oak", description ?? "a wooden table" }| ...
475```
476
477`??` in types marks optional fields. Only accessible via destructuring.
478
479## Crashing
480
481Crashes: integer overflow, out of memory, `crash` keyword.
482
483```roc
484when Str.from_utf8(bytes) is
485 Ok(str) -> str
486 Err(_) -> crash "This should never happen!"
487
488# TODO marker
489crash "TODO handle the x <= y case"
490```
491
492Not for error handling—use `Result` instead.
493
494## Testing
495
496```roc
497pluralize = |singular, plural, count|
498 if count == 1 then "${Num.to_str(count)} ${singular}"
499 else "${Num.to_str(count)} ${plural}"
500
501expect pluralize("cactus", "cacti", 1) == "1 cactus"
502expect pluralize("cactus", "cacti", 2) == "2 cacti"
503```
504
505Run: `roc test`
506
507### Inline Expects
508
509```roc
510pluralize = |singular, plural, count|
511 if count == 1 then "${Num.to_str(count)} ${singular}"
512 else
513 expect count > 0
514 "${Num.to_str(count)} ${plural}"
515```
516
517- `roc build`: discards all expects
518- `roc dev`: runs inline expects during execution
519- `roc test`: runs top-level expects and triggered inline expects
520
521## Modules
522
523Types: `app` (application), `module`, `package`, `platform`, `hosted`
524
525### Builtin Modules (auto-imported)
526
527Str, Num, Bool, Result, List, Dict, Set, Decode, Encode, Hash, Box, Inspect
528
529### App Module Header
530
531```roc
532app [main!] { pf: platform "https://..." }
533
534import pf.Stdout
535import AdditionalModule
536import uuid.Generate as Uuid
537import pf.Stdout exposing [line!]
538```
539
540### Importing Files
541
542```roc
543import "some-file" as some_str : Str
544import "some-file" as some_bytes : List U8
545```
546
547## Effectful Functions
548
549Pure functions: `->`, effectful: `=>`
550
551```roc
552with_extension : Str -> Str # Pure
553read_file! : Str => Str # Effectful
554```
555
556Effectful functions can call pure or effectful. Pure can only call pure.
557`!` suffix is naming convention (compiler-enforced).
558
559```roc
560Stdout.line! : Str => Result {} [StdoutErr IOErr]
561Stdin.line! : {} => Result Str [EndOfFile, StdinErr IOErr]
562```
563
564### Reading Input
565
566```roc
567main! = |_args|
568 Stdout.line!("Type something:")?
569 input = Stdin.line!({})?
570 Stdout.line!("You entered: ${input}")
571```
572
573### Handling Failure
574
575```roc
576main! : List Arg => Result {} [Exit I32 Str]
577main! = |_args|
578 Result.map_err(my_function!({}), |err|
579 when err is
580 StdoutErr(_) -> Exit(1i32, "Error writing to stdout.")
581 StdinErr(_) -> Exit(2i32, "Error writing to stdin.")
582 EndOfFile -> Exit(3i32, "End of file reached.")
583 )
584```
585
586### Tagging Errors
587
588```roc
589main! = |_args|
590 Stdout.line!("Prompt") ? UnableToPrintPrompt
591 input = Stdin.line!({}) ? UnableToReadInput
592 Stdout.line!("You entered: ${input}") ? UnableToPrintInput
593 Ok({})
594```
595
596### Inspect.to_str
597
598```roc
599Inspect.to_str(any_value) # String representation for debugging
600```
601
602### Early return
603
604```roc
605if this_is_a_bad_time then
606 return "Error message"
607else
608 continue_normally
609```
610
611## Advanced Concepts
612
613### Open vs Closed Records
614
615```roc
616# Closed: exact fields only
617full_name : { first_name : Str, last_name : Str } -> Str
618
619# Open: at least these fields (note the *)
620full_name : { first_name : Str, last_name : Str }* -> Str
621
622# Constrained: same type in and out
623add_https : { url : Str }a -> { url : Str }a
624```
625
626Inference:
627- Creating record → closed
628- Using as argument/destructuring → open
629- Record update → constrained
630
631### Type Alias with Variable
632
633```roc
634User a : { email : Str, first_name : Str, last_name : Str }a
635
636is_valid : User * -> Bool # Open
637user_from_email : Str -> User {} # Closed
638capitalize : User a -> User a # Constrained
639```
640
641### Open vs Closed Tag Unions
642
643```roc
644# Open: might have unknown tags (needs _ -> branch)
645example : [Foo Str, Bar Bool]* -> Bool
646example = |tag|
647 when tag is
648 Foo(str) -> Str.is_empty(str)
649 Bar(bool) -> bool
650 _ -> Bool.false
651
652# Closed: exactly these tags
653example : [Foo Str, Bar Bool] -> Bool
654example = |tag|
655 when tag is
656 Foo(str) -> Str.is_empty(str)
657 Bar(bool) -> bool
658```
659
660New tags are inferred as open unions and can accumulate through conditionals.
661
662### Constrained Tag Unions
663
664```roc
665example : [Foo Str, Bar Bool]a -> [Foo Str, Bar Bool]a
666example = |tag|
667 when tag is
668 Foo(str) -> Bar(Str.is_empty(str))
669 Bar(bool) -> Bar(Bool.false)
670 other -> other
671```
672
673### Record Builder
674
675```roc
676user_tab_matcher =
677 { combine_matchers <-
678 _: exact_segment("users"), # Ignored field
679 user_id: u64_segment,
680 tab: any_segment,
681 }
682```
683
684Desugars to nested `combine_matchers` calls.
685
686### Reserved Keywords
687
688`as`, `crash`, `dbg`, `else`, `expect`, `expect-fx`, `if`, `import`, `is`, `return`, `then`, `try`, `when`
689
690### Operator Desugaring
691
692| Operator | Desugars To |
693|----------|-------------|
694| `a + b` | `Num.add(a, b)` |
695| `a - b` | `Num.sub(a, b)` |
696| `a * b` | `Num.mul(a, b)` |
697| `a / b` | `Num.div(a, b)` |
698| `a // b` | `Num.div_trunc(a, b)` |
699| `a ^ b` | `Num.pow(a, b)` |
700| `a % b` | `Num.rem(a, b)` |
701| `-a` | `Num.neg(a)` |
702| `a == b` | `Bool.is_eq(a, b)` |
703| `a != b` | `Bool.is_not_eq(a, b)` |
704| `a && b` | `Bool.and(a, b)` |
705| `a \|\| b` | `Bool.or(a, b)` |
706| `!a` | `Bool.not(a)` |
707| `a \|> f` | `f(a)` |