Stream: ideas

Topic: Inline type annotations


view this post on Zulip Sam Mohr (Dec 20 2024 at 00:10):

I recently proposed having warnings for unannotated exposed symbols in a thread as a way to shore up a concern on the interaction of type inference and static dispatch. Namely, current Roc qualifies most function calls with the module that they're from (e.g. List), which makes both reading what type is being used as well as inferring a human-readable type annotation quite easy. This is less true in Static Dispatch Roc:tm:: taking the example from the linked thread, something as simple as this:

count_valid_elements = elems ->
    elems.map_try(.validate()).len()

Would infer a pretty complex type signature without any type guidance:

count_valid_elements : a -> e
    where
        a.map_try(b -> c) -> d,
        b.validate() -> c,
        d.len() -> e
count_valid_elements = elems ->
    elems.map_try(.validate()).len()

I want to find a mechanism that helps devs get good autocomplete suggestions and type as they go without needing to plan too far ahead. My thought is that just by constraining the argument types of the count_valid_elements function, we can get pretty good autocomplete on map_try and len:

count_valid_elements : List(Foo) -> _
count_valid_elements = elems ->
    elems.map_try(.validate()).len()

Even if we only annotated the arg type as List(_), we'd still get map_try and len autocompleted, just not .validate. And of course, a reader for these functions can also generally infer what's happening even without a return type.

Hence why I want to consider a syntax that enables people to get this context without going out of their way, maybe that will make it more likely to happen! I can think of quite a few languages that have a syntax for this...

It's (optional) inline type annotations!

count_valid_elements = |elems: List(Foo)|
    elems.map_try(.validate()).len()

We're not all totally onboard with using | for function arg delimiting, but if we do, I think this quite naturally falls out of that. Users can now optionally write arg types as they go and get a significantly better experience for it. I really think that without type annotations on the args (at least), a lot of the static dispatch benefits are lost.

Let's see some other examples and then talk about why they look the way they do:

simple_func = |arg1: Str| -> List(U8)
    arg1.to_utf8()

effectful! = |text: Str| => {}
    Stdout.line! text

complex_func = |{ left, right }: Pair, Unwrap(inner) as outer: TagUnion| -> OtherTag(a, b)
    where a.foo() -> b, b.bar() -> List(Str)

    # required newline after multiline type signatures
    combo: U64 = left + right
    var loop_var_: List(Str) = [inner]

    for item: Str in more_items do
        loop_var_ = loop_var_.append(item)

    OutputTag(loop_var_)

# this is not as nice, but still allows breaking up typing
other_complex_func = |
    { left, right }: Pair,
    Unwrap(inner) as outer: TagUnion,
| -> OtherTag(a, b)
    where a.foo() -> b, b.bar() -> List(Str)

    function.body().below().newline()

# I think we disallow annotating return types in inline function bodies
inline = |val: U64| -> U64; val + 64

A few nice things about this:

I think that there's some wiggle room on the syntax here that I'm open to discuss, but I'm mostly here to see if people agree that this pushes people towards better dev/reading experience.

A few things I'd like to hear from you all if you don't like this proposal:

view this post on Zulip Brendan Hansknecht (Dec 20 2024 at 00:15):

The where clause is very strange with this syntax

view this post on Zulip Sam Mohr (Dec 20 2024 at 00:16):

It's almost exactly the same as where in Rust

view this post on Zulip Brendan Hansknecht (Dec 20 2024 at 00:17):

Yes, but it's in the function body

view this post on Zulip Brendan Hansknecht (Dec 20 2024 at 00:17):

Cause we don't have braces

view this post on Zulip Brendan Hansknecht (Dec 20 2024 at 00:18):

Also, in rust you often use a trait directly in the type annotation which reduces how often where is used.

view this post on Zulip Sam Mohr (Dec 20 2024 at 00:18):

Yeah, that's why I have the mandatory newline. As is demonstrated by the |arg| -> U64; arg + 123 example, the boundary between the return type and the function body is IMO the weakest part of this proposal

view this post on Zulip Sam Mohr (Dec 20 2024 at 00:20):

Brendan Hansknecht said:

Also, in rust you often use a trait directly in the type annotation which reduces how often where is used.

Sounds like we could just emulate impl Trait from Rust to help with this issue:

func = |arg: is a.foo() -> Str|
    arg.foo()

view this post on Zulip Brendan Hansknecht (Dec 20 2024 at 00:20):

Anyway, I'm pretty neutral to this. I think the separate line is a lot cleaner to read still.

view this post on Zulip Brendan Hansknecht (Dec 20 2024 at 00:20):

I do agree that static dispatch is more complex without type info

view this post on Zulip Brendan Hansknecht (Dec 20 2024 at 00:20):

Kinda blind

view this post on Zulip Brendan Hansknecht (Dec 20 2024 at 00:21):

But I also think that rust function definitions with types are a mess and don't really want them in roc

view this post on Zulip Brendan Hansknecht (Dec 20 2024 at 00:22):

Personally if a function has any sort of vague complexity, I type first then write the body.

view this post on Zulip Sam Mohr (Dec 20 2024 at 00:24):

I think Python has the exact same story here for type annotations and autocomplete. If you write:

def to_uppercase(s):
    return s.up...

And your cursor is where the ... is, you get no autocomplete to .upper(), which is why type hints help with that:

def to_uppercase(s: str):
    return s.up...

This will autocomplete to upper() from my memory

view this post on Zulip Richard Feldman (Dec 20 2024 at 00:26):

I think this is something where we can (and should) wait and see if it's a problem in practice before considering taking action on it

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

might turn out to be totally fine! :big_smile:

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

I'd be surprised, but I'm open to the possibility

view this post on Zulip Luke Boswell (Dec 20 2024 at 00:33):

Would infer a pretty complex type signature without any type guidance

I see this as a positive thing. People who think in types and write annotations are given more useful assistance from the compiler... therefore people will be more inclined to do that for any nontrivial type.

I like that type annotations are completely optional, because there are times when you don't care. Maybe you're still thinking, or maybe it's a quick and dirty to throw away, or maybe it's just plain obvious.

