Is there an official reason why roc doesn't support methods?
as in, functions associated with objects?
Different languages use the word "method" to refer to different things so it would be helpful to know which you mean. It often refers to functions attached to an object, which is an object-oriented concept and Roc is a functional programming language that doesn't have object orientation. Functional languages deliberately separate functions from data.
Rust has a different concept of trait methods.
Just a question I received on Reddit.
I forgot the FP nature of Roc (of course it is, but slipped my mind).
I responded more in line of consistency. "Methods or just functions with a self argument" and "pipe operator allow chaining".
So I looked at the reddit post and I find adhoc polymorphism an interesting goal. iiuc, they want it so that someone could say dict.insert k v
. The major thing hidden here from regular Roc is the implementation of the dictionary. I could theoretically use that every time I use a dictionary even if one is array backed and another is actually hashing.
Currently in Roc, if I wanted to that in the same module, I couldn't write Dict.insert dict k v
in both locations. I would have to write FlatDict.insert
in one location and HashDict.insert
in the other.
The other goal sounds like it would be a direct alternative to pipeline syntax via function chaining. E.g:
dict.insert k0 v0
.insert k1 v1
.insert k2 v2
Which as I typed I realized has a major problem with parsing due to how args and functions are grouped together.
so would Dict be an ability in that case, leveraging builtins like dictInsert?
It seems like part of the value in non-default dict implementations is the exposure of novel capabilities or semantics. Performance is another reason for them, but performance is often obtained through those novel access approaches
If we exposed Dict
as an ability, I guess we could make any dictionary use Dict.insert
.
In this cases, I was assuming Dict
was just the builtin dictionary and that a user was potentially using other dictionaries that happen to use the same API.
Imagine any set of types that have different implementations but would likely have the same interface.
abilities are ad hoc polymorphism, same as Rust traits
as for the syntax, foo.bar baz
already means that foo
is a record, it has a bar
field which holds a function, and foo.bar baz
calls that function passing baz
also, that calling style is the reason Rust has errors of the form "this doesn't compile...did you forget to import a trait maybe?"
fully-qualified calls don't have that problem, which is one reason abilities in Roc are designed to be called that way :big_smile:
e.g. Bool.isEq : a, a -> Bool | a supports Equating
the a supports
is the ad-hoc polymorphism; the code for isEq
changes based on the implementation provided in the definition of a
- so it's dispatching to a completely different implementation based on the type
but you still call it like a normal function, e.g. Bool.isEq foo bar
Sounds about right. Was just trying to give context on the question. I guess this does open questions around the standard library types potentially being abilities as well so that it is easy to make user land drop in replacements.
so as long as Bool
is imported, we know where to find the definition of isEq
in contrast, if we had foo.isEq bar
then it's not immediately clear which module the definition of isEq
lives in :sweat_smile:
so if it wasn't already in scope, the compiler would have to give an error
the current plan is for abilities not to have type parameters
so you can define an ability Equating
but not IsDict k v
because we'd need either higher-kinded types or associated types to allow something like that
e.g. Dict.insert : Dict k v, k, v -> Dict k v
works fine if Dict
is an opaque type
but if we want to make it an ability, then it would need to be something like Dict.insert : dict, k v -> d | dict supports IsDict k v
and then map
gets kinda weird, e.g. instead of Dict.map : Dict k v1, (k, v1 -> v2) -> Dict k v2
we have Dict.map : d1, (k, v1 -> v2) -> d2 | d1 supports IsDict k v1 | d2 supports IsDict k v2
but more to the point, at that point you can do Monad.bind : m1, (a -> m2) -> m2 | m1 supports Monad a | m2 supports Monad b
and there's a whole FAQ entry about why not to support that :big_smile:
ah, didn't think about the higher kinded type part. yeah. makes sense.
You could still do
dict =
Dict.empty
. insert k1 v1
. insert k2 v2
... I guess that technically makes a unique syntax that we could parse. Still has the other naming and higher kindled type problems, but at least doesn't break parsing.
Last updated: Jul 26 2025 at 12:14 UTC