# Roc Style Notes Small cleanup rules for Roc code. ## Types - Add type annotations for top-level functions, including `main!` and small helpers. These are part of the reader-facing API of an example. - Keep type annotations when they document public API shape, non-obvious records/lists, custom tags, or numeric intent. - Omit type annotations for simple top-level constants when the value is an obvious literal, especially plain strings. - Use `_` in an annotation only when spelling out the inferred type would expose distracting internal machinery or make an example harder to read. - For example `main!` annotations, prefer a named error union when the error cases are part of the example. Use `_` when the row is just low-level plumbing from effects like stdin/stdout/file IO. - Prefer named records over tuples when the fields have meaning. For example, headers should be `{ name : Str, value : Str }`, not `(Str, Str)`. - Destructure records in function arguments when the fields are few and the meaning stays obvious. ```ruby Header := { name : Str, value : Str, }.{ to_str : Header -> Str to_str = |{ name, value }| "${name}: ${value}" } ``` ## Function Shape - Prefer receiver/static-dispatch style when it reads naturally, such as `items.map(...)`, `items.keep_if(...)`, `items.len()`, and `items.fold(...)`. - Avoid extra braces around a function body when the body is just one `match`, `if`, record update, or expression. - Keep short pure helpers compact, but expand nested `if`/`match` branches when the inline version hurts scanning. - Prefer builder chains over numbered intermediate values. - Prefer `with_headers([...])` when setting multiple headers at once. Use repeated `add_header(...)` when the step-by-step construction is the point of the example. ```ruby request = Request.from_method(POST) .with_uri("/widgets") .with_headers( [ { name: "Accept", value: "application/json" }, { name: "X-Trace-Id", value: "demo-123" }, ], ) ``` ## Static Dispatch Hooks - Treat well-known static-dispatch methods as opt-in hooks for language syntax and builtin APIs. They are not dynamic interfaces; each use still resolves to a concrete implementation. - Prefer well-known methods when they reduce consumer boilerplate without making behavior surprising. - Use `to_inspect : T -> Str` for debug, logging, and test output through `Str.inspect(value)`. - Use `to_str : T -> Str` only when the type has one clear user-facing string representation. - Prefer explicitly named renderers when several string meanings exist, such as `to_debug_str`, `to_markdown`, `to_plain_text`, or `to_html`. - Use `is_eq : T, T -> Bool` when `==` and `!=` should work naturally for a nominal type. - If a type defines custom `is_eq` and will be used in hash-based APIs, also define `to_hash : T, Hasher -> Hasher` consistently. Equal values must hash the same way. - Use ordering hooks (`is_lt`, `is_lte`, `is_gt`, `is_gte`) only when the type has one obvious ordering. - Use arithmetic hooks (`plus`, `minus`, `times`, `div_by`, `div_trunc_by`, `rem_by`) only when the operation is obvious and unsurprising for the type. - Use unary hooks (`negate`, `not`) only when unary `-` or `!` is the clearest spelling for the type's operation. - Use `from_numeral : Num.Numeral -> Try(T, [InvalidNumeral(Str)])` only when plain numeric literals should construct the type. - Use `from_quote : Str -> Try(T, [BadQuotedBytes(Str)])` only when quoted string literals should construct the type. - Use `from_interpolation : Str, Iter((item, Str)) -> T` only when interpolation should construct the type. - Prefer explicit constructors over literal hooks when the conversion is surprising, lossy, contextual, or one of several possible interpretations. - Use `iter : T -> Iter(item)` for collection-like wrappers so `for` loops and lazy pipelines work naturally. Collection authors usually implement `iter`; iterator values provide `next`. - Use `parser_for` and `encoder_for` for structured parse/encode behavior. Do not overload `to_str` as serialization. - When delegating well-known methods to shared helpers, prefer an explicit wrapper body. This keeps the public method surface readable and avoids relying on bare alias compiler behavior. ```ruby Markdown := [...].{ to_inspect : Markdown -> Str to_inspect = |node| inspect_markdown(node) is_eq : Markdown, Markdown -> Bool is_eq = |left, right| markdown_is_eq(left, right) to_debug_str : Markdown -> Str to_debug_str = |node| inspect_markdown(node) } ``` ```ruby Port := U16.{ from_numeral : Num.Numeral -> Try(Port, [InvalidNumeral(Str)]) from_numeral = |numeral| parse_port_numeral(numeral) } port : Port port = 8080 ``` ## Records And Builders - Prefer named record-builder composition for multi-signal values, such as `{ first: first_signal, last: last_signal }.Signal`, instead of adding `Signal.map3+`. - Keep record fields explicit when the names communicate domain meaning. - Let record types document parse targets and response bodies in examples. ## Error Handling - Use postfix `?` to propagate a `Try` in a function or `expect`. - Use infix `?` to map low-level errors into domain errors before propagating. - Use `??` only at a real boundary where a fallback value is appropriate. Avoid using it in parsing paths where it would hide invalid input. - Prefer a fallible internal function plus a small boundary wrapper for workflows that need a fallback response. - Do not ignore effect results with `_ = effect!()` or `match effect!() { _ => {} }`. Use postfix `?`, infix `?`, an explicit `match`, or `??` at a boundary. - Avoid manual `Ok(value) = result` unwrapping in example code. Prefer `?` or an explicit `match` when handling the error is meaningful. ```ruby main! : List(Str) => Try({}, _) main! = |_args| { data = Stdin.bytes!()? Stdout.write_bytes!(data)? Ok({}) } parse_widget_request : Request -> Try(CreateWidget, WidgetRequestError) parse_widget_request = |request| { body = Str.from_utf8(request.body()) ? |_| BadBodyUtf8 widget = Json.parse(body) ? BadBodyJson Ok(widget) } route : Request -> Response route = |request| route_result(request) ?? internal_error_response() ``` ## CLI Example Error Boundaries - Small CLI examples should usually let structured errors bubble to `main!`. The platform boundary can render unexpected failures with `Str.inspect`, so examples do not need to convert every failure into `Exit(1)` or a `Str`. - Prefer tags, with payloads when useful, over stringly errors. This keeps failures inspectable, composable, and easier to refine later. - Use infix `?` to name the operation that failed when the low-level error alone would lose useful context. - Use a render helper only at a true boundary where custom human-facing text is part of the example. Keep that rendering in one place instead of scattering one-off print-and-exit branches through the workflow. - For cleanup-sensitive workflows, capture the fallible result, perform cleanup, then rethrow with `?`. ```ruby main! : List(Str) => Try({}, _) main! = |_args| run!() run! : () => Try({}, _) run! = || { response = Http.send!(request) ? |err| SendFailed(err) decoded = Http.decode_json_response(response) ? |err| DecodeResponseFailed(err) Stdout.line!("received: ${Str.inspect(decoded)}")? Ok({}) } ``` ```ruby main! : List(Str) => Try({}, _) main! = |args| { Tty.enable_raw_mode!() result = game_loop!(args) Tty.disable_raw_mode!() result? Ok({}) } ``` ## Strings And Bytes - Use `Str.inspect` for debug output of byte lists, records, tags, and booleans instead of writing one-off `bytes_to_str` or `headers_to_str` helpers. - For nominal types, implement `to_inspect` when the default `Str.inspect` representation is not the useful debug representation. - Use `Str.from_utf8(...) ? ...` when invalid UTF-8 should be treated as an error. - Use `Str.from_utf8(...) ?? fallback` only for display or boundary fallbacks. - Use `Str.from_utf8_lossy` sparingly, when lossy display is explicitly the desired behavior. ## Tests - Prefer top-level `expect` tests for small packages and examples. - Put a short doc comment immediately before every top-level `expect`. Roc includes that text in failure output. - Use postfix `?` inside `expect` blocks for setup that should fail the test immediately. - Compare decoded values directly instead of comparing `Try` values on the happy path. - Keep simple tests as direct comparisons. - Avoid long chained `and` expressions when checking several related facts. - For multi-signal behavior, build a labeled `actual` report string and compare it to a labeled multiline `expected` string. - Use `Str.inspect(...)` in report strings for booleans, tags, records, and nested values. - Include negative parse tests for important parser behavior. - CI should run `roc test` for every example so top-level expects in examples are exercised. ```ruby ## Inline debug helpers expose inspect and equality behavior. expect { inline : Markdown.Inline inline = Strong([Text("Roc")]) actual = \\inline inspect: ${Str.inspect(inline)} \\inline debug: ${Markdown.inline_to_debug_str(inline)} \\inline eq same: ${Str.inspect(inline == Strong([Text("Roc")]))} \\inline eq different: ${Str.inspect(inline == Emphasis([Text("Roc")]))} expected = \\inline inspect: Strong([Text("Roc")]) \\inline debug: Strong([Text("Roc")]) \\inline eq same: True \\inline eq different: False actual == expected } ```