Reading the Roc-for-Elm tutorial. So far most things are reasonable and/or improvements over Elm.
The first weird thing I found is the syntax for open records which is just... Weird
{ name : Str, email : Str }a -> { name : Str, email : Str }a
especially because I'd feel like writing it as
{ name : Str, email : Str } a -> { name : Str, email : Str } a
which feels... even weirder :thinking:
Qualified-but-not-dotted record update is :chefs-kiss:
...and the table function will fill in its default values for
x
andy
. There is no need to use adefaultConfig
record.
Except it has default for title
and description
. Eh. Guess a PR is incoming :laughing:
https://github.com/roc-lang/roc/pull/5184
Optional record fields are just Comfortable
Destructuring is the only way to implement a record with optional fields. (For example, if you write the expression
config.title
andtitle
is an optional field, you'll get a compile error.)
I get it but also... it could just be a Maybe
? But then you'd bless a specific type which is suboptimal I guess? :thinking:
when/is
instead ofcase/of
Fair enough
pipe for multiple patterns
YES thank you
pattern guards
Yes
can use
if
guards to match against constants
YESSSSS
So tempted to rewrite nuPlot in Roc now :laughter_tears:
and Bool is an opaque type. The values Bool.true and Bool.false work like True and False do in Elm.
weeeeird. Is this to avoid when/is
on booleans? Guess one would when/is/if
then :thinking:
Having to qualify booleans like Bool.true
and Bool.false
feels weeeeeird for such a basic thing. Is the idea to push towards custom types harder?
Task
type args are flipped
...mmmmmmmmmmh. I get it. But also. Mhhhhhhh
combined error tags
AMAZING
open tag unions syntax
see my comment about re: open record syntax. weeeeeeeeeeeeeeeeeeeird
You don't need to declare tag unions before using them. Instead, you can just write a tag (essentially a variant) anywhere you like, and Roc will infer the type of the union it goes in.
Thank you
> y = Foo "hi" Bar
Foo "hi" 5 : [Foo Str [Bar]*]*
This is another typo but I'm not sure what the intent was
Similarly to how if you put { name = "" } into elm repl, it will infer a type of { a | name : String } - that is, an open record with an unbound type variable and name : Str field
It doesn't tho. It only infers that open record if you hello {name} = "Hello " ++ name
For optional record fields:
I get it but also... it could just be a Maybe? But then you'd bless a specific type which is suboptimal I guess?
This is because optional record fields aren't really optional. They really should be called record fields with default values for a specific function. This is all known and dealt with at compile time. Depending on types, we generate specific functions with those values set as need. If you had a record field that was truly optional at runtime, you would use a Result
or Maybe
like value to hold the data.
Makes sense. I learned from Elm that it's fine to restrict features to nudge users towards better code
Opaque types
:shrug: ok
Also, for bool, the context is here: #ideas > opaque bools
Doc at the top here: https://roc.zulipchat.com/#narrow/stream/304641-ideas/topic/opaque.20bools/near/286971346
Yupp, I'm a convert. Love it
app vs interface modules
Mmmmh :thinking: wouldn't this prevent multiple entrypoints? Which is a nice thing sometimes
@Brendan Hansknecht I appreciate your responses, thanks for taking the time :purple_heart:
All imports and exports in Roc are enumerated explicitly; there is no .. syntax.
YES
(Later on, we'll talk about how opaque types work in Roc.)
_technically_ you already did
Elm does permit overriding open imports - e.g. if you have import Foo exposing (bar), or import Foo exposing (..), you can still define bar = ... in the module. Roc treats this as shadowing and does not allow it.
Good
Roc has no << or >> operators, and there are no functions in the standard library for general-purpose pointfree function composition.
:clap:
The only "good" use of <<
is List.filter (not << String.isEmpty)
Which. Like. List.filter String.Extra.isNotEmpty
Separately, you don't need parens or an operator to pass when or if as arguments. Here's another example:
Nize
list =
num <- List.map numbers
num + 1
oooooooooh, this is NICE. At first I was like "oh, ok, monadish syntax" but no, this is way better
incrementedNumbers =
num <-
[1, 2, 3]
|> List.reverse
|> List.map
num + 1
ooooh, it combines So Nicely
Oh, _that's_ why you Task.await
! It's just an andThen
mean to be used in backpassing style!
Dec
:clap:
If you encounter integer or Dec overflow in Roc, by default you get a runtime exception.
Oh. Oh no. Oh PLEASE no. Nope. Noooope. Like. I get it. But. Ugh
No Char. This is by design.
Good
dbg
auto-yeeted in prod
Good
crash
keyword
mmmmmmmh :thinking: I like that it's uncatchable, but I would also be wary of a library just randomly crashing on me?
Leonardo Taglialegne said:
If you encounter integer or Dec overflow in Roc, by default you get a runtime exception.
Oh. Oh no. Oh PLEASE no. Nope. Noooope. Like. I get it. But. Ugh
For reference, this was kinda a fitted compromise that matches the other numeric types. We tried enforcing more checked arithmetic on numeric types, but fundamentally it adds too much friction to the language. So Dec behaves the same as all the other regular number types. When you do a + b
, whether it is a U64
, I8
, or Dec
, it may overflow (which will run time panic). You, of course, can opt into checked arithmetic that will return a result.
Leonardo Taglialegne said:
crash
keywordmmmmmmmh :thinking: I like that it's uncatchable, but I would also be wary of a library just randomly crashing on me?
There was a lot of debate around this and it may change at some point, but fundamentally crash can be useful in libraries. For example, Dict
is written in Roc. It uses crash for states that should be unreachable. Without crash, there is not really a clear thing to do if somehow you reached a state that should be unreachable and would definitely otherwise be a bug. The general thought is that it will hopefully turn out like panic
in rust libraries. Most rust libraries will never panic, or if they do it is an unrecoverable library bug that should be filed upstream.
Interesting, for some reason I thought crash
only worked in app
modules.
Here is Richard's proposal and zulip thread discussion for anyone who is interested in reading more about crash
design.
Interesting, for some reason I thought
crash
only worked inapp
modules.
Just double checked. I was able to put a crash directly in an interface in a platform.
Yeah, I believe you. It is definitely in Dict
. I guess I assumed somewhere along the way. Good to know. I think it's a good trade-off.
Anyway. Considering that you're only a novice once I wanted to share my impressions :)
I will now probably try and play with roc a bit
Btw, thanks to all the people who worked on flake.nix, I was able to install roc by just adding it to home-manager like this: https://github.com/miniBill/dotfiles/commit/c0f1775048011f9e0580d842604870109be12a3f
7 messages were moved from this topic to #ideas > open record syntax by Richard Feldman.
The List module does not expose `length`:
I'd write "The List module does not expose a value or function named length
".
The current verbiage sounds like "the function exist but is not exposed".
» -(List.len ['a'])
18446744073709551615 : Nat
... I know why it happens but it's still surprising
» -(List.len ['a']) + 3.0
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Erroneous', crates/compiler/mono/src/ir.rs:10433:10
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Whoops :D
Leonardo Taglialegne said:
» -(List.len ['a']) 18446744073709551615 : Nat
... I know why it happens but it's still surprising
oh this is actually a bug! I've opened https://github.com/roc-lang/roc/issues/5197 for it
Leonardo Taglialegne said:
» -(List.len ['a']) + 3.0 thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Erroneous', crates/compiler/mono/src/ir.rs:10433:10 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
This is also a bug! Would you mind opening an issue for it?
Richard Feldman said:
oh this is actually a bug! I've opened https://github.com/roc-lang/roc/issues/5197 for it
Would it be possible to instead forbid using negate
on Nat
?
Richard Feldman said:
This is also a bug! Would you mind opening an issue for it?
Minimized and posted https://github.com/roc-lang/roc/issues/5199
Leonardo Taglialegne said:
Richard Feldman said:
oh this is actually a bug! I've opened https://github.com/roc-lang/roc/issues/5197 for it
Would it be possible to instead forbid using
negate
onNat
?
I considered it a long time ago, but I concluded it wouldn't be worth it because it would introduce an entire type-level distinction between signed and unsigned numbers basically just to prevent calling a function unnecessarily :big_smile:
(also there would be a nonzero cost to compile times to introducing that distinction)
I could see an argument for having negation of an unsigned integer always crash, even if you give it 0
and just say "you tried to negate an unsigned number, and unsigned numbers can't be negative"
that would mean that if you ever called Num.neg
on an unsigned number, it wouldn't give you a compile-time error, but in practice you'd always find out about it right away if you ever tried it
you wouldn't even have the "whoops it was 0 in my test cases, so I didn't notice" edge case
actually that seems worth trying and seeing if it causes problems; I've updated the issue
Richard Feldman said:
I considered it a long time ago, but I concluded it wouldn't be worth it because it would introduce an entire type-level distinction between signed and unsigned numbers basically just to prevent calling a function unnecessarily :big_smile:
For builtin operations, Roc would not need to introduce a type-level distinction - the compiler already has support we added for checking, for example, does this integer fit in a U8? So, we could apply the same analyses for catching negations of unsigned integers, without having to change the type-level syntax or integer types a Roc programmer needs to know.
The same kind of thing could be used to catch checks like myU8 < 0u8
which will never succeed.
hm, what about something like this?
negatesArgInSomeCodePath : Num a -> Num a
x = negatesArgInSomeCodePath someSignedInt
y = negatesArgInSomeCodePath someUnsignedInt
in that case, would it give an error for y
? if so, what would the error be? :thinking:
yeah, that might not detectable at compile time - but something like
f : U8 -> U8
f = \x ->
...
g = -x * -x
...
g
the compiler could provide today with no language changes
ahh sure, that's a good point!
so just like "if we know for sure it's gonna be a problem, we can tell you; otherwise, you might get a runtime error"
Last updated: Jul 06 2025 at 12:14 UTC