helps devs get good autocomplete suggestions and type as they go without needing to plan too far ahead

I also think this is a good thing. People are less likely to chain massive things together because it's harder to follow... so they will break it up and give things names (and hopefully annotations).

Even if we only annotated the arg type as List(_),

It's really nice adding even a partial annotation, even leaving holes everywhere is helpful. This encourages people to do that, and also not get to hung up on trying to write out the full type because we know with full inference the compiler can get what were talking about.

count_valid_elements = |elems: List(Foo)|

What I don't like about the inline annotation is that now it mixes in the types and makes things more verbose. Maybe for a simple List a it's not too cluttered, but it quickly get's wild. Why not just make an annotation on another line at this point?

I like that with |_| the -> and => are relegated to "type" level information.

Do you agree with me that non-annotated function args make the autocomplete experience worse?

Yes, but I think think this is why people who want nice autocomplete will naturally begin to think in types, and use annotations for anything that is not trivial or quick and dirty.

what other tools do we have available that you would like to see combat this issue?

I guess I disagree that this is an issue, at least in the same way... but I don't fully understand the interaction with language tooling.

How bad is the autocomplete if I'm at this point and hit .?

count_valid_elements : List(_) -> _
count_valid_elements = |elems|
    elems.

Basically I think the separate line for type annotations has really grown on me, and I've come to really appreciate it.

view this post on Zulip Brendan Hansknecht (Dec 20 2024 at 00:38):

Note, an lsp code action should be able to generate a placeholder all underscore type automatically:

count_valid_elements : _ -> _

And set the cursor at the first underscore to fill it in

view this post on Zulip Brendan Hansknecht (Dec 20 2024 at 00:38):

So at that point it is really no different on ergonomics than the proposed

view this post on Zulip Sam Mohr (Dec 20 2024 at 00:39):

You're right that if you have autocomplete from an LSP, you also have code actions

view this post on Zulip Sam Mohr (Dec 20 2024 at 00:43):

So we can plan to have code actions that might, for example, let you type count_valid_elements and then run "Add annotation" that adds Brendan's suggested skeleton, and your cursor would get moved right to the first argument's type hole.

view this post on Zulip Sam Mohr (Dec 20 2024 at 00:47):

I'd be much more likely to write count_valid_elements = |elems| and then think "I need a type" and copy the name of count_valid_elements and then write it, which is a bit more execution function required than just writing the type right after the arg, but it's not that much

view this post on Zulip Sam Mohr (Dec 20 2024 at 01:06):

I guess I disagree that this is an issue, at least in the same way... but I don't fully understand the interaction with language tooling.

@Luke Boswell when I said "what other tools do we have" I was talking about syntax, typechecking, or LSP, not just code actions. Generally asking what can be done to get good autocomplete

view this post on Zulip Sam Mohr (Dec 20 2024 at 01:08):

Yeah, this whole discussion comes down to either:

view this post on Zulip Sam Mohr (Dec 20 2024 at 01:09):

I'm happy to wait for that to bear out, though it is somewhat "syntax explosion season" right now, meaning changing things now would cause fewer disruptions later

view this post on Zulip Anthony Bullard (Dec 20 2024 at 01:49):

My first question, @Sam Mohr , is why are you such a bad person? :joy::rolling_on_the_floor_laughing::joy::rolling_on_the_floor_laughing:

view this post on Zulip Sam Mohr (Dec 20 2024 at 01:51):

I spend my time here not to improve Roc, but to break @Luke Boswell 's hopes and dreams of a better state of programming languages

view this post on Zulip Anthony Bullard (Dec 20 2024 at 01:54):

I think this fits well with the recent threads poking at “what will we accept syntactically to find mainstream acceptance?” proposals. I don’t think this would be good or bad - but man it’s getting harder and harder to find the Elm lineage in the language as we go on.

view this post on Zulip Richard Feldman (Dec 20 2024 at 01:55):

the APIs are still straight outta Elm

view this post on Zulip Richard Feldman (Dec 20 2024 at 01:55):

same as always

view this post on Zulip Anthony Bullard (Dec 20 2024 at 01:55):

True

view this post on Zulip Richard Feldman (Dec 20 2024 at 01:55):

the only substantial change there has been Task becoming =>

view this post on Zulip Anthony Bullard (Dec 20 2024 at 01:55):

It’s just the vibes are changing fast :joy:

view this post on Zulip Richard Feldman (Dec 20 2024 at 01:55):

which has been one of the most well-received changes in the history of the language :big_smile:

view this post on Zulip Anthony Bullard (Dec 20 2024 at 01:56):

Oh I mean with the proposed changes

view this post on Zulip Anthony Bullard (Dec 20 2024 at 01:56):

Not what’s actually happened

view this post on Zulip Richard Feldman (Dec 20 2024 at 01:56):

I don't think that'll change the APIs much either

view this post on Zulip Anthony Bullard (Dec 20 2024 at 01:56):

I think in my brain some of this has happened

view this post on Zulip Richard Feldman (Dec 20 2024 at 01:56):

well take Str as an example

view this post on Zulip Richard Feldman (Dec 20 2024 at 01:57):

when static dispatch lands, off the top of my head I think the only changes would be:

view this post on Zulip Richard Feldman (Dec 20 2024 at 01:57):

that's probably it?

view this post on Zulip Richard Feldman (Dec 20 2024 at 01:57):

same with List

view this post on Zulip Luke Boswell (Dec 20 2024 at 01:58):

Until the next big :light_bulb:

view this post on Zulip Sam Mohr (Dec 20 2024 at 01:58):

Side note, equals is better than is_eq

view this post on Zulip Richard Feldman (Dec 20 2024 at 01:58):

I actually think this is gonna be what v0.1.0 looks like

view this post on Zulip Luke Boswell (Dec 20 2024 at 01:58):

I have really appreciated how (correction Richard) keep discovering nice ways to simplify the language

** my contribution is just throwing rocks

view this post on Zulip Richard Feldman (Dec 20 2024 at 01:58):

maybe I should start a thread about that

view this post on Zulip Luke Boswell (Dec 20 2024 at 01:59):

