Stream: ideas

Topic: static dispatch revisions


view this post on Zulip Richard Feldman (Oct 04 2025 at 12:26):

I wrote up some design ideas for improving static dispatch - feedback welcome! https://docs.google.com/document/d/12URaMmsgatVVwW-paKNWeCAsc862Cqp-npcUzqRk704/edit?usp=sharing

view this post on Zulip Fabian Schmalzried (Oct 04 2025 at 13:25):

Generally looks really nice. Some comments/questions.

When I see equals : _ without any implementation below, I would not guess that there is a default one. I think equals = default would be more clear that there is an actual implementation and it's the default one.

If before the line import dest as Dest I would add a line dest = "foo", that would probably be an error? But would it be on the first or second line (not allowed to override type variables, or not possible to import a string)?

view this post on Zulip Fabian Schmalzried (Oct 04 2025 at 13:30):

Also would

decode = |bytes, fmt| {
module(dest).decode(bytes, fmt)
}

work today without type annotation? Because

decode = |bytes, fmt| {
import dest as Dest
Dest.decode(bytes, fmt)
}

does not. So this would be one of the rare places where a type annotation is required.

view this post on Zulip Richard Feldman (Oct 04 2025 at 13:33):

Fabian Schmalzried said:

If before the line import dest as Dest I would add a line dest = "foo", that would probably be an error?

that would actually work fine! There are separate scopes for types and for expressions, and import dest would be referring to the type scope.

it's similar to how this is totally allowed because elem is defined once in type scope and then again (separately) in expression scope, so it's not shadowing:

append : List(elem), elem -> List(elem)
append = |list, elem|

view this post on Zulip Richard Feldman (Oct 04 2025 at 13:38):

Fabian Schmalzried said:

Also would

decode = |bytes, fmt| {
module(dest).decode(bytes, fmt)
}

work today without type annotation?

no, this already requires a type annotation. We talked about it in a different thread - if the goal is to say "I want to dispatch on a type at a point where no value of that type exists at runtime" (such as the return type) then in the general case there's no way to express that using just value.

we could have special-case syntax for like "dispatch on the return type" but then you run into the problem of "what if I don't want to dispatch on exactly the return type, because the return type is Result, but I do want to dispatch on a type variable inside the return type..." - which is exactly the situation of the major use case for this style of dispatch (decoding returns a Result).

So at that point it seems like using the type annotation is the best design!

view this post on Zulip Richard Feldman (Oct 04 2025 at 13:41):

or to say it more concisely: when the thing you want to express is "use type information only to select a function to run" and there's no value we could use to infer that type, it's not possible to offer that feature in a way where you only use values and not types, because no value exists at that point :smile:

view this post on Zulip Mike (Oct 04 2025 at 22:36):

Does this change anything regarding the use case of “I want to add a method to a type I don’t own”? It looks like no - the solution there is still “write a normal function which takes that type as its first argument and call it with ->”

view this post on Zulip Richard Feldman (Oct 04 2025 at 23:08):

that's exactly right, yeah!

view this post on Zulip Sky Rose (Oct 05 2025 at 00:50):

My reactions:

view this post on Zulip Sky Rose (Oct 05 2025 at 00:50):

  1. This is another dose of OOP flavor. Past times we've added OOP/imperative flavor to Roc it's worked well, I expect this to as well.

view this post on Zulip Sky Rose (Oct 05 2025 at 00:50):

  1. This does go a little further than past times. By forcing the paradigm of one type for file, it's not just a tool you can reach for when OOP makes sense for your problem, it's something you have to do even when it doesn't fit. You touched on this a little bit with the section on Util. Requiring a type to wrap a standalone function makes me think of Execution in the Kingdom of Nouns.

view this post on Zulip Sky Rose (Oct 05 2025 at 00:50):

  1. No gen keyword means no label that a function is being generated. Seeing a type for hash with no definition looks like a mistake unless you know that hash can be generated. (Fabian touched on this above, too.)

view this post on Zulip Sky Rose (Oct 05 2025 at 00:50):

  1. All methods are now indented instead of top level, which might be minorly annoying, but also I don't want to start a fight about whitespace, it's not a big deal.

view this post on Zulip Sky Rose (Oct 05 2025 at 00:50):

  1. If you have a small private helper function that you call from your public method, you can't put them next to each other. You have to put the private function all the way at the end of the file outside the type definition.

view this post on Zulip Sky Rose (Oct 05 2025 at 00:54):

I've written mostly negative reactions because they're what I have to add, but I do think this is a positive idea overall, you just already covered the positive parts in the document.

view this post on Zulip Brendan Hansknecht (Oct 05 2025 at 01:17):

