Stream: ideas

Topic: Redundant name in type annotations


view this post on Zulip Joshua Warner (Dec 27 2024 at 01:03):

Switching gears then: Is there perhaps a syntax we could use to remove the redundancy of naming the function twice?

view this post on Zulip Joshua Warner (Dec 27 2024 at 01:05):

As a strawman, what about just removing the name? e.g.

: Str, Str -> Str
foo = |a, b| Str.join ["Hello, ", a, " and ", b]

view this post on Zulip Richard Feldman (Dec 27 2024 at 01:40):

I'd say that's worth a different thread :big_smile:

view this post on Zulip Notification Bot (Dec 27 2024 at 01:41):

3 messages were moved here from #ideas > Inline type annotations by Joshua Warner.

view this post on Zulip Richard Feldman (Dec 27 2024 at 01:41):

here's some prior art on this topic from Elm Discourse: https://discourse.elm-lang.org/t/removing-redundancy-from-type-annotations/1319

view this post on Zulip Joshua Warner (Dec 27 2024 at 02:03):

That doesn't seem to have come to a really compelling conclusion?

view this post on Zulip Sam Mohr (Dec 27 2024 at 02:25):

Yep

view this post on Zulip Richard Feldman (Dec 27 2024 at 02:26):

agreed, just wanted to share it

view this post on Zulip Richard Feldman (Dec 27 2024 at 02:27):

personally I like the status quo, but I don't have as strong feelings about it as Evan expressed in that thread

view this post on Zulip Sam Mohr (Dec 27 2024 at 02:27):

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

view this post on Zulip Richard Feldman (Dec 27 2024 at 02:27):

good point!

view this post on Zulip Sam Mohr (Dec 27 2024 at 02:27):

Neither of which exist in Elm

view this post on Zulip Sam Mohr (Dec 27 2024 at 02:27):

Which is actually also a point against inline types as well...

view this post on Zulip Joshua Warner (Dec 27 2024 at 02:28):

I don't think that fully invalidates it; it just means we'd need some adjustment elsewhere

view this post on Zulip Joshua Warner (Dec 27 2024 at 02:28):

For example, there could be some syntax for eliding the body in that case

view this post on Zulip Brendan Hansknecht (Dec 27 2024 at 02:28):

To be fair for those rarer special annotations, you could just do something like:

: Str, U64 -> Str
fn

Or:

: Str, U64 -> Str
fn = ...

view this post on Zulip Joshua Warner (Dec 27 2024 at 02:28):

e.g.

: Str, Str -> Str
foo = ...

view this post on Zulip Karl (Dec 27 2024 at 02:29):

Is the problem having to type it or having to read it?

view this post on Zulip Joshua Warner (Dec 27 2024 at 02:29):

Having to type it, and having to keep it up to date if I decide to rename the function

view this post on Zulip Sam Mohr (Dec 27 2024 at 02:30):

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!

view this post on Zulip Brendan Hansknecht (Dec 27 2024 at 02:30):

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.

view this post on Zulip Karl (Dec 27 2024 at 02:30):

Could we have the LSP do it?

view this post on Zulip Karl (Dec 27 2024 at 02:31):

I think something that spat out the name and inferred types would be nice in general

view this post on Zulip Joshua Warner (Dec 27 2024 at 02:32):

I don't like depending on the LSP to make the language pleasant to use

view this post on Zulip Brendan Hansknecht (Dec 27 2024 at 02:33):

But if you are renaming, you already have to update tons of locations

view this post on Zulip Brendan Hansknecht (Dec 27 2024 at 02:34):

These 2 are just the start

view this post on Zulip Brendan Hansknecht (Dec 27 2024 at 02:34):

It could be called all over the place

view this post on Zulip Joshua Warner (Dec 27 2024 at 02:34):

Not if I haven't written any callers yet

view this post on Zulip Brendan Hansknecht (Dec 27 2024 at 02:34):

But in that case it is super minor and just and single copy and paste

view this post on Zulip Joshua Warner (Dec 27 2024 at 02:35):

I definitely still find it annoying

view this post on Zulip Joshua Warner (Dec 27 2024 at 02:35):

Maybe it's a personal taste thing

view this post on Zulip Brendan Hansknecht (Dec 27 2024 at 02:39):

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.

view this post on Zulip Joshua Warner (Dec 27 2024 at 02:42):

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.

view this post on Zulip Brendan Hansknecht (Dec 27 2024 at 02:42):

Totally

view this post on Zulip Brendan Hansknecht (Dec 27 2024 at 02:42):

I will gladly accept not repeating the name

view this post on Zulip Richard Feldman (Dec 27 2024 at 03:03):

I guess the thing I like about repeating the name is just considering the type annotation as sort of a self-contained thing

view this post on Zulip Richard Feldman (Dec 27 2024 at 03:03):

like "this is the type of this name"

view this post on Zulip Brendan Hansknecht (Dec 27 2024 at 04:01):

It definitely has some minor readability benefits, though I question if that is mostly due to being so used to it

view this post on Zulip Brendan Hansknecht (Dec 27 2024 at 04:03):

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).

view this post on Zulip Georges Boris (Dec 27 2024 at 04:24):

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.

view this post on Zulip Kilian Vounckx (Dec 27 2024 at 09:05):

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

view this post on Zulip Oskar Hahn (Dec 27 2024 at 11:50):

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.

view this post on Zulip Sam Mohr (Dec 27 2024 at 11:53):

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

view this post on Zulip Sam Mohr (Dec 27 2024 at 11:57):

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

view this post on Zulip Oskar Hahn (Dec 27 2024 at 12:01):

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.

view this post on Zulip Sam Mohr (Dec 27 2024 at 12:02):

Oh man, I was mixing the threads :sweat_smile:

view this post on Zulip Sam Mohr (Dec 27 2024 at 12:02):

I was thinking about #ideas > Inline type annotations

view this post on Zulip Sam Mohr (Dec 27 2024 at 12:04):

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

view this post on Zulip Sam Mohr (Dec 27 2024 at 12:06):

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

view this post on Zulip Anthony Bullard (Dec 27 2024 at 14:06):

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)

view this post on Zulip Richard Feldman (Dec 27 2024 at 14:13):

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

view this post on Zulip Joshua Warner (Dec 27 2024 at 14:47):

Yep, I think it fundamentally comes back to personal taste for me

view this post on Zulip Joshua Warner (Dec 27 2024 at 14:52):

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 |
         ...

view this post on Zulip Georges Boris (Dec 27 2024 at 15:16):

this_function_disagrees_with_you : Str
                                 = "hopefully"

view this post on Zulip Joshua Warner (Dec 28 2024 at 14:50):

Why does that function disagree with me?

view this post on Zulip Joshua Warner (Dec 28 2024 at 14:52):

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.

view this post on Zulip Joshua Warner (Dec 28 2024 at 14:53):

Could you expound? I think maybe I just don't understand.

view this post on Zulip Anthony Bullard (Dec 28 2024 at 14:57):

At that point, why not just allow the = on the same line as the end of the annotation?

view this post on Zulip Joshua Warner (Dec 28 2024 at 15:14):

Yeah, that is exactly what I was thinking

view this post on Zulip Joshua Warner (Dec 28 2024 at 15:14):

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