I wrote up some design ideas for improving static dispatch - feedback welcome! https://docs.google.com/document/d/12URaMmsgatVVwW-paKNWeCAsc862Cqp-npcUzqRk704/edit?usp=sharing
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)?
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.
Fabian Schmalzried said:
If before the line
import dest as DestI would add a linedest = "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|
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!
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:
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 ->”
that's exactly right, yeah!
My reactions:
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.)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.
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.
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.
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)
I think he's saying "Python, Ruby, JS, Perl, and in addition to those non-functional languages of course all the functional ones too"
Ahh... Yeah. I parsed that wrong... I see now
Missed the word "and" despite quoting the text.
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: .
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.
IMO there are two distinct things in this proposal that are interrelated, but not necessarily the same thing:
.{ ... } on the end of the typeAs to the former, I think there are lots of upsides as mentioned in the proposal:
eql or hash with default implementationsdest.decode : ...)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)
Jared Ramirez said:
But the pros outweigh that con IMO. And, as the proposal mentions, it's similar to Rust's type
implblock 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.
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!
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:
Sounds reasonable
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| ...
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| ...
that is a nice way to do it!
A little split, but still not too bad
Ooh - in some ways I actually really like that split. Signature in the type block and impl down farther.
That makes it really easy to get an overview of the library by just reading the top of the file.
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.
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.
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.
(and yes i am just now seeing this)
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?
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).
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
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