For 2, I agree....not sure if it is pit of success or likely to build bad apis....don't think I will know until it is used wider.

I feel like 3 has a direct solution with = default or = gen. I think we should do something like that. Or even = _.

I think 4 and 5 are quite real and we should consider addressing them. Honestly, I would prefer true namespaces with private and public functions. Then you can have the public function followed by private helpers all in the same space. You also have more control of scoping via nested types. I like that kind of control in general.

view this post on Zulip Richard Feldman (Oct 05 2025 at 02:33):

not to go on a long tangent or anything, but "Execution in the Kingdom of Nouns" was about Java not having standalone functions at all, only methods (at the time of the article; now it has lambdas):

It's odd, though, that Java appears to be the only mainstream object-oriented language that exhibits radically noun-centric behavior. You'll almost never find an AbstractProxyMediator, a NotificationStrategyFactory, or any of their ilk in Python or Ruby. Why do you find them everywhere in Java? It's a sure bet that the difference is in the verbs. Python, Ruby, JavaScript, Perl, and of course all Functional languages allow you to declare and pass around functions as distinct entities without wrapping them in a class.

view this post on Zulip Brendan Hansknecht (Oct 05 2025 at 02:39):

Python, Ruby, JavaScript, Perl, and of course all Functional languages

I must live in a different world than this guy. I don't think I would ever call any of these languages "functional languages". Sure they have lambdas.... But having a feature does not magically make the language functional. If anything it is about standard concepts and ways of coding....which is definitely not function in my opinion for these languages (though I don't really know ruby and perl)

view this post on Zulip Richard Feldman (Oct 05 2025 at 02:53):

I think he's saying "Python, Ruby, JS, Perl, and in addition to those non-functional languages of course all the functional ones too"

view this post on Zulip Brendan Hansknecht (Oct 05 2025 at 03:01):

Ahh... Yeah. I parsed that wrong... I see now

view this post on Zulip Brendan Hansknecht (Oct 05 2025 at 03:02):

Missed the word "and" despite quoting the text.

view this post on Zulip Jasper Woudenberg (Oct 05 2025 at 16:34):

I quite like being able to use static dispatch while still putting multiple types in a module, because of the extra control it gives me in figuring out "what goes in the top-level" when designing a library.

The one thing I'm less sure about is the strong guidance of designing module's around a (top-level) type. While I do agree many nice library APIs have such a design, I'm not sure "single-type-per-module" provides a good guidance while designing nice APIs. It's a bit like the "don't repeat yourself" advice: I think really nice code probably doesn't have much duplication, but also that stamping out duplication as soon you see it is unlikely to result in nice code.

And that's when designing libraries. When writing applications I think it's really beneficial to work in a single file for as long as possible and only extract bits out when a pattern becomes quite obvious. In a couple of OO codebases I've worked in I've seen a single-class-per-file convention result in a lot of premature abstraction. Maybe Roc would be less vulnerable to that with platforms imposing some top-level structure on applications though :shrug: .

view this post on Zulip Jasper Woudenberg (Oct 05 2025 at 17:14):

Also love that opaque type wrapping/unwrapping goes away with this. I know why it was necessary, but it always felt a bit out of place in Roc, like you'd need to know its ML ancestry to understand why it worked that way.

view this post on Zulip Jared Ramirez (Oct 05 2025 at 23:02):

IMO there are two distinct things in this proposal that are interrelated, but not necessarily the same thing:

  1. Change how static dispatch works to not be "A function defined in the same module as the nominal type, that has the nominal type as an arg", and instead be defined as functions inside a .{ ... } on the end of the type
  2. Change how modules work to always be centered around a type

As to the former, I think there are lots of upsides as mentioned in the proposal:

I agree with Sky that the main con is splitting private helpers from the public methods they support.

But the pros outweigh that con IMO. And, as the proposal mentions, it's similar to Rust's type impl block which seems to have worked out pretty well.

Then, related but IMO separately, is the idea of type-modules, or modules centered around a type. I see the idea here, but I'm much less convinced about this. Echoing others in this thread, I think there are certain types of applications where having this design makes sense, but others where it may not.

The main pro I see is: Making a common design pattern more baked into the language

But the cons I see are:

