Stream: ideas

Topic: static dispatch - abilities concern


view this post on Zulip Tanner Nielsen (Nov 17 2024 at 16:52):

Starting a new thread specifically for this topic because the proposal thread was getting too long:

I'm concerned that static dispatch doesn't fully fill the void created by removing abilities. Just to make up an example, if I wanted to define a ToString ability and implement it for two different types within the same module, I totally can do that today:

ToString implements
    toString : a -> Str where a implements ToString

Foo := Str implements [
        ToString {
            toString: fooToString,
        },
    ]
Bar := Bool implements [
        ToString {
            toString: barToString,
        },
    ]

fooToString = ...
barToString = ...

print : a -> Task {} _ where a implements ToString
print = ...

How would I approach this in the static dispatch world? A naive translation might look like...

print : a -> Task {} _ where a.toString() -> Str
print = ...

Foo := Str
toString : Foo -> Str
toString = ...

Bar := Bool
toString : Bar -> Str
toString = ...

...but this obviously won't work because toString is defined multiple times. The only way around this appears to be defining Foo and Bar in separate modules, but I feel like I shouldn't be forced to define them in separate modules if I don't want to.

view this post on Zulip Richard Feldman (Nov 17 2024 at 16:54):

oh I actually think this is a selling point of static dispatch :big_smile:

view this post on Zulip Richard Feldman (Nov 17 2024 at 16:54):

the point of modules is to create interface boundaries

view this post on Zulip Richard Feldman (Nov 17 2024 at 16:54):

to hide implementation details

view this post on Zulip Richard Feldman (Nov 17 2024 at 16:54):

