The static dispatch proposal has been generally accepted as a good move for Roc in providing a more discoverable and user-friendly syntax that requires less provision of context by devs when writing Roc code. Instead of writing out the types you're using:
countValidElements = \allElems ->
allElems
|> List.mapTry \elem -> validate elem
|> List.len
now you'd write:
countValidElements = allElems ->
allElems.mapTry(.validate()).len()
This is more concise, and pretty legible, but it doesn't tell you want data structure it's acting on. Maybe it's a Dict? It's probably a List because the arg's name starts with "all". This isn't a problem if we annotate the function type.
countValidElements : List a -> U64
countValidElements = allElems ->
allElems.mapTry(.validate()).len()
With the addition of the CLI command + LSP action for auto type annotations, it'll soon be very easy for devs to get annotations on these functions. Because it'll be so easy to annotate types for top-level defs, I propose we require them to be type annotated by adding warning for exposed top-level defs without a type annotation.
It's nice to be able to have "guerilla" private functions, and if a function is private, it's less likely to be important to people reading the code. I'm open to requiring them to be type annotated as well.
While you're writing a function, you may not have decided what type it should return, or may change your mind halfway through. I think this would over-constrain the dev.
We already have this for unexposed defs. Every top-level def that's not exposed will continuously have a warning on it until it's used by something or exposed from the module. You can fix said warning quickly by exposing the function before writing the body, and you can avoid the type annotation warning by adding the annotation before you write the body as well.
What about intentionally generic function?
Also, in many cases, the lsp will pick an overly generic type if it types the arguments for you
So the user experience may not be great
I say this as someone who types most type level functions in anything that isn't a quick script
You're right that the LSP will by default annotate the principle type for the function, which is even less sane for static dispatch.
Which puts pressure on the user to write a specific type.
But that feels more like a problem with what we render inferred types as rather than whether type annotations are good to pressure users to include.
Yeah, I'm not sure here.
I'm having trouble making a type annotation for the above example... I think the type would be:
countValidElements : a -> e
where
a.mapTry(b -> c) -> d,
b.validate() -> c,
d.len() -> e
countValidElements = allElems ->
allElems.mapTry(.validate()).len()
I'm not sure if this is the equivalent of a good clippy error that should always be on or one of the clippy warnings that it regularly annoying and you would rather turn off.
Especially given that scripting is a use case for roc
Yeah, that type looks right I think
Unfortunately, we don't have clippy by design. I know your point is "would people just ignore this by default?" but we've already committed to "we know at least a little bit more than you about what's good for you"
Oh, I'm specifically thinking about the too many args for a function clippy warning. Whenever it is hit we just turn it off cause it is not useful.
In this case, typing when we know the exact static type feels fine. But if it automatically generates signatures like the above, it is just an annoyance. I would rather see no signature than the signature above.
So I guess I would be fine with the formatter automatically adding types to all tope levels if there is no where clause required.
When a where clause is involved I don't think it will pick a useful signature.
This all made harder by static dispatch not supporting higher kinded types which limits how nice the signature can be
Brendan Hansknecht said:
So I guess I would be fine with the formatter automatically adding types to all tope levels if there is no where clause required.
I don't think we can automatically add type signatures via formatter because people save code while writing it. We could have the formatter overwrite incorrect type signatures with the latest one to fix this, but then people can't say "here's my type, Roc, I'm gonna keep writing until you tell me we agree on that being the right type"
I personally like annotating everything.
But I think this specific proposal is in the wait it out and see space for me.
I just don't think we have seen enough roc (certainly in static dispatch format) to know if this is an issue yet, or to really inform the discussion.
So I think it's good to note as a concern, but I don't think we should pre-emptively make it a warning or enything.
I would be okay with tabling this until later if there's a metaphorical table to put this on. Do we have a "for later" doc? Maybe https://www.roc-lang.org/plans?
My suggestion was more, if there is no type annotations for a top level and the type annotion doesn't need a where clause, add it.
Wouldnt do anything for a wrong type annotation
Though I guess formatting is before type checking....so :shrug:
Then we get to the scenario of prematurely annotating things. I expect this is a pretty common case:
countValidElements = elems -> List.mapTry(elems, .validate())
If I save this now, it gets annotated
countValidElements : List a -> List b where ...
countValidElements = elems -> List.mapTry(elems, .validate())
But I didn't finish adding the .count(), which would've made it
countValidElements : List a -> U64 where ...
countValidElements = elems -> List.mapTry(elems, .validate()).count()
Ah
Yeah
Hence "warnings are good because they aren't wrong, just annoying"
But easily "fixable" with either the auto-added annotation which is verbose but perfectly correct, or manually (which most static languages make you do manually anyway)
Brendan Hansknecht said:
Though I guess formatting is before type checking....so :shrug:
This is fixable, but not trivially. We lose a lot of info by the time canonicalization strips that info pre-typechecking, so we'd either need to carry that around (bad for perf), or be able to tie it back to the roc_parse::Ast (lots of detritus)
The auto annotation could just be something really dumb like _.
my_fn : _
my_fn = \_ -> ...
That option works if we also have a code action along the lines of "realize inferred type" that gives the type that _ is inferring, which we should have anyway
But it would defeat the point of this, which is to fix the bottom of the static dispatch boat
No real impact for people who dont want to annotate. But still encourages people towards annotate top-level exposed things.
I could imagine a cultural norm forming around that.
AKA static dispatch is more terse, but is confusing unless you know the type of the initial state in a pipeline, e.g. the function's arg's types
My suggestion I hope still enables frictionless dev wrokflow, but that trends things towards our desired outcome.
I would consider my_fn : _ harmful to readability, because devs will be satisfied with a zero info solution. At least no type def at all makes them aware that they don't know the type
Yeah, but that _ looks out of place or ugly or embarrassing for anything I'm publishing or sharing.
Imagine the code review...
You're one of the good ones
I think disciplined devs will write good type annotations because they care about communicating with the reader.
I wonder if allowing no types, but making it easy to add types would be enough. Make it so trivial to add types that people will just do it.
If there's no warning, I think lots of people won't do it. Tons of people prefer JS to TS, even if they didn't have to write the code. Some people don't like type annotations
I think that's fine. We are still raising the floor quite a lot
There are always static types in the final app
I dunno, this feels like a solution in search of a problem :sweat_smile:
like in Elm there's a culture of annotating things and people annotate things and it's fine :shrug:
if people want to write quick scripts where they don't annotate things, that also seems fine
I'm fine without it. I'm just trying to encourage lazy people to make hard-to-read code more readable
And the main problem I'm seeing is that static dispatch without arg annotations can get very hairy
well, but as we've seen with advent of code (for example), sometimes you're being lazy because you're writing code you don't intend to maintain
So guiding people towards at least annotating arg types helps a lot IMO
like not everything Roc will be used for will be a production code base worked on by multiple people, and while I do want to create pits of success, I don't know that we should go out of our way to disallow doing things in a quick-and-dirty way either
You can also solve this by requiring the first thing in a static-dispatch method chain to have a qualified module
(and I don't think "it's a warning, just ignore it" is reasonable)
I'm actually not considering "just ignore the warning" because Roc doesn't either.
Since they block builds
Couldn’t a roc version of clippy do this?
For your enterprise use cases :joy:
If we had a clippy, I'd much rather this be there
But we don't have one by design, I think
Maybe a —nits flag for roc check with verbose diagnostics?
at some point I'd like to have a builtin linting feature actually, but mostly for team-specific things - e.g. "we are moving away from A and towards B, so these current uses of A are allowed but nobody is allowed to introduce new A to this code base"
If that's planned, then that would make me happy. I still agree that letting people configure all of their lints or suppress them inline isn't great
But having "baseline good stuff" and "pedantic" as two lint levels is probably all we need
famous last words, maybe
Then you end up with one pendantic thing you want to enforce and one you don't, so you still end up falling into the pit of wanting per-rule configuration.
Last updated: Jun 16 2026 at 16:19 UTC