Stream: ideas

Topic: ✔ Static Dispatch - without nominal typing?


view this post on Zulip Anthony Bullard (Dec 28 2024 at 13:52):

I was reading through the static dispatch proposal for the 6th time, and I started having the thought - a thought I had a couple of years ago developing my own language - couldn't we have static dispatch that considers any _in scope_ top-level function that takes this type as it's first argument?

See #ideas > Static Dispatch - without nominal typing? @ 💬 below to see an example of the workflow.

The biggest disadvantage is that it may mean that abilities may have to stick around / have changes

view this post on Zulip Anthony Bullard (Dec 28 2024 at 13:54):

I think with having the requirement that the function would have to be top-level and available unqualified it could work. Though it would (I'm assuming) _not_ allow for ability-like polymorphism (unless all callers and all implementations of the members are defined in the same module)

view this post on Zulip Joshua Warner (Dec 28 2024 at 13:54):

This also resolves the weirdness around .pass_to(...) / .> / etc (which I like)

view this post on Zulip Anthony Bullard (Dec 28 2024 at 13:55):

I'll try to cook up an example later, but I have dentist appointment I need to get ready for

view this post on Zulip Georges Boris (Dec 28 2024 at 15:53):

One thing I dislike about this idea is how moving a piece of code across modules may not only make it break, but also make it behave differently.

view this post on Zulip Joshua Warner (Dec 28 2024 at 16:24):

That's true regardless, for approximately the same reasons. Your source/target module can have different things imported under the same name (or even defined locally!), and copying code between those can of course change semantics then

view this post on Zulip Brendan Hansknecht (Dec 28 2024 at 16:25):

Yeah, I have thought about this a few times. Technically speaking, we could make it so that .name( first checks static dispatch, then checks anything imported locally.

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

Like there has never been a need for a .pass_to or .> technically. We could just have an extra step in name resolution.

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

The main disadvantage to something like that is that there are now more complex rules someone must know to figure out which function is called. Every time I imagine hitting a conflict, I find .pass_to or .> to be more reasonable.

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

We'd just need a compiler error if there are two functions with the same name. We could just use the custom type's function first, but that seems like an easy footgun

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

If you don't use the custom types function first, that will get painful. Suddenly, you have to import every list function and pollute your namespace cause your module uses lists. As a prime example, think of defining a dictionary. It both uses tons of functions from list and exposes tons of functions with the same names.

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

Not to mention also using result.map which has the same name as list.map.

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

Im pretty sure in the original thread I linked what d lang does around this as a reference to resolution rules. D lang has a more complex picture, but we would hit similar issues.

view this post on Zulip Anthony Bullard (Dec 28 2024 at 16:46):

If you had a function that only uses functions shared between both modules (and available in scope) become polymorphic instead if all of those functions were otherwise the same in shape?

view this post on Zulip Georges Boris (Dec 28 2024 at 16:46):

Joshua Warner said:

That's true regardless, for approximately the same reasons. Your source/target module can have different things imported under the same name (or even defined locally!), and copying code between those can of course change semantics then

I thought static dispatch was limited to functions defined on the module that defined the type, no? Do you mean something like a common type name that is defined in different modules?

e.g. Store - lets say I have a Store defined in a user/store.roc module and a Store defined in a post/store.roc module. In a particular module that uses both I'm always qualifying their usages. How would that + static dispatch possibly cause errors?

view this post on Zulip Georges Boris (Dec 28 2024 at 16:48):

I don't know if I'm seeing problems where there is none - but I still think that the edge case of similar named types is way more manageable than inferring function call based on scope precedence.

I can see things getting really unweildy in this scenario.

view this post on Zulip Anthony Bullard (Dec 28 2024 at 16:51):

@Georges Boris This is the reason why I said the local functions would have to be available _unqualified_ in the top-level of the current module.

Some have suggested allowing searching other Modules in scope for would-be qualified functions. I think that will lead to a lot of issues (and some intriguing possibilities as well to be fair - but I think the issues outweigh the advantages).

view this post on Zulip Anthony Bullard (Dec 28 2024 at 16:52):

It would literally just be syntax for "treat this expr as the first argument in the proceeding function"

view this post on Zulip Anthony Bullard (Dec 28 2024 at 16:53):

With the only caveat being static dispatch.

view this post on Zulip Anthony Bullard (Dec 28 2024 at 16:54):

Which I _could_ argue should not be done the way it is proposed (by having the compiler do some minor magic during compilation), but instead use this methodology and have the LSP/tooling which we are relying on for the DX of static dispatch anyway add the imports as expected when you autocomplete.

view this post on Zulip Anthony Bullard (Dec 28 2024 at 16:56):

So I have an expr, type . and then the LSP would say "OK, what is available locally that has this type as the first arg, then (if it is nominal/custom type) what is not exposed that could work here, and then what is available in other modules already imported ...." When you choose one it would add the function name, parens, and arg templates as normal, but also decide how to expose it in the file.

view this post on Zulip Georges Boris (Dec 28 2024 at 16:57):

Yeah - I can see that. When I first heard about static dispatch this is how I first imagined it working tbh. It seems quite powerful. But it opens up the problem tremendously.

view this post on Zulip Anthony Bullard (Dec 28 2024 at 17:06):

So I have this file:

module [foo]

len = |s| s.count_utf8_bytes

foo = |a, b|
    "Hello"

Then I type "."

I get autocomplete for these items

I type re it gets filtered down to Str.replaceEach, Str.replaceFirst, Str.replaceLast.
I hit enter with Str.replaceEach selected.

My module then becomes

module [foo]

import Str exposing [replaceEach] # this is done by LSP

len = |s| s.count_utf8_bytes

foo = |a, b|
    "Hello, world!".replaceEach(original, new)
                              # ^ this is highlighted in most editors with LSP support

I fill in the args with the correct values, and then after the ) I type . again, and get the following list of autocomplete

I type fi and get a autocomplete list that's filtered to replaceFirst. I press Enter and then my module looks like this:

module [foo]

import Str exposing [replaceEach, replaceFirst] # this is done by LSP

len = |s| s.count_utf8_bytes

foo = |a, b|
    "Hello, world!!!!".replaceEach("!", a).replaceFirst(original, new)
                                                      # ^ this is highlighted in most editors with LSP support

view this post on Zulip Anthony Bullard (Dec 28 2024 at 17:07):

@Georges Boris What are the problems it opens?

view this post on Zulip Brendan Hansknecht (Dec 28 2024 at 17:07):

Anthony Bullard said:

Some have suggested allowing searching other Modules in scope for would-be qualified functions. I think that will lead to a lot of issues (and some intriguing possibilities as well to be fair - but I think the issues outweigh the advantages).

I think it would have to be fully imported or addressed in a qualified form.
some_structural_object.MyModule.do_thing(...)
Should be able to also just work

view this post on Zulip Brendan Hansknecht (Dec 28 2024 at 17:07):

Though maybe the dot qualifier is a little strange in that case

view this post on Zulip Anthony Bullard (Dec 28 2024 at 17:08):

Yeah, in my language I was exploring using something different like : or :: or ->

view this post on Zulip Anthony Bullard (Dec 28 2024 at 17:08):

But I think . works

view this post on Zulip Anthony Bullard (Dec 28 2024 at 17:08):

Since we already have record accessors

view this post on Zulip Brendan Hansknecht (Dec 28 2024 at 17:08):

I totally agree that any form of search for non-imported things is a mistake (past nominal type exported functions)

view this post on Zulip Anthony Bullard (Dec 28 2024 at 17:09):

I think dropping the nominal-focused thing at all for all but (LSP) search preference might be a path worth exploring

view this post on Zulip Brendan Hansknecht (Dec 28 2024 at 17:09):

What happens when you use a dict, list, and record which all require .map

view this post on Zulip Anthony Bullard (Dec 28 2024 at 17:11):

So that is an interesting thing to think about. One path is that the LSP could come up with a conventional way to namespace the later-introduced conflicting members in some minimal way (which you could refactor to your liking later).

view this post on Zulip Anthony Bullard (Dec 28 2024 at 17:11):

Like if I introduced len in my module for List, and then use it for Dict, it could use dict_len

view this post on Zulip Anthony Bullard (Dec 28 2024 at 17:12):

Or even prompt you to choose a name (Maybe this is terrible???)

view this post on Zulip Anthony Bullard (Dec 28 2024 at 17:13):

Does some_dict.dict_len() look better than some_dict.>Dict.len()? I think so

view this post on Zulip Anthony Bullard (Dec 28 2024 at 17:13):

But it also keeps the ability to know where EVERYTHING is coming from by reading the file.

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

I'm going to refine the first post of this topic to make it clear EXACTLY what I'm proposing and include this part.

view this post on Zulip Anthony Bullard (Dec 28 2024 at 17:21):

And for this to be a proper proposal, I'd have to write a proper Google doc and think through it a LOT more. I know Richard put a ton of thought into his proposal

view this post on Zulip Brendan Hansknecht (Dec 28 2024 at 17:29):

Yeah, definitely take the time you need and feel free to explore in public if you want (though definitely understand that a single proposal is much easier to discuss than public meandering).

view this post on Zulip Brendan Hansknecht (Dec 28 2024 at 17:32):

I think something like this definitely could work, I think it likely will want to pair with static dispatch and be a solution for structural types rather than a full replacement for static dispatch, but feel free to explore either.

view this post on Zulip Brendan Hansknecht (Dec 28 2024 at 17:34):

Like one very limited form of method dot syntax would be to simply take |> today and replace it with a single . and paren calling syntax. That is much more verbose in the default case for static dispatch, but it is always locally imported or qualified. Could also use import renaming to deal with conflicts.

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

so I thought about this originally, but I think it's the wrong direction for a few reasons

view this post on Zulip Richard Feldman (Dec 28 2024 at 17:38):

first, let's compare to something like #ideas > static dispatch - pass_to alternative

view this post on Zulip Richard Feldman (Dec 28 2024 at 17:38):

in that design, we have something like:

foo.(bar)

here, we have:

foo.bar()

view this post on Zulip Richard Feldman (Dec 28 2024 at 17:39):

a crucial difference is that in the second design, every single time you see foo.bar() forever, you no longer know what that means without checking what's in scope

view this post on Zulip Richard Feldman (Dec 28 2024 at 17:39):

this, to me, is such a massive downside it's basically inconceivable to me that anything in this direction could possibly be a better design than something that's just different syntax sugar than |> :sweat_smile:

view this post on Zulip Richard Feldman (Dec 28 2024 at 17:40):

second, this encourages things like Rails's Active Support

view this post on Zulip Richard Feldman (Dec 28 2024 at 17:40):

where you can do things like str.my_custom_thing() as long as you make sure to import my_custom_thing() everywhere

view this post on Zulip Richard Feldman (Dec 28 2024 at 17:40):

I also think this is a downside

view this post on Zulip Richard Feldman (Dec 28 2024 at 17:41):

so many people use Rails (and Active Support) in the Ruby community, it's actually common that people aren't sure which things are part of the stdlib and which are part of Active Support

view this post on Zulip Richard Feldman (Dec 28 2024 at 17:41):

I definitely don't think that is a healthy dynamic for an ecosystem

view this post on Zulip Richard Feldman (Dec 28 2024 at 17:41):

especially because in a lot of cases, I think the way people extend the stdlib ends up being a mistake

view this post on Zulip Richard Feldman (Dec 28 2024 at 17:44):

as an example, there is Elm's (third-party) List-Extra package, which includes things like intercalate and interweave, neither of which should be confused with the actual stdlib List's intersperse

view this post on Zulip Richard Feldman (Dec 28 2024 at 17:45):

I think it's better if only the builtin modules can provide methods on the builtin types, and if you're doing something third-party, you know it because the syntax is different

view this post on Zulip Richard Feldman (Dec 28 2024 at 17:48):

third, this relies on functions being unqualified and in scope, which means that if you happen to have two functions named foo from different modules, the only way to use this syntax with both of them in the same pipeline is to import and rename one or both of them with as, which is less ergonomic than using the usual way of solving this problem (namely, qualifying the module - e.g. .(Foo.bar)) - which is what the |> alternatives do

view this post on Zulip Richard Feldman (Dec 28 2024 at 17:48):

anyway, sorry for the long dump of negativity, but I don't think this is the direction we should go, and wanted to lay out why :big_smile:

view this post on Zulip Anthony Bullard (Dec 28 2024 at 18:00):

I think there’s a chance you aren’t fully understanding my proposal

view this post on Zulip Anthony Bullard (Dec 28 2024 at 18:00):

It’s not like active support at all, which uses duck typing

view this post on Zulip Anthony Bullard (Dec 28 2024 at 18:00):

Everything you use is available under the exact name in the module where it is used

view this post on Zulip Anthony Bullard (Dec 28 2024 at 18:01):

And seems to me at least to be way less magical feeling and hard to track than nominal-based static dispatch

view this post on Zulip Anthony Bullard (Dec 28 2024 at 18:02):

Really it’s what Brendan said, using . as a replacement for pizza and having the LSP do fancier things for LSP off of it

view this post on Zulip Anthony Bullard (Dec 28 2024 at 18:02):

And that’s all

view this post on Zulip Anthony Bullard (Dec 28 2024 at 18:03):

But maybe you understood all of that and we are talking past each other. Wouldn’t be a first for me!

view this post on Zulip Brendan Hansknecht (Dec 28 2024 at 18:09):

Anthony Bullard said:

But maybe you understood all of that and we are talking past each other. Wouldn’t be a first for me!

It definitely happens (which is why larger written out proposal can be so helpful)

view this post on Zulip Richard Feldman (Dec 28 2024 at 18:51):

if I understand the idea correctly, I can write:

blahblah = |arg| # ...

something.blahblah()

for this to work, blahblah has to be in scope, either because I defined it, or because I imported it using exposing from a module. But it does Just Work; I don't need to have something be a custom type with blahblah in the same module where that custom type was defined.

view this post on Zulip Richard Feldman (Dec 28 2024 at 18:51):

do I have that right, or did I miss something? :big_smile:

view this post on Zulip Georges Boris (Dec 28 2024 at 18:58):

My 2c is that static dispatch can be a rats nest of implicitness unless the constraints are extremely clear... IMO the more constraints the better in this case, to avoid any questions like "Where is this coming from?"

So the proposal of only the module that defines the type can provide its methods seems like a clear way of structuring things...

And if by some reason I want to create my own version of Dict with extra methods, I can create a module for that, with its own type wrapping the builtin Dict and providing its own API with no limitations... (this is not the same as List.Extra, more in the lines of BiDict)

view this post on Zulip Georges Boris (Dec 28 2024 at 18:59):

(I absolutely hated the feeling of working on a Rails application due to how not clear a call path was for basically any function...)

view this post on Zulip Anthony Bullard (Dec 28 2024 at 20:11):

Richard Feldman said:

do I have that right, or did I miss something? :big_smile:

To be clear my statement above was most likely due to my poor communication skills and lack of time put into presenting the proposal. It came to my brain and I wanted to put it out there and get some early feedback. Should probably wait in the future and put out something with more polish

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

haha no worries!

view this post on Zulip Richard Feldman (Dec 28 2024 at 20:21):

the first rule of #ideas is that you can always feel free to share an idea in it

view this post on Zulip Richard Feldman (Dec 28 2024 at 20:21):

doesn't have to be polished or fully formed

view this post on Zulip Anthony Bullard (Dec 28 2024 at 20:22):

Thanks for that.

view this post on Zulip Anthony Bullard (Dec 28 2024 at 20:23):

I think I will try to implement this idea in a rag-tag sort of way as soon as PNC lands

view this post on Zulip Anthony Bullard (Dec 28 2024 at 20:24):

Just because I think it won’t be that hard and it’s something that needs to be used in anger to get a feel for it

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

up to you, but fair warning - I think if I understand the idea correctly it has almost no chance of making it into the language :sweat_smile:

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

(if I don't understand it correctly, who knows?)

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

It basically is the current language, the only change being |> becoming .

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

And changes to the LSP

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

I guess that’s why I don’t understand the push back. And why I’m assuming I’m not communicating it well

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

|> becoming . is the specific thing I think we shouldn't do :big_smile:

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

I mean if it doesn’t make it in - that’s fine, this is just an idea

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

fair enough! :smiley:

view this post on Zulip Anthony Bullard (Dec 28 2024 at 20:29):

I guess I’d like to understand why that’s the case - it seems to fit really well to me. I also didn’t design the language :joy:

view this post on Zulip Anthony Bullard (Dec 28 2024 at 20:31):

I see it as an extension in a way of record field accessors. Today they can be used as a function or suffixed to an expression . This just extends this to the other functions that exist for a type in scope

view this post on Zulip Anthony Bullard (Dec 28 2024 at 20:32):

But I’ll step away and focus on PNC for now

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

the main thing is that - if I understand the idea correctly - it would mean that I can no longer look at foo.bar() and know whether bar is resolving to something based on the type of foo (static dispatch) or bar being in local scope (like |>)

view this post on Zulip Anthony Bullard (Dec 28 2024 at 20:33):

Ah! Thats why I said above that this would be static dispatch - the opaque type module members would only be preferred in autocomplete but everything would be explicit

view this post on Zulip Anthony Bullard (Dec 28 2024 at 20:34):

Not that necessarily makes it sound better to you - but that’s the idea

view this post on Zulip Richard Feldman (Dec 28 2024 at 20:34):

:thinking: so is the idea to change the language server but not the compiler?

view this post on Zulip Anthony Bullard (Dec 28 2024 at 20:34):

Yes, on top of the syntax change we just discusssed

view this post on Zulip Richard Feldman (Dec 28 2024 at 20:35):

the compiler change is the only part I have a strong reaction to :big_smile:

view this post on Zulip Anthony Bullard (Dec 28 2024 at 20:35):

Since we are optimizing for a LSP/IDE workflow

view this post on Zulip Richard Feldman (Dec 28 2024 at 20:36):

another consequence which I forgot to mention above is that if foo.bar() can get barfrom local scope, it means modules exposing new functions could potentially break your build

view this post on Zulip Anthony Bullard (Dec 28 2024 at 20:36):

So everything you use . access for would be explicit in the module

view this post on Zulip Anthony Bullard (Dec 28 2024 at 20:36):

It couldn’t. They have to be available unqualified, so they are all exposed explicitly

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

but like imagine I have bar defined locally

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

such that foo.bar() Just Works

view this post on Zulip Anthony Bullard (Dec 28 2024 at 20:37):

It would still use that bar unless you renamed it and imported a new bar from another module

view this post on Zulip Richard Feldman (Dec 28 2024 at 20:38):

and now the module where foo is defined exposes something named bar

view this post on Zulip Anthony Bullard (Dec 28 2024 at 20:38):

A new bar in a different module would only change the autocomplete

view this post on Zulip Anthony Bullard (Dec 28 2024 at 20:39):

The language to the compiler is the exact same. Dot would literally just be Pizza

view this post on Zulip Anthony Bullard (Dec 28 2024 at 20:39):

But a really small, uncut pizza

view this post on Zulip Richard Feldman (Dec 28 2024 at 20:39):

ok, so to check my understanding - that means if I add len = |arg| ... to the top of my module, then every instance of getting list length in the module will silently change to use that definition of len instead?

view this post on Zulip Anthony Bullard (Dec 28 2024 at 20:40):

Well if you were using something.len() that means you already had an exposed member called len. And you would be shadowing it and would have to resolve it

view this post on Zulip Anthony Bullard (Dec 28 2024 at 20:41):

By either renaming your local or renaming (with LSP) the exposed member called

view this post on Zulip Richard Feldman (Dec 28 2024 at 20:42):

ok I'm convinced I don't correctly understand the idea :joy:

view this post on Zulip Anthony Bullard (Dec 28 2024 at 20:42):

At least we got there :joy::joy::joy::joy::joy:

view this post on Zulip Anthony Bullard (Dec 28 2024 at 20:42):

Tell you what. I will write a long form proposal tonight and share it soon. Until then we can leave this convo alone

view this post on Zulip Anthony Bullard (Dec 28 2024 at 20:43):

And maybe I should stop using the term static dispatch because I think this is a different path and the name is causing confusion

view this post on Zulip Anthony Bullard (Dec 28 2024 at 20:44):

Thanks for the engaging back and forth though! Gave me a lot of ideas for what to discuss in the proposal

view this post on Zulip Richard Feldman (Dec 28 2024 at 20:44):

sure!

view this post on Zulip Anthony Bullard (Dec 28 2024 at 20:45):

If we were side by side at a computer I could explain it in 2 minutes, but remote collab is what it is

view this post on Zulip Richard Feldman (Dec 28 2024 at 20:45):

one thing to consider: if the proposal isn't doing static dispatch anymore, what happens to the use cases filled by Abilities today?

view this post on Zulip Richard Feldman (Dec 28 2024 at 20:45):

e.g. json decoding

view this post on Zulip Anthony Bullard (Dec 28 2024 at 20:45):

Yes, I hope to address that

view this post on Zulip Anthony Bullard (Dec 28 2024 at 20:45):

And I might not be able to

view this post on Zulip Anthony Bullard (Dec 28 2024 at 20:45):

Lots of fodder for thought

view this post on Zulip Georges Boris (Dec 28 2024 at 21:01):

@Anthony Bullard how would this work in your idea?

Parser.parse(input)
|> Result.map(List.map(|x| x * 2))

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

Georges Boris said:

Anthony Bullard how would this work in your idea?

Parser.parse(input)
|> Result.map(List.map(|x| x * 2))

This won’t work with static dispatch as written, we aren’t doing partial application. That said I imagine you are referring to the two different map functions. One would be namespaced.

import Result exposing [map]
import List exposing [map as list_map]
import Parser exposing [parse]

# …
input.parse().map(|l| l.list_map(|x| x * 2))

Remember that those imports are added automatically by the LSP, but you understand where the implementation is coming from by reading the file alone.

view this post on Zulip Anthony Bullard (Dec 28 2024 at 21:20):

And if you don’t like the name the LSP chose for List.map you can just go to definition and then rename

view this post on Zulip Georges Boris (Dec 28 2024 at 21:59):

Got it! Tks.

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

Here's my rough draft proposal @Richard Feldman . It's basically a "fork" of your proposal: https://docs.google.com/document/d/1mYpihfZhbzRRbhuLM7Zf0CQOmnm_3301mz6XZGIINKc/edit?usp=sharing

view this post on Zulip Brendan Hansknecht (Dec 30 2024 at 15:40):

How do you ensure consistency when many functions might have the same name (thus only one can be imported locally without renaming)?

I think it would be bad for the roc ecosystem if in one file you see .map to mean List.map in another .list_map and in a third .lmap. I think this proposal makes that highly likely. Every conflict has to be renamed by someone.

view this post on Zulip Georges Boris (Dec 30 2024 at 15:45):

@Brendan Hansknecht I think that is the idea - as Anthony answered my similar question just above!

view this post on Zulip Anthony Bullard (Dec 30 2024 at 15:45):

It’s the same as if you didn’t want qualified names today. The LSP would give you one by default (which would be helpful for decoding where it comes from without looking at imports!) and I think most people would stick with that

view this post on Zulip Anthony Bullard (Dec 30 2024 at 15:46):

And there is no magic!

view this post on Zulip Anthony Bullard (Dec 30 2024 at 15:47):

And no reliance on nominal types

view this post on Zulip Brendan Hansknecht (Dec 30 2024 at 15:47):

Yeah, I think that is a issue of this proposal. There is no reason to use list_map over List.map

view this post on Zulip Brendan Hansknecht (Dec 30 2024 at 15:47):

That said, I think the biggest loss of this proposal is that you still have abilities and don't have implicit interfaces. I think static dispatch replacing abilities is one of the biggest selling points for me. It enables much more powerful generic functions without the explicit need for every single type to opt in. I can make a function that works on both sets and lists and any other container with a similar interface.

view this post on Zulip Anthony Bullard (Dec 30 2024 at 15:48):

I think [].list_map(…) reads cleaner than [] |> List.map(…) and is clearer than [].map(…) with no clue in the source where it comes from

view this post on Zulip Brendan Hansknecht (Dec 30 2024 at 15:49):

So does it prepend all imports always

view this post on Zulip Anthony Bullard (Dec 30 2024 at 15:49):

No, only if the name would shadow another

view this post on Zulip Anthony Bullard (Dec 30 2024 at 15:49):

Top-level name

view this post on Zulip Georges Boris (Dec 30 2024 at 15:50):

So if I move a function to another file for instance - it would also involve some effort of making sure the imports have the same aliases

view this post on Zulip Anthony Bullard (Dec 30 2024 at 15:50):

I would think the LSP would have a code action to fix that

view this post on Zulip Anthony Bullard (Dec 30 2024 at 15:50):

Unless you renamed it to something custom

view this post on Zulip Anthony Bullard (Dec 30 2024 at 15:51):

Which is the same if you use unqualified functions today

view this post on Zulip Anthony Bullard (Dec 30 2024 at 15:51):

There really is no difference to today for any situation

view this post on Zulip Brendan Hansknecht (Dec 30 2024 at 15:51):

So in one file you may get List.map as .map with Result.map as .result_map. in another you would get List.map as .list_map with Result.map as .map. in a third maybe both are renamed

view this post on Zulip Anthony Bullard (Dec 30 2024 at 15:51):

You just have a terser pipeline syntax and some LSP help

view this post on Zulip Brendan Hansknecht (Dec 30 2024 at 15:51):

Just depends on the import order when first writing the code

view this post on Zulip Anthony Bullard (Dec 30 2024 at 15:51):

Yes

view this post on Zulip Anthony Bullard (Dec 30 2024 at 15:52):

And I think that’s fine!

view this post on Zulip Anthony Bullard (Dec 30 2024 at 15:52):

You can understand in each what’s happening

view this post on Zulip Anthony Bullard (Dec 30 2024 at 15:52):

But this is all IMHO

view this post on Zulip Brendan Hansknecht (Dec 30 2024 at 15:53):

I don't see the advantage over static dispatch. If it changes on a file by file basis on is base in import order, it means I will have to guess what .map means anyway. I would rather have static dispatch and it all just be .map and based on the type than this missed world.

view this post on Zulip Anthony Bullard (Dec 30 2024 at 15:54):

Here's my motivation - and maybe I should include in the doc.

view this post on Zulip Georges Boris (Dec 30 2024 at 15:54):

Just as context - the motivation for the proposed changes was to solve how one would call/distinguish between functions defined in the type module vs other functions that would need to fallback to .(some_func) syntax, right?

view this post on Zulip Anthony Bullard (Dec 30 2024 at 15:54):

I don't want Nominal types everywhere

view this post on Zulip Brendan Hansknecht (Dec 30 2024 at 15:54):

To me, the main advantage of this proposal is to allow it to work with all functions not just functions from the defining module.

view this post on Zulip Anthony Bullard (Dec 30 2024 at 15:54):

I really, really, really don't want nominal types everywhere

view this post on Zulip Anthony Bullard (Dec 30 2024 at 15:55):

And yes, not having a different syntax for piping to normal functions

view this post on Zulip Brendan Hansknecht (Dec 30 2024 at 15:55):

I agree with that sentiment, but I really feel like I need to see how it plays out in practice first.

view this post on Zulip Anthony Bullard (Dec 30 2024 at 15:55):

And I don't like opaque compiler magic

view this post on Zulip Anthony Bullard (Dec 30 2024 at 15:56):

That's fair

view this post on Zulip Anthony Bullard (Dec 30 2024 at 15:56):

And by opaque I mean textually opaque.

view this post on Zulip Anthony Bullard (Dec 30 2024 at 15:57):

I don't mind tools helping us write - that's great!. But I do mind having to have tools to read code effectively. You do that so much more than write

view this post on Zulip Anthony Bullard (Dec 30 2024 at 15:58):

See yes if sometimes I see list_map that's fine - it's probably clearer what it is. And if I see .map I can say "Hey, is that List or Result or Dict or Set .map? Oh there it is in the imports, it's List.map!"

view this post on Zulip Georges Boris (Dec 30 2024 at 15:58):

Just as a con for the approach - I'm not a fan of relying on LSP code actions for making language features to be ergonomic.

view this post on Zulip Anthony Bullard (Dec 30 2024 at 15:58):

This isn't really a language feature

view this post on Zulip Anthony Bullard (Dec 30 2024 at 15:58):

This is the current language

view this post on Zulip Anthony Bullard (Dec 30 2024 at 15:59):

The LSP is just enabling a more concise way of writing that will look nice in some contexts

view this post on Zulip Georges Boris (Dec 30 2024 at 15:59):

Well, yeah it is right. Since you're proposing for . to mean the same as |>

view this post on Zulip Brendan Hansknecht (Dec 30 2024 at 15:59):

I agree that a .map call can be ambiguous, but I feel like reading code in the static dispatch world will be like reading code in so many common statically typed languages today. I don't find that hard, but maybe it is still harder than reading qualified dispatch.

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

This is a case where seeing list once in pipeline is nice, but seeing it 5 times in a row is just a waste.

view this post on Zulip Georges Boris (Dec 30 2024 at 16:00):

For me, static dispatch needs to be really explicit so I know what I'm reading with no guessing based on context.

view this post on Zulip Anthony Bullard (Dec 30 2024 at 16:01):

And most if not all usages of |> binop becoming .as part of an accessor chain (needs to be tight against RHS expr)

view this post on Zulip Brendan Hansknecht (Dec 30 2024 at 16:01):

Explicit how? Explicit that it is static dispatch and thus you need to know the type to know what it is doing?

view this post on Zulip Anthony Bullard (Dec 30 2024 at 16:01):

That's my problem with static dispatch - it's not explicit

view this post on Zulip Anthony Bullard (Dec 30 2024 at 16:02):

And when it is explicit, it's because you know the API of the implementing module of the nominal type

view this post on Zulip Anthony Bullard (Dec 30 2024 at 16:03):

And this - I fear - will drive more people to wrap types in Custom types / Opaque Refs (whatever our nominal typing mechanism).

view this post on Zulip Georges Boris (Dec 30 2024 at 16:03):

It was my original concern, but since learning there was a big constraint of "only the type module can define methods" I felt like it became explicit enough for me at least.

view this post on Zulip Anthony Bullard (Dec 30 2024 at 16:03):

I know Richard has said he's not worried about it - but I am. I believe in "If you give a mouse a cookie" very deeply

view this post on Zulip Anthony Bullard (Dec 30 2024 at 16:04):

I guess there's some explicitness if you have type annotations

view this post on Zulip Anthony Bullard (Dec 30 2024 at 16:04):

If every function had to have type annotations, then I think I would be less opposed.

view this post on Zulip Brendan Hansknecht (Dec 30 2024 at 16:04):

I think you misunderstand. I think Richard fully expects more nominal types than we currently have opaque. I think he is not worried about the ramifications of that.

view this post on Zulip Anthony Bullard (Dec 30 2024 at 16:05):

Ah, ok. I misunderstood his position. I think we are just a step away then from "Functional Go" if that comes to be true.

view this post on Zulip Georges Boris (Dec 30 2024 at 16:06):

I'm not following your concern Anthony, do you mean you think the constraint of methods being limited to type modules will enforce people to extend interfaces by constantly wrapping these modules into other modules?

E.g. to extend List, I would create MyList and just expose all public methods of List + my own extended set of methods?

view this post on Zulip Brendan Hansknecht (Dec 30 2024 at 16:06):

Yeah, would be a lot closer to a functional go for sure.

view this post on Zulip Anthony Bullard (Dec 30 2024 at 16:06):

If I get "method syntax" by moving a types and it's functions to a different module and wrapping it in a Custom type, I'll probably do it

view this post on Zulip Anthony Bullard (Dec 30 2024 at 16:07):

I want my record type to have these cool methods!

view this post on Zulip Georges Boris (Dec 30 2024 at 16:07):

And is that what you mean by nominal typing? (Sorry for my ignorance, not used to the term in this context)

view this post on Zulip Anthony Bullard (Dec 30 2024 at 16:08):

Yes, Custom/Opaque types here means "I know exactly where this type and the functions that can work on it are defined", which is close enough to Nominal (It's structure is hidden, so only the Name is known)

view this post on Zulip Brendan Hansknecht (Dec 30 2024 at 16:08):

I don't expect to see tons of wrapping of other types like list (though I'm sure those form of extensions will happen). I do expect a lot of structural types that probably should just be bags of data turned into nominal types just to get static dispatch

Nominal type -> new name for opaque types but maybe slightly different. Required to use static dispatch.

view this post on Zulip Anthony Bullard (Dec 30 2024 at 16:09):

So everything about this proposal is "I don't care about nominal types"

view this post on Zulip Georges Boris (Dec 30 2024 at 16:09):

But IMHO I would much prefer to know that I'm working on a codebase that has a different extended "prelude" ("you should use our Dict instead of the builtin") and then that would be a know fact across all the codebase, than having to wonder that module by module.

view this post on Zulip Anthony Bullard (Dec 30 2024 at 16:10):

Well Georges, I think you wouldn't have to worry about that

view this post on Zulip Anthony Bullard (Dec 30 2024 at 16:10):

At some point you'd have to get that type, which means you would probably at least import some module that has a function that creates it

view this post on Zulip Anthony Bullard (Dec 30 2024 at 16:13):

And to be clear to all - if static dispatch happens, I'm not storming out :rofl:. I just think Roc will become a different language, and that might be fine!

view this post on Zulip Anthony Bullard (Dec 30 2024 at 16:13):

I think my next proposal would be to require type annotations on all functions, so yeah, definitely lean into "Functional Go with ADTs"

view this post on Zulip Richard Feldman (Dec 30 2024 at 16:15):

I'm curious to learn more about the concern about nominal types compared to structural types :thinking:

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

like let's say I have a Roc program and all I do is change one record type alias from : to := and then I expose all of its fields as public

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

what would be the concern about the effect that change would have on my code base?

view this post on Zulip Anthony Bullard (Dec 30 2024 at 16:20):

It's more about coding style. Saying "If I expose all my fields as public" is nice, but I think most people will go OO and not do that. And know we are primarily passing around black boxes from one function to the next.

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

And that might be fine, but it doesn't feel like the language I've been following for nearly 5 years now.

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

And again, that might be fine!

view this post on Zulip Anthony Bullard (Dec 30 2024 at 16:23):

Having builtin primitives from the Prelude be nominal is totally reasonable - but I fear the over-abstracted code that will be written by incentivizing nominality and ad-hoc polymorphism.

view this post on Zulip Richard Feldman (Dec 30 2024 at 16:26):

ahh I see

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

Maybe it’s the Elm in me :joy:

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

Or the fact that I have to read Java a lot at work

view this post on Zulip Richard Feldman (Dec 30 2024 at 16:28):

so maybe another way to say it is: if people are coming from an OO background, static dispatch makes it easier to code in a style that's familiar to them rather than learning the local style

view this post on Zulip Anthony Bullard (Dec 30 2024 at 16:28):

Yes. And maybe we can find a way - like Go largely has - of avoiding that

view this post on Zulip Richard Feldman (Dec 30 2024 at 16:29):

Anthony Bullard said:

Maybe it’s the Elm in me :joy:

for what it's worth, passing around black box opaque types is a common practice in Elm :big_smile:

view this post on Zulip Anthony Bullard (Dec 30 2024 at 16:29):

I avoided it like the plague

view this post on Zulip Anthony Bullard (Dec 30 2024 at 16:29):

I don’t think there’s NO reason for it

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

yeah so my overall thought on that is that there's an innate tradeoff there: on the one hand, if people are able to code in a style that's familiar when starting out, that can be a helpful onboarding tool which can lead them to learn the more idiomatic style over time

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

in which case it makes learning the idiomatic style more of a gentle curve

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

Totally!

view this post on Zulip Anthony Bullard (Dec 30 2024 at 16:31):

But it’s a question of will that second step happen at scale?

view this post on Zulip Richard Feldman (Dec 30 2024 at 16:31):

the innate flip side of that is that there's no guarantee that someone actually goes on to learn the idiomatic style :big_smile:

view this post on Zulip Georges Boris (Dec 30 2024 at 16:32):

And what would the non-idiomatic style be if that happens? I'm failing to visualize a minimal example of a worst case scenario.

view this post on Zulip Richard Feldman (Dec 30 2024 at 16:32):

this is one of the things I've been consciously trying to do differently from Elm though. The best example of this is something we haven't actually implemented yet, namely nested record updates

view this post on Zulip Richard Feldman (Dec 30 2024 at 16:33):

I don't personally care if Roc ever gets that feature because I don't personally run into situations where I want that :big_smile:

view this post on Zulip Richard Feldman (Dec 30 2024 at 16:33):

and I think it's actually a sign of structuring code wrong if nested record updates seem like a really appealing feature

view this post on Zulip Richard Feldman (Dec 30 2024 at 16:34):

Evan's philosophy on this is that nested record updates shouldn't be in Elm because it's better to learn the right way to structure your programs

view this post on Zulip Richard Feldman (Dec 30 2024 at 16:35):

but I'd like to try having them because I'm interested in them as a learning tool - like, you can write the code the way you want to, and hopefully over time you'll learn how to structure your code in a way where you use it less and less

view this post on Zulip Richard Feldman (Dec 30 2024 at 16:37):

and part of the reason I want to try this is that I've tried the "insist on learning how to do it the right way" approach in Elm, and one of the nonobvious downsides I saw was that it led to some people using lenses instead, which I think is an absolute catastrophe and way worse than having a code base that uses nested record updates :sweat_smile:

view this post on Zulip Anthony Bullard (Dec 30 2024 at 16:39):

That’s admirable, but as a delegate to ECMA TC-39 - the JavaScript standards body - I have seen the “feature ramp “ that can occur to get people from different persuasion comfortable to start and how it can take language in directions you can’t control

Now having strong leadership (and being pre-v1.0) will help, but it could lead to a rocky period of time that could disenfranchise people who love what would today be called idiomatic Roc.

view this post on Zulip Anthony Bullard (Dec 30 2024 at 16:41):

But to admit my biases, I love the Roc status quo!(modulo the compiler errors!)

view this post on Zulip Brendan Hansknecht (Dec 30 2024 at 16:42):

How do public and private fields work with nominal types (I don't think I ever saw a doc or syntax for this)? One of the nice things about opaque types is that they are 100% private. As such, they are annoying for many use cases. I haven't thought much about this, but I feel like it would be better for them to stay either 100% public or 100% private instead of being mixed. I think the convenience of mixed is part of what leads to a lot of the problems of OO.

view this post on Zulip Anthony Bullard (Dec 30 2024 at 16:42):

I think PNC and ergonomic pipelining are reasonable syntactic changes to encourage people to take a look. And the new lambda syntax feels nice.

view this post on Zulip Anthony Bullard (Dec 30 2024 at 16:44):

Reading the custom types proposal it seems like the author can choose either 100% public or private and no in-between. But that means you will see a mix of the two in practice

view this post on Zulip Anthony Bullard (Dec 30 2024 at 16:46):

I kind of like Zigs lack of opaque types, though it’s a little too strident in my opinion (same with interfaces - I get it though)

view this post on Zulip Brendan Hansknecht (Dec 30 2024 at 16:48):

I really enjoy the zig world of structs with methods, but I don't think it fits roc.

view this post on Zulip Anthony Bullard (Dec 30 2024 at 16:49):

Brendan Hansknecht said:

I really enjoy the zig world of structs with methods, but I don't think it fits roc.

Isn’t that exactly what static dispatch is?

view this post on Zulip Brendan Hansknecht (Dec 30 2024 at 16:51):

Sorry, that was referring to having everything be public and having nothing equivalent to opaque types.

view this post on Zulip Brendan Hansknecht (Dec 30 2024 at 16:52):

I do think methods can fit into roc just fine, but I think having the ability to hide type internals makes more sense in roc than in zig.

view this post on Zulip Brendan Hansknecht (Dec 30 2024 at 16:53):

zig is low level and often times you need to access internals to do the most performant thing possible. On top of that the exact layout and data always matters. Roc is much higher level, abstracts a lot of that away, and wants to allow you to safely pass data to a package without worrying about what could leak.

view this post on Zulip Richard Feldman (Dec 30 2024 at 16:57):

worth considering: Rust has the "mix of public and private fields on structs" thing and I haven't seen a problem in the Rust ecosystem (or in the two large Rust application code bases I've worked on, namely Roc and Zed) of people trying to write Rust like Java :big_smile:

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

Maybe it is just a general shift away from the OO model? I feel like it is much more common nowadays to see all public or all private. (often switching to public instead of making tons of getters and setters)

Also, in roc it is extra weird, if you had a single public field, could you use record update syntax to update that specific field?

view this post on Zulip Brendan Hansknecht (Dec 30 2024 at 17:01):

That said, I think roc's structural types add a level of freedom beyond rust that is really nice.

view this post on Zulip Anthony Bullard (Dec 30 2024 at 17:03):

Richard Feldman said:

worth considering: Rust has the "mix of public and private fields on structs" thing and I haven't seen a problem in the Rust ecosystem (or in the two large Rust application code bases I've worked on, namely Roc and Zed) of people trying to write Rust like Java :big_smile:

This is I think because of the people attracted to
Rust in its early days having a particular mindset against OO and establishing the culture. So maybe people can see the light

But Rust doesn’t have structural typing which I think has a more fragile base of support than structs with methods style

view this post on Zulip Richard Feldman (Dec 30 2024 at 17:04):

Anthony Bullard said:

Richard Feldman said:

worth considering: Rust has the "mix of public and private fields on structs" thing and I haven't seen a problem in the Rust ecosystem (or in the two large Rust application code bases I've worked on, namely Roc and Zed) of people trying to write Rust like Java :big_smile:

This is I think because of the people attracted to
Rust in its early days having a particular mindset against OO and establishing the culture. So maybe people can see the light

that would be encouraging if true, because I think that's even more strongly true of Roc than Rust :big_smile:

view this post on Zulip Anthony Bullard (Dec 30 2024 at 18:38):

I’m going to mark this resolved but I’d say let’s keep it in the back pocket. It might be a nice thing to backslide into if we don’t like the way SD goes

view this post on Zulip Notification Bot (Dec 30 2024 at 18:39):

Anthony Bullard has marked this topic as resolved.

view this post on Zulip Sam Mohr (Dec 30 2024 at 18:43):

Anthony Bullard said:

I think my next proposal would be to require type annotations on all functions, so yeah, definitely lean into "Functional Go with ADTs"

Bro, welcome to my world:
#ideas > Warning for unannotated exposed symbols
#ideas > Inline type annotations

Both of these come from trying to resolve that issue surrounding resolution. The general consensus is mixed, but mostly leans towards "we don't know if it's even a problem yet". We should revisit this once we get static dispatch and start seeing how the dev experience goes


Last updated: Jun 16 2026 at 16:19 UTC