Maybe a blog post?

view this post on Zulip Luke Boswell (Dec 20 2024 at 02:00):

That sounds like a big commitment though

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

actually I don't think blog posts are big commitments

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

:grinning_face_with_smiling_eyes:

view this post on Zulip Anthony Bullard (Dec 20 2024 at 02:10):

So you have no worries about the types that will be inferred for these SD-using functions? I’m trying to understand how - absent annotation - we could even supply this wonderful autocomplete experience we imagine with SD.

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

well going back to this example:

count_valid_elements = |elems|
    elems.map_try(.validate()).len()

certainly you can't get it to autocomplete .validate without giving the compiler more information than what's in that snippet

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

because it can only possibly work with the information that it has

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

if you've already written code that calls count_valid_elements (before you implemented it), that can do it

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

or if you add a type annotation, that can also do it

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

given that it's about as much work to write : List Elem as it is to write .validate() without the assistance of autocomplete, and that LLM-powered autocomplete can (some of the time) guess implementations based on surrounding context even in the absence of type information, I'm just not sure that in practice people will be like "yeah if I had that I would use it and it would save me time"

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

maybe people will say "hey I wish this existed so I could use it" in practice, in which case we can talk about specific scenarios that have come up for people in practice

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

but to me it seems premature to talk about solving a problem which is so hypothetical at this point

view this post on Zulip Anthony Bullard (Dec 20 2024 at 02:27):

That’s fair, but I’d like to understand how we would even autocomplete map_try? elems is an a and we just try matching it to all methods from all available modules that contain opaque types? And the once one method has been used, all chained methods are easy?

Is that why you think .validate is the harder case?

view this post on Zulip Luke Boswell (Dec 20 2024 at 02:28):

Would you have to type m a p ... etc and it reduces that list down?

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

Anthony Bullard said:

That’s fair, but I’d like to understand how we would even autocomplete map_try? elems is an a

oh, sure - we also wouldn't be able to infer map_try if you were writing jut that from scratch with no annotations

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

Anthony Bullard said:

we just try matching it to all methods from all available modules that contain opaque types?

we could certainly do that if desired

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

we know which modules are in scope, and we know what all their exposed types are

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

so yeah, we couldn't narrow it all the way down to just the List ones but if you type m, we could make it so that map_try (from List) is one of the ones you see in the autocomplete list

view this post on Zulip Eli Dowling (Dec 21 2024 at 03:48):

I'm strongly in favour of inline annotations. I have often thought while writing roc that my experience would be significantly improved by having access to them.

My number one roc frustration is weird type mismatches which propagate to far off places. Almost always to fix that I want to hop around my program "pinning" types here and there when I know what they will be.

Currently in roc that process is really annoying. It's not really even possible in pattern matching, it feels super verbose for individual variables, It isn't possible for anonymous lambdas (which are very often the place I want to do this btw).

So I'm strongly in favour, mostly because I think it improves the debugging experience for what I think of as roc's most uniquely frustrating issue.

view this post on Zulip Brendan Hansknecht (Dec 21 2024 at 03:54):

sarcasm... If a lambda has no name, does it deserve to have types?

view this post on Zulip Eli Dowling (Dec 21 2024 at 03:59):

Hahah. Well that's it right, I generally don't want to write a whole type. I just want to type a single arg.

#.. some chain of pipes
|>List.map \ {a,b}:{a:Str, b:[Yeah, Nahh]} ->

Like all I want to do is basically say the type system:
"alright I know my type should look like this:, don't go running off union tags and records and producing some ungodly mess of a type error later on, just tell me if I'm wrong right here"

view this post on Zulip Sam Mohr (Dec 21 2024 at 05:03):

The first good person in this thread!

view this post on Zulip Brendan Hansknecht (Dec 21 2024 at 05:58):

I like typing things. I just think inline types look pretty terrible.

view this post on Zulip Brendan Hansknecht (Dec 21 2024 at 05:59):

And I live just fine in roc without them so feel no need for them

view this post on Zulip Sam Mohr (Dec 21 2024 at 06:01):

Yeah, I think my want for this feature comes down to incentivizing Roc devs to improve readability, but disciplined devs don't benefit as much

view this post on Zulip Brendan Hansknecht (Dec 21 2024 at 06:13):

Given roc infers all types, I don't worry too much about what other devs do. In any code base that grows I think that annotating top levels will become a pretty natural lint.

view this post on Zulip Sam Mohr (Dec 21 2024 at 06:22):

I'm thinking about reading code in PRs and on GitHub without an LSP. All of my concerns are mostly assuaged with tooling, but it's not great if an LSP is required to read someone's fully type-inferred code

view this post on Zulip Eli Dowling (Dec 21 2024 at 06:45):

Brendan Hansknecht said:

I like typing things. I just think inline types look pretty terrible.

Well I think types on their own line look terrible.
So there! :joy:

view this post on Zulip Sam Mohr (Dec 21 2024 at 06:46):

I'm averse to redundancy which makes me dislike typing the name of the def twice, but I see the benefit of keeping the body of the def cleaner, so I'm not sure which I visually prefer

view this post on Zulip Eli Dowling (Dec 21 2024 at 06:51):

I think the multi line is fine in top level Defs but just really ugly and messy when used inside a function body or to annotate a single variable or soemthing..

view this post on Zulip Brendan Hansknecht (Dec 21 2024 at 07:18):

Yeah, I can agree with that one.....honestly, I think c style is the cleanest inline

view this post on Zulip Brendan Hansknecht (Dec 21 2024 at 07:19):

As much as I try to love x : Type = ... I think Type x = ... is just less noisy inline.

view this post on Zulip Brendan Hansknecht (Dec 21 2024 at 07:22):

Also, a lot of languages have less and less types inside of functions locally. But I think they all essentially always type function and lambda args (which is probably the more important piece of complexity here)

view this post on Zulip Brendan Hansknecht (Dec 21 2024 at 07:22):

Like in rust, it is just let x = ... with no type. In c++ you see more and more auto x = ...

view this post on Zulip Brendan Hansknecht (Dec 21 2024 at 07:23):

