llms.md

  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)` |