I read about optional record fields here https://github.com/rtfeldman/roc/blob/trunk/roc-for-elm-programmers.md#optional-record-fields but one thing I didn't understand is the motivation behind them. To me it seems like the record update approach Elm does for config works fine without the need for additional syntax.
so besides the inconvenience factor, one downside of the defaultConfig
alternative is that it means adding new config options has to be a breaking change, even if there's a safe default that could be provided in all cases
with optional fields, you can always add new optional fields to your public API and have it be fully backwards-compatible
(not just in practice, but also from the perspective of the package manager's semver inference)
Yea exactly it provides some nice ergonomics for using records as configs
Good point about avoiding major version changes, I hadn't considered that.
Writing rules in elm-review has made me quite apprehensive about adding conveniences to a language's syntax but I understand the advantages with having optional record fields now. Tradeoffs as always :smile:
Writing rules in elm-review has made me quite apprehensive about adding conveniences to a language's syntax
now I'm curious! What rules in particular? :big_smile:
I don't know of any rules that would be directly impacted by optional record fields (maybe the NoUnused rules would get significantly more complicated?) But having it means a more complicated AST, which means more situations that a rule might need to handle in order to be useful.
Here's an example that's a bit contrived because in practice it wouldn't be an issue but maybe can give an idea of possible problems:
At work I've created a rule that converts Element.paddingEach { left = 4, right = 4, top = 0, bottom = 0 }
into Element.xy 4 0
. With optional record fields, maybe elm-ui would be changed so that someone could write Element.paddingEach { left = 4, right = 4 }
. Now this rule would need to look up data from the elm-ui package to know what the defaults are. Again, this is contrived because I'm pretty safe in just assuming the default is 0.
More generally though, I don't know of any other language ecosystem that has something like elm-review*. I believe this is because Elm hits a sweet spot of lightweight syntax and pure functions that makes it easy for anyone to write their own rules or tools. For that reason I'm cautious about adding syntax features, it seems like it can quickly move a language out of the "easy to create tools" zone.
*There are static analysis tools and linters for other languages created by one or more experts but I don't know of any tools in other languages that make it easy for just about anyone to quickly create a useful rule.
gotcha, thanks! Roc's syntax is very similar to elm's overall, and there aren't any plans to make any significant expansions to it. I want it to stay small too!
(deleted)
Oh, I thought of some more questions:
{ fieldA = 5 } == {}
?a = { fieldA = 5 }
b = {}
c = a == b # Did the user forget to add fieldA? Or are they trying to test equality on two optional records?
optional fields are not fields with defaults
so { fieldA : 5 } == {}
is just a type error
rather you can check if the field is present in the type, and if not you could then provide the default. It's not baked in
it's a bit like polymorphism. In elm you cannot say
x : { a | y: Int }
x = { y = 5 }
you also cannot do this
f : { a | x: Int } -> Bool
f = \r -> r == {x= 5, y= "foo"}
Okay, I think I understand. Writing f : { a : Int, b ? Int} -> Int
is like writing this?
f : { a : Int }* -> Int
f = \record -> fHelper record |> ... #rest of the function
fHelper : { a : Int }e -> { a : Int, b : Int }e
I think you're on the right track. Both of these would fail the type checker
f : { a : Int, b ? Int} -> Int
f = \r -> { a : 42, b : 43 }
f : { a : Int, b ? Int} -> Int
f = \r -> { a : 42 }
But you can do
f : { a : Int, b ? Int}, { a : Int, b ? Int} -> Int
f = \r1, r2 -> r1 == r2
or, at least I think you should be able to do that. Not sure if that actually works today now that I look at it
or rather, I don't think the error message is great if you do f { a } { a, b }
Maybe I'm doing something silly, but I can't get a working example out of the tutorial section: Optional Record Fields. All variations on the following code:
main =
# dbg table { height: 100, width: 150, title: "a", description: "b" }
# dbg table {100, 150, "a", "b"}
# Stdout.line "\(table {100, 150})"
Stdout.line "the end"
table : { height : Num, width : Num, title ? Str, description ? Str } -> Str
table = \{
height,
width,
title? "oak",
description? "a wooden table"
}
-> "abc"
give the following compiler error (compiling hangs):
thread '<unnamed>' panicked at 'internal error: entered unreachable code: Any other pattern should have given a parse error', crates/compiler/can/src/pattern.rs:737:26
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.
Here is the stack backtrace:
stack backtrace:
0: rust_begin_unwind
at /rustc/90743e7298aca107ddaa0c202a4d3604e29bfeb6/library/std/src/panicking.rs:575:5
1: core::panicking::panic_fmt
at /rustc/90743e7298aca107ddaa0c202a4d3604e29bfeb6/library/core/src/panicking.rs:65:14
2: roc_can::pattern::canonicalize_pattern
3: roc_can::scope::Scope::inner_scope
4: roc_can::def::canonicalize_pending_body
5: roc_can::def::canonicalize_value_defs
6: roc_can::module::canonicalize_module_defs
7: roc_load_internal::file::run_task
8: core::ops::function::FnOnce::call_once{{vtable.shim}}
Perhaps this is just a design example and isn't supposed to work. But if possible I would like to have a more expert advice on this :smile:
Looks like a bug, can you log an issue for this?
Ok: # 5653
@Ivo Balbaert I don't know if it's exactly the same, because I don't have my laptop on me. I had a similar error. Putting all function arguments on one long line worked for me
table : { height : Num, width : Num, title ? Str, description ? Str } -> Str
table = \{ height, width, title? "oak", description? "a wooden table" } ->
"abc"
Like so. Not the prettiest, but it should work until the bug is fixed. At least if it was the same as mine
Thanks Kilian, that works! I'll mention your workaround in the issue, but leave it open until the parsing problem is fixed.
Last updated: Jul 06 2025 at 12:14 UTC