So clearly there is a more complex balance here

view this post on Zulip lue (Dec 21 2024 at 12:31):

I mean if the only issue is the extra :, many languages like v allow you to drop it x Type = and IMO it reads perfectly fine (I especially love it for record types).

view this post on Zulip Anthony Bullard (Dec 21 2024 at 12:32):

I agree with @lue here, I think Go syntax is great, and even better for a language with optional annotations.

view this post on Zulip Anthony Bullard (Dec 21 2024 at 12:33):

And with PNC syntax, it fits in

view this post on Zulip Anthony Bullard (Dec 21 2024 at 12:37):

Here's Sam's original code sample with Go-style type annotation:

simple_func = |arg1 Str| -> List(U8)
    arg1.to_utf8()

effectful! = |text Str| => {}
    Stdout.line! text

complex_func = |{ left, right } Pair, Unwrap(inner) as outer TagUnion| -> OtherTag(a, b)
    where a.foo() -> b, b.bar() -> List(Str)

    # required newline after multiline type signatures
    combo U64 = left + right
    var loop_var_ List(Str) = [inner]

    for item Str in more_items do
        loop_var_ = loop_var_.append(item)

    OutputTag(loop_var_)

# this is not as nice, but still allows breaking up typing
other_complex_func = |
    { left, right } Pair,
    Unwrap(inner) as outer TagUnion,
| -> OtherTag(a, b)
    where a.foo() -> b, b.bar() -> List(Str)

    function.body().below().newline()

# I think we disallow annotating return types in inline function bodies
inline = |val U64| -> U64; val + 64

view this post on Zulip Richard Feldman (Dec 21 2024 at 13:50):

Eli Dowling said:

My number one roc frustration is weird type mismatches which propagate to far off places. Almost always to fix that I want to hop around my program "pinning" types here and there when I know what they will be.

Currently in roc that process is really annoying. It's not really even possible in pattern matching, it feels super verbose for individual variables, It isn't possible for anonymous lambdas (which are very often the place I want to do this btw).

I'm curious to learn more about this! Do you think type hints in the editor would help, if they revealed inline what types the compiler thinks things are?

Also I'm curious how often you have top-level type annotations when you're encountering this.

view this post on Zulip Richard Feldman (Dec 21 2024 at 13:59):

as an aside, a downside of inline annotations is that it would create two competing ways to annotate top-level functions. So this would become syntactically valid:

foo : Str -> Str
foo = |arg : Bool| -> Bool
    ...

...and then you could have a new category of error, namely that the two annotations for the same thing disagree :sweat_smile:

view this post on Zulip Sky Rose (Dec 21 2024 at 14:33):

The same error could happen even if we switch to only having inline definitions, if there are multiple places that you can put the type:

# typing the foo pattern, and also typing the function definition
# this is more likely to come up if you have an alias for the function type, like `foo : MyPredicate = ...`
foo : Str -> Str = |arg : Bool| -> Bool
    ...

# typing the a and b subpatterns, and also typing the record as a whole
{a : Str, b : Str} : {a: Bool, b: Bool} = ...

I don't think we should try to come up with strict rules about which parts of a pattern can get types, so I think this is just an inherent tradeoff with inline types. But I do still like them, and think they could be worth allowing even with the extra way to trigger a type error.

view this post on Zulip Anthony Bullard (Dec 21 2024 at 14:35):