All this to say, it may be valuable to pull apart these ideas and consider them independently, as the former (which seems valuable to me in it's own right), doesn't necessarily require the latter (which to me seems more questionable)

view this post on Zulip Brendan Hansknecht (Oct 05 2025 at 23:12):

Jared Ramirez said:

But the pros outweigh that con IMO. And, as the proposal mentions, it's similar to Rust's type impl block which seems to have worked out pretty well.

impl let's you intermingle public and private..I would prefer if we had this as well.

view this post on Zulip Richard Feldman (Oct 06 2025 at 01:47):

it seems like two themes here are:

I think there's a good enough chance that these end up being fine that we should do the experiment and see how things actually feel. As Jared noted, certain aspects of this proposal can be altered without affecting the others, so we can always make adjustments based on how it goes trying this design out!

view this post on Zulip Richard Feldman (Oct 06 2025 at 01:49):

and I'd like to start with this because it's the simplest. The adjustments we'd make to address these concerns would require making the language strictly bigger, so I'd like to see how the smaller design plays out in practice before adding things. :smile:

view this post on Zulip Brendan Hansknecht (Oct 06 2025 at 02:10):

Sounds reasonable

view this post on Zulip Isaac Van Doren (Oct 12 2025 at 03:42):

You could always do something like this with the current design to put a public method and private helper together, and remove the leading indentation:

MyType := [Foo].{
    display : MyType -> Str
    display = displayImpl
}
displayImpl = |x| ...
displayHelper = |y| ...

view this post on Zulip Isaac Van Doren (Oct 12 2025 at 03:48):

I'm definitely on board with trying the proposal out without adding anything extra, but if it does end up being an issue, something like the above could be supported intentionally:

MyType := [Foo].{
    display : MyType -> Str
}

# display is automatically exported as a method because the name matches the method name in the type declaration
display = |x| ...

displayHelper = |y| ...

view this post on Zulip Brendan Hansknecht (Oct 12 2025 at 03:50):

that is a nice way to do it!

view this post on Zulip Brendan Hansknecht (Oct 12 2025 at 03:50):

A little split, but still not too bad

view this post on Zulip Joshua Warner (Oct 14 2025 at 03:44):

Ooh - in some ways I actually really like that split. Signature in the type block and impl down farther.

view this post on Zulip Joshua Warner (Oct 14 2025 at 03:44):

That makes it really easy to get an overview of the library by just reading the top of the file.

view this post on Zulip Matthieu Pizenberg (Oct 17 2025 at 12:13):

Really like that split too, but at the same time, it’s also nice to have the type def just above the impl. But you don’t want to duplicate the type def right? Or maybe we can? I mean if the compiler can check that both are identical maybe it’s ok to have the duplication.

view this post on Zulip Anton (Oct 17 2025 at 13:10):

I would prefer to avoid the duplication. If users make a manual edit, run roc check/run/build and see the error because they forgot to update the second type def, I suspect that would not leave them with a favorable opinion of Roc.

view this post on Zulip Anthony Bullard (Dec 29 2025 at 02:30):

i think my only concern is that this mean that the concept of exposing a function is coupled to the concept of associating it with a type,

even if that type (like Util) is neither an argument or a return type of said function - the function still must be associated with it for it to be exposed. Feels very OO in the Smalltalk sense.

view this post on Zulip Anthony Bullard (Dec 29 2025 at 02:31):

(and yes i am just now seeing this)

view this post on Zulip Fabian Schmalzried (Dec 29 2025 at 10:25):

True, and is that a problem in practice? Why does it matter if you have a Util module or a empty Util type? In both cases you just do Util.some_stuff(a,b). Util :: [].{... technically defines a type, but you can explain any beginning that this is just how to create modules and they will be unblocked.

That said, would it make sense to have syntax for typeless modules, i.e. make something like Util { ... #functions here ...} work?

view this post on Zulip Anthony Bullard (Dec 29 2025 at 14:37):

It is largely academic and conceptual of course, but a "type" having "methods" associated with them that have nothing to actually do with the type is strange, unless the thought is that a Module(really file) _is_ a type (like a class in Java), and then some "methods" are "static" (i.e., associated with the type but not with an instance of the type since the type is never used as a value), and some are not.

It makes the language feel like less about "just functions and data" in the classical functional (or even imperative) sense, but where a datatype is instead the root concept of the language, and functions don't really exist outside of associated methods to a datatype, or those closures created with the scope of one of those associated methods.

So it is very much more about what it _feels_ like to use a language (called purely functional, but presenting strongly as a OO-adjacent imperative one).

view this post on Zulip Anthony Bullard (Dec 29 2025 at 14:38):

And I would indeed feel better if there was a notation for a "typeless" module so that someone can't create a (zero-sized) value of a type that was created solely to expose plain functions

view this post on Zulip Anthony Bullard (Dec 29 2025 at 14:39):

And to be clear, I'm not saying I'm categorically opposed here, but I feel strongly that this is going to make Roc a much different language that speaks to a different audience (and that may in fact be a good thing).


Last updated: Jun 16 2026 at 16:19 UTC