(that's why the old keyword for them was interface instead of module - to try to emphasize this)

view this post on Zulip Richard Feldman (Nov 17 2024 at 16:55):

static dispatch makes it more clear that this is their purpose, for exactly this reason

view this post on Zulip Richard Feldman (Nov 17 2024 at 16:55):

it creates a natural incentive to organize each module around a particular type, in order to expose functions on that type which become methods on that type

view this post on Zulip Richard Feldman (Nov 17 2024 at 16:55):

today, this is just a recommendation, but there's no baked-in incentive to encourage that

view this post on Zulip Richard Feldman (Nov 17 2024 at 16:56):

I think it's a benefit of static dispatch that it creates that incentive naturally! :smiley:

view this post on Zulip Tanner Nielsen (Nov 17 2024 at 16:58):

Do you see any future where Roc modules aren't coupled to files? Kind of like how you can define same-file modules in Rust? I could just see this being a bit irritating if you have lots of small types and are forced to scatter them across many files.

view this post on Zulip Tanner Nielsen (Nov 17 2024 at 16:59):

it creates a natural incentive to organize each module around a particular type, in order to expose functions on that type which become methods on that type

I think this :point_up: is a good thing for what it's worth. It just feels a bit heavy handed to require it.

view this post on Zulip Richard Feldman (Nov 17 2024 at 17:00):

Tanner Nielsen said:

Do you see any future where Roc modules aren't coupled to files? Kind of like how you can define same-file modules in Rust? I could just see this being a bit irritating if you have lots of small types and are forced to scatter them across many files.

I've thought about it but I don't think it's likely to be worth it

view this post on Zulip Notification Bot (Nov 17 2024 at 17:27):

Tanner Nielsen has marked this topic as resolved.

view this post on Zulip Jasper Woudenberg (Nov 17 2024 at 20:47):

Oh, I hadn't realized this. What about this situation?

Would I need to move the helper types into a submodule?

I get the idea of organizing modules around types, but I've always applied that to external-facing types/modules. For internal implementation details, I feel that the OO convention of a single class per file often results in abstracting too early, and in places where you get no benefit, i.e. internal implementation details of a class.

Put differently, if a little helper type is used in a single place place, don't spend energy designing an API for it, there's not enough users/information to do a good job at it anyway. I'd prefer just to combine that in the module the helpers is used.

view this post on Zulip Notification Bot (Nov 17 2024 at 20:48):

Tanner Nielsen has marked this topic as unresolved.

view this post on Zulip Jasper Woudenberg (Nov 17 2024 at 20:50):

Practical example, the elm Http module:
https://package.elm-lang.org/packages/elm/http/latest/Http

Which defines these types:

view this post on Zulip Richard Feldman (Nov 17 2024 at 21:31):

personally I'd be fine with those being in separate files

view this post on Zulip Richard Feldman (Nov 17 2024 at 21:32):

Metadata is a type alias that goes with Response, so I'd probably put that one in there

view this post on Zulip Richard Feldman (Nov 17 2024 at 21:33):

Expect could probably go in Http because it's only used to specify arguments to functions like Http.get

view this post on Zulip Richard Feldman (Nov 17 2024 at 21:34):

same with Body actually

view this post on Zulip Richard Feldman (Nov 17 2024 at 21:34):

I also never understood why Header was an opaque type in elm/http instead of a tuple or dictionary like in most languages

view this post on Zulip Richard Feldman (Nov 17 2024 at 21:35):

like we have it as a structural type alias in basic-cli

view this post on Zulip Jasper Woudenberg (Nov 17 2024 at 21:39):

I dunno, doesn't seem ideal to need to import multiple modules to do an HTTP request, even if we can reduce it to two. Documentation might not flow as nice either if it's split over mutliple files.

view this post on Zulip Richard Feldman (Nov 17 2024 at 21:42):

so I'd probably go

view this post on Zulip Jasper Woudenberg (Nov 17 2024 at 21:42):

Plus, if we make an API change that adds a method working on Expect, maybe map or something, and that means we want to move it into its own module, that would now be a breaking API change. Maybe not very likely with HTTP which models a relatively stable API that doesn't change much over time, but for other domains if you want to avoid breaking API changes, would we recommend putting opaque types in their own module just to be sure?

view this post on Zulip Richard Feldman (Nov 17 2024 at 21:43):

although honestly I'm not totally sure I'd want Expect and Part to be opaque types

view this post on Zulip Richard Feldman (Nov 17 2024 at 21:44):

both of them are only used as arguments to http requests

view this post on Zulip Richard Feldman (Nov 17 2024 at 21:45):

so they both seem like they could just be tag unions in Roc

view this post on Zulip Richard Feldman (Nov 17 2024 at 21:46):

e.g. instead of

Http.get { url: ..., expect: Http.expectString ... }

...it could be:

Http.get { url: ..., expect: ExpectString ... }

view this post on Zulip Richard Feldman (Nov 17 2024 at 21:47):

to be clear, I understand the concern in general, but I do think it's helpful to look at specific examples to see what they would look like in a world where this design applied

view this post on Zulip Richard Feldman (Nov 17 2024 at 21:47):

also interesting to note, custom tag unions would be really useful in this case

view this post on Zulip Richard Feldman (Nov 17 2024 at 21:50):

specifically because they could allow non-exhaustive versions

view this post on Zulip Richard Feldman (Nov 17 2024 at 21:50):

this might seem like a subtle distinction, but I think the reason Expect is opaque in Elm is just so that more variations on it can be added in the future as nonbreaking changes

view this post on Zulip Richard Feldman (Nov 17 2024 at 21:50):

but custom tag unions allow that too!

view this post on Zulip Richard Feldman (Nov 17 2024 at 21:50):

so you would say:

Http.get { url: ..., expect: Expect.String ... }

view this post on Zulip Richard Feldman (Nov 17 2024 at 21:51):

and then by marking the Expect custom tag union as non-exhaustive, you can freely add new variants to it as nonbreaking changes

view this post on Zulip Richard Feldman (Nov 17 2024 at 21:52):

same goes for Part - I'm pretty sure the motivation there is just being able to add more as nonbreaking changes

view this post on Zulip Richard Feldman (Nov 17 2024 at 21:53):

with the custom tag union approach, you don't have to have functions with unusually namespace-y names like Http.expectString (which would usually be Expect.string) - in this design, you can just have Expect.String and not need a separate function for it, and it's still backwards-compatible!

view this post on Zulip Jasper Woudenberg (Nov 17 2024 at 21:59):

You'd need to expose Expect for that though, right?

import Http exposing [Expect]

If you're willing to do that, you could do the same in Elm

import Http exposing (expectString)

view this post on Zulip Richard Feldman (Nov 17 2024 at 22:00):

right

view this post on Zulip Richard Feldman (Nov 17 2024 at 22:02):

or I guess you could write Http.Expect.String if you didn't want to expose it

view this post on Zulip Richard Feldman (Nov 17 2024 at 22:02):

but personally I assume I'd choose to bring Expect into scope

view this post on Zulip Richard Feldman (Nov 17 2024 at 22:02):

the bigger difference is in the Http docs I think

view this post on Zulip Richard Feldman (Nov 17 2024 at 22:03):

where you just have one entry for Expect which explains how it works instead of having a separate entry for each variant, which doesn't really seem necessary to me

view this post on Zulip Richard Feldman (Nov 17 2024 at 22:06):

to be clear, I'm not saying that one approach is much better than the other, just that I think we could have a very nice HTTP library in terms of module structure, types, and dispatch in the proposed design :smiley:

view this post on Zulip Jasper Woudenberg (Nov 17 2024 at 22:36):

I dunno, I'm having a hard time with this :sweat_smile:, static dispatch and custom tag union/records both. Like, in terms of the amount of language they add, it'd be easier to get excited if it the HTTP library they enabled were much better then Elm's. I guess the big advantages of both proposals lie outiside the realm of HTTP APIs though.

view this post on Zulip Richard Feldman (Nov 17 2024 at 22:56):

yeah I don't think the HTTP library experience would be much different honestly

view this post on Zulip Richard Feldman (Nov 17 2024 at 22:56):

which is to say, I think it'd still be nice :big_smile:

view this post on Zulip Richard Feldman (Nov 17 2024 at 23:00):

fundamentally I don't expect APIs to be significantly different in terms of what's exposed, what the types are, etc

view this post on Zulip Richard Feldman (Nov 17 2024 at 23:01):

maybe the file structure might be different sometimes but I don't think file structure is the big deal with APIs; the big deal is what the guarantees are, what's exposed and what's hidden, how ergonomic it is to use, etc.

view this post on Zulip Brendan Hansknecht (Nov 17 2024 at 23:14):

I do think it could get quite annoying in a webserver to have like 20 file for 20 different JSON payloads

view this post on Zulip Brendan Hansknecht (Nov 17 2024 at 23:14):

Sometimes it's really nice to group all of those

view this post on Zulip Richard Feldman (Nov 17 2024 at 23:15):

hm, why would those want to be nominal at all though? :thinking:

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

oh, fair. I was thinking you may want a different internal layout from what actually gets serialized, but can be done by having two different structural types if needed

view this post on Zulip Richard Feldman (Nov 17 2024 at 23:17):

totally! :grinning_face_with_smiling_eyes:

view this post on Zulip Brendan Hansknecht (Nov 17 2024 at 23:38):

Though you may want all of those types to be nominal due to getting method dot syntax for other things....so may still be pushed to make a ton of modules

view this post on Zulip Richard Feldman (Nov 17 2024 at 23:46):

but if I want method syntax for more than the things that come standard, that means I'm writing custom functions on that type - which in turn creates its own demand for separate files :big_smile:

view this post on Zulip Richard Feldman (Nov 17 2024 at 23:47):

like today sometimes I'm like "it's just 2 functions...do I really want a separate module just so I can call Foo.run instead of putting it in the Bar module and having it be Bar.runFoo instead?"

view this post on Zulip Richard Feldman (Nov 17 2024 at 23:48):

so the "put it in a separate module so calls look normal, or keep it in the same module because I don't want to make a separate file?" tension already exists today

view this post on Zulip Richard Feldman (Nov 17 2024 at 23:49):

but I'd say method syntax makes a stronger case for following the convention consistently

view this post on Zulip Richard Feldman (Nov 17 2024 at 23:49):

which probably leads to more files than today but also more consistent APIs than today, which seems like a fine tradeoff to make!


Last updated: Jun 16 2026 at 16:19 UTC