If you have a top level annotation, in what situations would you need to annotate a local binding or pattern? (I'd like to just a list of problematic scenarios).

view this post on Zulip Dawid Danieluk (Dec 21 2024 at 14:39):

Brendan Hansknecht said:

As much as I try to love x : Type = ... I think Type x = ... is just less noisy inline.

One thing I dislike about Type x is that variable names don't start at the same column. When I'm parsing the code quickly and want to get rough idea of what code is doing I'm first looking for variable names and see types as 'details'.

String name = ""
UInt32 age = 27
List<ChatMessage> messages = .....
Decimal account_balance = ...

vs

name String = ""
age UInt32 = 27
messages List<ChatMessage> = .....
account_balance Decimal = ....

For me second variant is DRASTICALLY easier to understand. Usually variable names are pretty descriptive so you get what's going on by just looking at them and if they're always starting on first column of the text it's just perfect for me.
Types really are just implementation details, today age is UInt32, tomorrow it will be UInt8 and as such I think they should come after variable name.

view this post on Zulip Sky Rose (Dec 21 2024 at 14:46):

Another problem with Type x = ... is that in today's syntax it's ambiguous with Tag x = .... (But x Type = ... works.)

view this post on Zulip Eli Dowling (Dec 21 2024 at 15:12):

Richard Feldman said:

I'm curious to learn more about this! Do you think type hints in the editor would help, if they revealed inline what types the compiler thinks things are?

Also I'm curious how often you have top-level type annotations when you're encountering this.

I've started compiling a list of specific examples of bad and confusing type errors I encounter in the wild and I will report back.

Yes I definitely think editor annotations would help, but we would also have to solve the current issue where type incompatibilities aren't propagated to the lsp as anything other than <type mismatch>. But if we could figure out a way to display type mismatches in a useful way within the editor that would definitely help a lot.

I definitely thought as I was writing, that maybe improvements in tooling and the compiler would also solve the issue.

But also I like inline annotations so still for this regardless :sweat_smile:

As for too level annotations, I tend to not have many. To be fair most of my roc code is exploratory programming where I'd really rather not constrain things though.

Part of my lack of top level annotations is actually that I find it really annoying to rewrite the signature and name and put in all the _ for stuff I don't care about like error accumulation. It breaks my happy flow.

I think "a should be a string"
And instead of pulling up and writing :Str
Now I'm wrangling with how many arguments the function has and whether it's effectful, and so now I've got to add
my_func_name: _, _, Str, _ -> _
And my variable names don't line up with my annotations so I have to play a little counting game to make sure my annotation is on the right argument and it's all just a bit nasty. Most of this struggle would be solved with an "annotate" code action but I think inline annotations solves it more cleanly.

I think inline type annotation would generally make me much more likely to pepper little annotations through my code, as I often do in ocaml and fsharp

view this post on Zulip Brendan Hansknecht (Dec 21 2024 at 17:23):

For me second variant is DRASTICALLY easier to understand. Usually variable names are pretty descriptive so you get what's going on by just looking at them and if they're always starting on first column of the text it's just perfect for me.

To clarify, 99% of the time, my preference is no types inline (except if I make a named lambda). So just the name is all I want. If I want types, it is cause I think the name is not/can not be clear. Or for a very temporary moment due to be confused by types (very very rare if all functions are typed).

view this post on Zulip Joshua Warner (Dec 26 2024 at 14:48):

I really like the idea of at least optionally allowing arg types to be annotated without having to duplicate the function name.

view this post on Zulip Joshua Warner (Dec 26 2024 at 14:51):

However, this syntax will have some parsing problems:

foo : Str -> Str = |arg : Bool| -> Bool
    ...

We'll have a really hard time distinguishing between the cases where the return type is multi-line, and the start of the body. Both cases would naturally be indented by 4 spaces relative to the start of the previous line. Maybe you're suggesting we disallow multi-line return types in this syntax? Or maybe if they are multi-line, they must be put in parens?

Alternatively, the arrow could move to after the type, e.g.:

foo : Str -> Str = |arg : Bool|: Bool ->
    ...

view this post on Zulip Anthony Bullard (Dec 26 2024 at 15:18):

I didn't think there would be ambiguity here since || syntax is only used in implementation, not in the type signature (and there is no -> with || syntax here it would mean "next parse a return type" unambiguously).

view this post on Zulip Joshua Warner (Dec 26 2024 at 15:20):

Ahhh sorry; I messed up my example a bit. Also possible I didn't understand the proposal.

view this post on Zulip Anthony Bullard (Dec 26 2024 at 15:21):

I think you'd either see

foo : Str -> U64 = |arg| arg.toUtf8().len()
    ...

or

foo = |arg: Str| -> U64
    arg.toUtf8().len()
    ...

or (arrow free):

foo = |arg: Str|: U64
    arg.toUtf8().len()
    ...

view this post on Zulip Joshua Warner (Dec 26 2024 at 15:22):

Right, so in your second example, after we've parsed U64, and the parser sees a newline, indent, and then arg, how does it know whether that's a continuation of the type (so U64 arg), or the start of the body?

view this post on Zulip Anthony Bullard (Dec 26 2024 at 15:24):

Yeah, I see your point. It does seem that multiline signatures would require parens if there is not a sigil for introducing the body of the function

view this post on Zulip Anthony Bullard (Dec 26 2024 at 15:25):

Yeah, I don't love it. But how common are multiline return types?

view this post on Zulip Anthony Bullard (Dec 26 2024 at 15:27):

Let's look at osmething like Result.mathBoth

view this post on Zulip Anthony Bullard (Dec 26 2024 at 15:27):

Which today is

mapBoth :
    Result ok1 err1,
    (ok1 -> ok2),
    (err1 -> err2)
    -> Result ok2 err2

view this post on Zulip Sam Mohr (Dec 26 2024 at 15:32):

Joshua Warner said:

Right, so in your second example, after we've parsed U64, and the parser sees a newline, indent, and then arg, how does it know whether that's a continuation of the type (so U64 arg), or the start of the body?

I think the new PNC type world (types are Dict(Str, U64)) could only have the return type of a function followed by a where clause, so if "where" shows up, then you keep going, otherwise it's the function body

view this post on Zulip Sam Mohr (Dec 26 2024 at 15:32):

I can't personally think of an ambiguous case

view this post on Zulip Anthony Bullard (Dec 26 2024 at 15:33):

If you had inline annotations, it would be either(in proposed syntax)

map_both :
    Result(ok1, err1),
    (ok1 -> ok2),
    (err1 -> err2)
    -> Result(ok2, err2) = |res, ok_mapper, err_mapper|
        # ...

or

map_both = |res: Result(ok1, err1), ok_mapper: (ok1 -> ok2), err_mapper: (err1 -> err2)|: Result(ok2, err2)
    # ...

Which would be probably formatted as:

map_both = |
    res: Result(ok1, err1),
    ok_mapper: (ok1 -> ok2),
    err_mapper: (err1 -> err2)|: Result(ok2, err2)
        # ...

Does this mean that inline annotations have to either be solely on the def itself ORR on the args and return value?

Because

map_both :
    Result(ok1, err1),
    (ok1 -> ok2),
    (err1 -> err2)
    -> Result(ok2, err2) = |
        res: Result(ok1, err1),
        ok_mapper: (ok1 -> ok2),
        err_mapper: (err1 -> err2)|: Result(ok2, err2)
            # ...

Seems insane :rofl:

UPDATED TO USE PNC SYNTAX FOR TYPES

view this post on Zulip Anthony Bullard (Dec 26 2024 at 15:33):

@Sam Mohr Oh yeah, I forgot with PNC we don't have space application anymore

view this post on Zulip Anthony Bullard (Dec 26 2024 at 15:34):

Everything is now "enclosed by law" :smile:

view this post on Zulip Joshua Warner (Dec 26 2024 at 15:40):

Ah yeah if types no longer have a "space-separated apply" syntax, this isn't an issue

view this post on Zulip Brendan Hansknecht (Dec 26 2024 at 22:37):

I really think this shouldn't be optional..I think having inline and out of line options is a mistake and just messy. I think we should have only one or the other

view this post on Zulip Brendan Hansknecht (Dec 26 2024 at 22:37):

Of which, I much prefer out of line

view this post on Zulip Sam Mohr (Dec 26 2024 at 22:38):

That was my intention when I proposed this

view this post on Zulip Sam Mohr (Dec 26 2024 at 22:38):

Only inline types

view this post on Zulip Sam Mohr (Dec 26 2024 at 22:38):

(If we took this option)

view this post on Zulip Brendan Hansknecht (Dec 26 2024 at 22:38):

Yeah, I think only inline would be much better than a mix but of both, but clearly still lean only out of line.

view this post on Zulip Brendan Hansknecht (Dec 26 2024 at 22:39):

Especially with the knowledge that for loops are coming and deeply nested lambdas will be less and less likely with all the coming features.

view this post on Zulip Sam Mohr (Dec 26 2024 at 22:41):

It seems like we would need some benefit that inline gives that can't be gotten from header type annotations, or for more than 50% of people to want it. Otherwise we should stick with the default of our current approach

view this post on Zulip Brendan Hansknecht (Dec 26 2024 at 22:43):

Note: I think after our future syntax stabilizes seeing more code in practice will really tell how much impact an inline annotations might have. I think it heavily depends on the number of inline lambdas in roc (specifically lambdas where the types aren't obvious). I think those will be pretty rare. If they are not rare, then inline will make way more sense.

view this post on Zulip Sam Mohr (Dec 26 2024 at 22:45):

Well, I still fail to see how any functions will properly take advantage of static dispatch without people annotating their arg types, which is what this helps with

view this post on Zulip Sam Mohr (Dec 26 2024 at 22:48):

But I'm planning on pushing for some "infer type holes" LSP code action that means you can throw a func : List(U64) -> _ on top and then fix the underscore later

view this post on Zulip Joshua Warner (Dec 27 2024 at 00:50):

There are two things that I find non-ideal about out-of-line type annotations:

view this post on Zulip Joshua Warner (Dec 27 2024 at 00:51):

That said, 99% of my experience comes from languages with in-line type annotations, so maybe I just haven't given out-of-line a fair shake.

view this post on Zulip Joshua Warner (Dec 27 2024 at 00:51):

At this point, I'd be strongly in favor of switching to inline type annotations.

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

I think out-of-line is a really valuable tool for focus in a language based around pure functions

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

Hmm, say more?

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

a really common thing that would happen when I would talk to Evan about API design is that I'd start explaining how everything would fit together, and he'd stop me and say "just tell me the type"

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

here's an example, pulled at random from some code I'm writing right now:

Level, (Level, Str => {}) -> Logger

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

this is the type of a function

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

can you guess what it does just from the type, without knowing the names of any of the arguments or of the function itself?

view this post on Zulip Joshua Warner (Dec 27 2024 at 00:55):

Yep! Fair point

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

that's what I mean about it being a valuable focus tool - I think it's not only helpful to think in terms of the types alone, it's actually the most helpful way to think about how to put a program together

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

like "what are the inputs, what are the outputs"

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

and in the case of -> vs => the binary of "does it produce side effects or not?"

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

and the thing I don't like about inline annotations is that they make it harder to see the "shape" of functions

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

which I think is the really important part - what goes in, what comes out, etc.

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

I think this is less true in a language with unrestricted side effects, because so many functions are like "well yes, but also there are inputs that aren't mentioned here - namely that this goes and reads from the database, but you'd have no way of knowing that by looking at the type!"

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

so I can't really think about programs in quite the same way, or at least I can't focus on the types in the same way (in, say, Rust compared to Roc or Elm or Haskell)

view this post on Zulip Joshua Warner (Dec 27 2024 at 00:59):

As a counter-point, here's a rough translation of the type of a (rust!) function I was working on recently. I'd still consider this a valid-ish example, since this function is written in a functional style (no mutation of args)

Bump, State, (EExpr, Position -> e), ExprParseOptions, u32, Loc(List CommentOrNewline),  (Position -> e)  -> ParseResult (List (SpacesBefore (LocStmt))) e

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

yeah I mean in Roc I'd prefer to put that in a record

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

once the number of arguments gets past a certain point, I'm increasingly likely to do that

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

I'd do it more often in Rust except Rust doesn't have anonymous records, which is annoying :stuck_out_tongue:

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

but if you think about it, a Rust function with inline annotations looks a lot like a Roc function that takes a record

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

Ahhh!

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

except that the Roc one has an extra { and } around the args

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

(at least the type)

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

and then I specify the arguments with labels instead of positionally - which I actually prefer once there are that many args :big_smile:

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

Yep yep, that makes sense

view this post on Zulip Eli Dowling (Dec 27 2024 at 06:10):

Richard Feldman said:

he'd stop me and say "just tell me the type"

I'd like to push back on this. I think this attitude can also be what leads to FP languages having very beginner-unfriendly APIs.

Like yeah I think I can figure out what that is, but if you instead had showed

createLogger= | minLevel:Level, printer: Level, Str => {} |: Logger
    crash

It would be way faster for me to understand.

I find the "just look at the function type" is a trap people can get into when they're already in the domain and know what things are supposed to do. I think it can actually be quite opaque to newcomers. Particularly coming from languages with less emphasis on expressive type systems

A great example of this is that language servers for functional languages often don't include function parameter names in its type signature when hovering. This is fine if you "just read the type". I think it's crap, makes it harder to figure out what a function does and is strictly worse.

I've certainly used functional libraries that just absolutely atrocious parameter names for their functions and I'm sure coming from Haskell you have too. I worry demphasising the importance of parameter names can lead to that.

All of that is significantly why I'm strongly pro inline types.

view this post on Zulip Jasper Woudenberg (Dec 27 2024 at 08:14):

I'd like to push back on this. I think this attitude can also be what leads to FP languages having very beginner-unfriendly APIs.

I know the kind of APIs you refer too, I've definitely seen that in Haskell. But I don't think that sort of design sensibility is inherent to doing type-driven API design, or it would have shown up in Elm as well.

I find the "just look at the function type" is a trap people can get into when they're already in the domain and know what things are supposed to do. I think it can actually be quite opaque to newcomers. Particularly coming from languages with less emphasis on expressive type systems

This makes total sense to me when we're talking about documentation for packages/platforms. Types are not enough, there should be good doc comments with examples as well.

For designing new APIs I find using type annotations to sketch out ideas super useful, when I'm working by myself or in groups.

view this post on Zulip Eli Dowling (Dec 27 2024 at 12:07):

Just to be clear I'm definitely not suggesting sketching out APIs with just bare functions without any implementation is a bad idea.

I'm more suggesting I think also having parameter names is either neutral or better than just types.

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

Strong agree here

view this post on Zulip Eli Dowling (Dec 27 2024 at 12:10):

And also that culturally, thinking just types is enough, i think, can lead to less understandable APIs
Maybe not in elm, but certainly in other FP languages I've read code in.

view this post on Zulip Jasper Woudenberg (Dec 27 2024 at 12:15):

Yeah, that's a good way to put it. Elm doesn't really have the culture of types being enough in the broad sense, that might explain part of the difference with Haskell.

view this post on Zulip Eli Dowling (Dec 27 2024 at 12:18):

Also, to provide a concrete example.
If Richard showed me this logger builder function signature:

Level, (Level, Str => {}) -> Logger

I would immediately ask for parameter names.

The first parameter is probably a log level cutoff right?
But I don't know that, maybe it's a minimum level, maybe a maximum, maybe the logger will only log at that level. Sure I can make an assumption, but in that case a parameter name would immediately make our communication more clear

view this post on Zulip Eli Dowling (Dec 27 2024 at 12:19):

Or maybe I'm wrong about that entirely which would be hilarious....

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

you correctly inferred what the types meant :big_smile:

view this post on Zulip Georges Boris (Dec 27 2024 at 14:38):

I'm +1 that adding some param names is in the worst, neutral.

logger : | threshold : Level, handler : ( Level, Str => {} ) | -> Logger

logger : Level, ( Level, Str -> {} ) -> Logger

IMO the top level one is much clearer about the intents... if defining just that (no implementation) still works as usual, I'm mostly onboard with inline type annotations.

I think it loses readability on really small scenarios with implementation:

sum : | a : Num, b : Num | -> Num = a + b

# vs

sum : Num, Num -> Num
sum = | a, b | a + b

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

Just did a little thought experiment here - maybe it should be in its own thread but here it is.

Premises

# Impl. only

sum = | a, b | a + b

# Type only

sum = | a: Num, b: Num | -> Num

# Type + Impl

sum : | a: Num, b: Num | -> Num
sum = | a, b | a + b

Experiment 1:

To prevent redudancy between type params and impl. params, we could allow "borrowing" them from the type annotation:

sum : | a: Num, b: Num | -> Num
sum = || a + b

P.S.
This syntax is used on nushell, which utilizes a similar syntax to the new lambda. It allows an argument to be omitted and used directly, like so:

ls | each {|| get name}

# vs

ls | each {|file| file | get name}

Experiment 2:

By itself I don't like the redundant type + impl names but it does allow the implementation of a feature present in Gleam that is sometimes quite nice:

# Named type parameters (public names different from implementation names)

replace : | in: Str, each: Str, with: Str | -> Str
replace = | string, pattern, substitute |
    # ...


good_language_name =
    "rocq".replace(each: "q", with: "")


divide : | dividend: Num, divisor: Num | -> Num
divide = | x, y | x / y

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

Georges Boris said:

I'm +1 that adding some param names is in the worst, neutral.

it's definitely not neutral; it would mean there are two ways to do the same thing, and they could disagree in ways that cause new compiler errors :big_smile:

view this post on Zulip Anthony Bullard (Dec 27 2024 at 15:12):

I think our LSP should do a nice job of formatting the signature to match up the arg names with the same element in the signature. Think:

replace :  Str,    Str,     Str         -> Str
replace = |string, pattern, substitute|

Or something of the sort

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

I'd also only be in favor if we long-term ended up with a single annotation style, inline or out-of-line

view this post on Zulip Anthony Bullard (Dec 27 2024 at 15:12):

Not suggesting this should be the normal formatted presentation

view this post on Zulip Anthony Bullard (Dec 27 2024 at 15:12):

Only for "Hover" operation in LSP

view this post on Zulip Anthony Bullard (Dec 27 2024 at 15:13):

Doesn't seem like anyone likes that idea :rofl:

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

How about

replace :
    Str, # string
    Str, # pattern
    Str  # substitute
    -> Str

view this post on Zulip Anthony Bullard (Dec 27 2024 at 15:16):

Or with Richard's example

createLogger :
    Level,            # min_level
    Level, Str => {}  # print_fn
    -> Logger

view this post on Zulip Brendan Hansknecht (Dec 27 2024 at 16:36):

I think people miss that if you need arg names, you can wrap in a record

view this post on Zulip Brendan Hansknecht (Dec 27 2024 at 16:36):

Otherwise, types can remain simple without names

view this post on Zulip Brendan Hansknecht (Dec 27 2024 at 16:37):

While naming can add clarity in some cases, it also bloats the signature and removes a level of thinking. In functional languages lambdas are common. You don't level the parameters of a nested lambda

view this post on Zulip Brendan Hansknecht (Dec 27 2024 at 16:38):

Being able to think in lambdas and how they map together is important. Parameters are noise to that form of thinking

view this post on Zulip Brendan Hansknecht (Dec 27 2024 at 16:40):

Personally, I feel like records are enough for this. If a function is complete enough, that names are wanted in the type, a record can be used. If it is not so complex, regular args can be used. The entire different in syntax is an extra set of {}

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

I agree that the solution for most of our woes in Rust functions with too many args is to put everything in a record to force readability at the callsite

view this post on Zulip Brendan Hansknecht (Dec 27 2024 at 16:43):

I also think this pairs well with |...| -> ... syntax for lambda types:

create_logger : |{ min_level: Level, print_fn: |Level, Str| => {}}| -> Logger
create_logger = |{ min_level, print_fn }|
    ...

view this post on Zulip Brendan Hansknecht (Dec 27 2024 at 16:44):

Sam Mohr said:

I agree that the solution for most of our woes in Rust functions with too many args is to put everything in a record to force readability at the callsite

This doesn't make sense to me. Rust has inline type annotations and parameters names. If this issue still arises in rust, then inline types don't solve the problem.

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

Rust doesn't have named fields, which I think is a problem at the call site with complex call sites. What does true do if it's the 6th arg? That is separately solved by named args, or records, which function like named args

view this post on Zulip Brendan Hansknecht (Dec 27 2024 at 17:11):

Oh, that is a totally different question. I misunderstood. All the rest of this discussion has been about the definition site.

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

Yep, I should have said "as an aside..."

view this post on Zulip Brendan Hansknecht (Dec 27 2024 at 17:13):

Most large rust/c++ code bases I have work in just do /*arg_name=*/true to work around that for single problematic args. Roc uses tags to mostly work around that.

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

tags can work, and inlay type hints also work for Rust/C++

view this post on Zulip Anthony Bullard (Dec 27 2024 at 18:57):

And then what happens next is "all your args in a record" becomes a pattern, and someone asks for sugar for that(with PNC), and then....we have named args

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

I'd be totally okay with that

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

Though if we can get away with fewer features and not needing this, I'd love that

view this post on Zulip Anthony Bullard (Dec 27 2024 at 19:01):

That was kind of my point - if we can use tooling to obviate the need for wrapping args needlessly in a record and then going down that language evolution path, we can avoid this.

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

Anthony Bullard said:

That was kind of my point - if we can use tooling to obviate the need for wrapping args needlessly in a record and then going down that language evolution path, we can avoid this.

I don't understand. Can you... reword? sorry

view this post on Zulip Anthony Bullard (Dec 27 2024 at 21:16):

Sorry Sam, what I meant is if the tooling makes having 3+ args ergonomic enough - then there won't develop a convention of wrapping args in records. And then there won't be asks for the named arg sugar.

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

Ah, sure

view this post on Zulip Eli Dowling (Dec 27 2024 at 23:27):

Is there a performance implication to wrapping args in records?

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

I'd say no ish

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

If the record is owned in that it's only used once, then it's basically a list of values

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

If it's used multiple times and not just destructed, then it gets recounted, meaning it's heap allocated

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

Which is a worthwhile tradeoff for readability in a high-level language like Roc

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

actually records never get refcounted

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

Oh?

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

Good to know. I'll have to properly read the refcount code sometime

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

yeah the only things that get refcounted are List, Str, Set, Dict, Box, and recursive tag unions

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

everything else is stack-allocated

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

also - having a function take a record doesn't just affect the type, it also means you call it with "named arguments"

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

so having editor tooling show names associated with types doesn't necessarily make it easier to spot when a function that takes 3 strings is taking them in the wrong order

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

(whereas if you make it take a record, then they're labeled at the call sites)

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

you can have editor tooling always label all args, but that can get noisy

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

also, although it's not something we generally do in Roc, it is technically possible to create functions using pointfree function composition, at which point the editor would show actively unhelpful or confusing names for the arguments (probably a and b or arg1 and arg2)

view this post on Zulip Brendan Hansknecht (Dec 28 2024 at 00:14):

Eli Dowling said:

Is there a performance implication to wrapping args in records?

Technically yes, in practice, probably not

view this post on Zulip Brendan Hansknecht (Dec 28 2024 at 00:26):

To be specific, the highest cost would come from exceptionally small recursive functions that wrap args in a record only to immediately unwrap them in the function body.

Also, probably would have to be an exceptionally small non-tail recursive function. If it is tail recursive, the transformation to a loop should enable it to optimize away the record I think.

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

But I would have to double check in specific cases to see if it has any meaningful affect on performance.

view this post on Zulip Richard Feldman (Dec 28 2024 at 00:37):

if we really wanted to, we could theoretically do a special-case optimization where if you destructure the record right away in the function body, we compile it to exactly the same representation as if there were no record, but it probably wouldn't even be a noticeable performance difference if we did that :stuck_out_tongue:

view this post on Zulip Pit Capitain (Dec 29 2024 at 12:15):

Georges Boris thought element written slightly differently:

# Impl. only

sum | a, b | = a + b

# Type only

sum | a: Num, b: Num | -> Num

# Type + Impl

sum | a: Num, b: Num | -> Num
    = a + b

I added a line break in the last "Type + Impl" case just to show the relation to #ideas > Redundant name in type annotations . If you have the names in the function type there's no need to repeat anything for the implementation.

view this post on Zulip Georges Boris (Dec 29 2024 at 12:33):

I think the third example gives a false sense of readability due to small function name + simple types. I tried to test it on a slightly more complicated function + longer name and put it side by side with the "current" syntax:

lets_pretend_walk_has_a_long_name : |
  list : List elem,
  state : state,
  acc : (state, elem -> state)
| -> state =
  when list is
    [] -> state
    [ h, ..tail ] -> walk tail (acc state h) acc

vs

lets_pretend_walk_has_a_long_name :
    List elem,
    state,
    (state, elem -> state)
    -> state
lets_pretend_walk_has_a_long_name =
  | list, state, acc |
    when list is
      [] -> state
      [ h, ..tail ] -> walk tail (acc state h) acc

view this post on Zulip Dawid Danieluk (Dec 29 2024 at 13:52):

Tbh first one looks great. I'm probably too used to inline types, but I really like the fact that if I'd go to 'acc' definition I have it's type right next to it.
If I get this right the argument for out of line types is that it helps to think about function in term of types right?
Can't both be achieved via some 'smart' formatting?

lets_pretend_walk_has_a_long_name : |
  list:    List elem,
  state:   state,
  acc:     (state, elem -> state)
| -> state =
  when list is
    [] -> state
    [ h, ..tail ] -> walk tail (acc state h) acc

One columns for arguments, one column for their types (golang does similar formatting for struct tags).
Personally I think that inline types with that kind of formatting are less noisy than out of line types (fn name specified just once, you can look at either variable names, types or both easily - for var names read first column, for types second column, for both go line by line). Also - bonus points for extra dopamine rush you get when formatter does it's job and your ugly spaghetti mess turns into two perfectly formatted columns.
With out of line types I think you could quickly find yourself jumping between types and variables (especially without LSP support f.e. during PR code reviews).

view this post on Zulip Sky Rose (Dec 29 2024 at 22:05):

A problem with using records for named arguments is that you have to duplicate all the argument names.

I also really like the first version of lets_pretend_walk_has_a_long_name where the names and arguments are right next to each other.


Last updated: Jun 16 2026 at 16:19 UTC