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.
oh I actually think this is a selling point of static dispatch :big_smile:
the point of modules is to create interface boundaries
to hide implementation details
(that's why the old keyword for them was interface instead of module - to try to emphasize this)
static dispatch makes it more clear that this is their purpose, for exactly this reason
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
today, this is just a recommendation, but there's no baked-in incentive to encourage that
I think it's a benefit of static dispatch that it creates that incentive naturally! :smiley:
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.
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.
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
Tanner Nielsen has marked this topic as resolved.
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.
Tanner Nielsen has marked this topic as unresolved.
Practical example, the elm Http module:
https://package.elm-lang.org/packages/elm/http/latest/Http
Which defines these types:
HeaderBodyPartExpectProgressErrorResponseMetadatapersonally I'd be fine with those being in separate files
Metadata is a type alias that goes with Response, so I'd probably put that one in there
Expect could probably go in Http because it's only used to specify arguments to functions like Http.get
same with Body actually
I also never understood why Header was an opaque type in elm/http instead of a tuple or dictionary like in most languages
like we have it as a structural type alias in basic-cli
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.
so I'd probably go
Http.roc exposes Header, Progress, Error (all are structural types)Request.roc exposes Request, Expect, and Part opaque typesResponse.roc exposes Response opaque type and Metadata type aliasPlus, 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?
although honestly I'm not totally sure I'd want Expect and Part to be opaque types
both of them are only used as arguments to http requests
so they both seem like they could just be tag unions in Roc
e.g. instead of
Http.get { url: ..., expect: Http.expectString ... }
...it could be:
Http.get { url: ..., expect: ExpectString ... }
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
also interesting to note, custom tag unions would be really useful in this case
specifically because they could allow non-exhaustive versions
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
but custom tag unions allow that too!
so you would say:
Http.get { url: ..., expect: Expect.String ... }
and then by marking the Expect custom tag union as non-exhaustive, you can freely add new variants to it as nonbreaking changes
same goes for Part - I'm pretty sure the motivation there is just being able to add more as nonbreaking changes
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!
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)
right
or I guess you could write Http.Expect.String if you didn't want to expose it
but personally I assume I'd choose to bring Expect into scope
the bigger difference is in the Http docs I think
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
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:
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.
yeah I don't think the HTTP library experience would be much different honestly
which is to say, I think it'd still be nice :big_smile:
fundamentally I don't expect APIs to be significantly different in terms of what's exposed, what the types are, etc
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.
I do think it could get quite annoying in a webserver to have like 20 file for 20 different JSON payloads
Sometimes it's really nice to group all of those
hm, why would those want to be nominal at all though? :thinking:
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
totally! :grinning_face_with_smiling_eyes:
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
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:
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?"
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
but I'd say method syntax makes a stronger case for following the convention consistently
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