Switching gears then: Is there perhaps a syntax we could use to remove the redundancy of naming the function twice?
As a strawman, what about just removing the name? e.g.
: Str, Str -> Str
foo = |a, b| Str.join ["Hello, ", a, " and ", b]
I'd say that's worth a different thread :big_smile:
3 messages were moved here from #ideas > Inline type annotations by Joshua Warner.
here's some prior art on this topic from Elm Discourse: https://discourse.elm-lang.org/t/removing-redundancy-from-type-annotations/1319
That doesn't seem to have come to a really compelling conclusion?
Yep
agreed, just wanted to share it
personally I like the status quo, but I don't have as strong feelings about it as Evan expressed in that thread
I think that doing something like : Str, U64 -> Str is less viable in Roc since we use bodyless annotations for both Host functions and Zig implemented builtins
good point!
Neither of which exist in Elm
Which is actually also a point against inline types as well...
I don't think that fully invalidates it; it just means we'd need some adjustment elsewhere
For example, there could be some syntax for eliding the body in that case
To be fair for those rarer special annotations, you could just do something like:
: Str, U64 -> Str
fn
Or:
: Str, U64 -> Str
fn = ...
e.g.
: Str, Str -> Str
foo = ...
Is the problem having to type it or having to read it?
Having to type it, and having to keep it up to date if I decide to rename the function
Unless we do something like add a new keyword that says "I assert this function body is written outside of Roc"
writeFile! = |path: Str, contents: List(U8)| => {}
external!
This is where an lsp definitely can solve a lot of problems (or just copy and paste). That is part of the reason why I still prefer out of line.
Could we have the LSP do it?
I think something that spat out the name and inferred types would be nice in general
I don't like depending on the LSP to make the language pleasant to use
But if you are renaming, you already have to update tons of locations
These 2 are just the start
It could be called all over the place
Not if I haven't written any callers yet
But in that case it is super minor and just and single copy and paste
I definitely still find it annoying
Maybe it's a personal taste thing
Totally fair. I find it a minor inconvenience too, but I find out of line definitions so much nicer than all the c++ and rust I write. So the tradeoff just doesn't win for me.
If you had the same out-of-line definition but didn't have to repeat the name, is that still a tradeoff for you? That seems orthogonal to the out-of-line-ness to me.
Totally
I will gladly accept not repeating the name
I guess the thing I like about repeating the name is just considering the type annotation as sort of a self-contained thing
like "this is the type of this name"
It definitely has some minor readability benefits, though I question if that is mostly due to being so used to it
Like if you had:
someFunc : Num a, Str -> OtherType a
= | x, name |
...
Or something like that, it might have a single name and look fine (totally a random syntax, just trying to convey the rough idea).
Having written Elm extensively for the last 4 years, which uses the same syntax, I have to say that renaming the type def has never been an issue... it is literally two stack lines so it is hard to miss and easy to change at the same time by using multicursors / find-replace.
The non-inline typedef serves so that adding a type notation is exclusively a new line(s) diff. And having the redundant name makes it so that the placeholder typedef, with no implementation, is both valid by itself and adding an implementation is also just a new line(s) diff.
I honestly have more (still little) annoyance with having repeat exported/public names. If you have to change those, you have to scroll. But I definitely like seeing all exports at the top of the file. Anyway, I feel like this is in the same boat of being slightly annoying to update (but less so with LSP support), but much easier to read
When I code in Roc, I sometimes first write the type annotations before I write the actual function. For example
myFunc = \arg1, arg2 ->
if arg1 == "" then
doStuff arg2
else
...
doStuff : Str-> U64
When writing myFunc it helps to act, as if there was the function doStuff without thinking about how it would be implemented.
Roc has the nice feature, that you can compile code with functions, that do not have an implementation yet. Especially, you can run tests. They crash, when they are called, but it is nice, that you don't have to code everything before running the program or the tests.
Please implement this idea in a way, where it is still possible to add a type annotation, without writing the actual implementation.
It seems like the difference between
do_stuff : Str, U64 -> List(Str)
and
do_stuff = |text: Str, count: U64| -> List(Str)
Is not a big difference to me
So if you want to just write the type, you presumably know roughly what the arg names are? I tend to try to think of both
I though, that we taking about, that the current code:
do_stuff : Str, U64 -> List(Str)
do_stuff = |text, count|
List.repeat text count
would become something like
: Str, U64 -> List(Str)
do_stuff = |text, count|
List.repeat text count
I can not see, how you would create a type without its implementation in this world.
Oh man, I was mixing the threads :sweat_smile:
I was thinking about #ideas > Inline type annotations
Yes, I think so long as our current syntax for defining host-implemented effectful functions and Zig-implemented builtins is to just put name : type, I don't think omitting the duplicated name in the signature is viable
But the argument of "I like writing the signature just to outline my API before implementing" to me doesn't convince me that out-of-line types are better because it seems like inline signatures sans a body works just as well there
You could use a todo expression for unimplemented functions to be implemented in Roc, and then something like Gleam has for binding a signature to a runtime implementation (I think a builtin keyword that needs the name of the module and member)
Joshua Warner said:
I don't like depending on the LSP to make the language pleasant to use
I don't think you need LSP for this though - as Evan noted in the other thread, any editor that has "select next" (e.g. cmd-D in Sublime, VS Code, Zed, etc.) means it's trivial to select both names and change them both at the same time
Yep, I think it fundamentally comes back to personal taste for me
I do really like @Brendan Hansknecht 's earlier suggestion of omitting the second name - i.e. allowing an equals immediately after the annotation (with no extra name).
someFunc : Num a, Str -> OtherType a
= | x, name |
...
this_function_disagrees_with_you : Str
= "hopefully"
Why does that function disagree with me?
Yes, it's a long name. Yes, if we wanted the formatter to maintain alignment there, that means a lot of spaces. But I don't think wanting to maintain alignment is a given (we don't do it anywhere else, currently), and even if we do maintain alignment, that doesn't strike me as bad in this case.
Could you expound? I think maybe I just don't understand.
At that point, why not just allow the = on the same line as the end of the annotation?
Yeah, that is exactly what I was thinking
But maybe the formatter could format it as described there, for anything but single-identifier types
Last updated: Jun 16 2026 at 16:19 UTC