This has been brought up in this thread a couple times before, but I think we should consider another syntax or stricter rules for defining new abilities.
Recall that the current proposed syntax for the definition of an ability Eq is
Eq has { isEq, isNotEq }
isEq : val, val -> Bool where val has Eq
isNotEq : val, val -> Bool where val has Eq
the current rules are just that you need the Eq has ... line, and the functions defining the ability must be defined on the top-level.
This means that definitions can be non-local, which I believe makes it harder for humans and computers (the compiler) to understand. A few concerns the syntax introduces include:
Eq, I may have to search the rest of the file to figure out what isEq and what isNotEq look like, since there may be any number of intermediate top-level definitions between them and the Eq has ... line.Eq has ... definition, they must also find the relevant isEq and isNotEq definitions, which can be cumbersome if they are far away. This is mostly an issue in a plain text editor; we can certainly do better in the editor which is designed to deal with scenario. However,Eq look like?", we wouldn't be able to answer that question just by looking at the Eq has ... definition. In general, we would always have to do more work than if isEq and isNotEq were defined inside Eq itself. Whether this makes a measurable difference during re-analysis between file changes is something we won't know for a while, though.Embed has { embed }
# other defs here
embed : a, b -> Str where a has embed
# other defs here
embed : a, b -> Str where b has embed
which definition of embed is the one that Embed has { embed } is referencing? The compiler couldn't know, and worse yet, the author may not know. Again I'm not sure if this would actually arise ("default implementations" are more likely to arise, but those are unambiguous), but it's something to consider.
Anyway, I do think 1-3 are large enough concerns that it may be worth considering an alternative here. Here are a couple suggestions:
Eq has
{
isEq : val, val -> Bool,
isNotEq : val, val -> Bool
} where val has Eq
or
Eq has
isEq : val, val -> Bool where val has Eq
isNotEq : val, val -> Bool where val has Eq
(I prefer the second). This lessens the concerns (1)-(3) and eliminates the ambiguity component - it now enough to parse the Eq has block, and you have all you need to know about what the definition of Eq is.
One evident disadvantage of this is that it makes it less clear that isEq and isNotEq sit on the top-level of the module they are defined in. That might be something we can get away with teaching, but it is a concern.
Eq has { isEq, isNotEq }
# other defs
isEq : val, val -> Bool where val has Eq
# other defs
isNotEq : val, val -> Bool where val has Eq
is illegal, we must explicitly do
Eq has { isEq, isNotEq }
isEq : val, val -> Bool where val has Eq
isNotEq : val, val -> Bool where val has Eq
This doesn't actually resolve the problems above, it just warns the author that they should order their definitions in this way. The reason I say this is because we'd still want the compiler to do the work to find interspersed and possibly-ambiguous definitions even if they are defined out-of-band, so that user experience doesn't suffer. At the very least, we'd need to invest in good error messages that say something like "Did you mean to host this definition of isNotEq defined <here> up?".
The upside of this is that it still remains very clear that isEq, isNotEq are on the top-level.
Okay, apologies for the wall of text :) I don't intend this to be an armchair exercise - in the design and implementation, I do think it's a legitimate concern. I think we should have the computer do as much work as possible to figure out stuff for the author, but not when the author must also do more work.
Maybe-noob-y question: why do/should those ability functions still sit at the top level? Are other things referencing them?
So that if Eq is defined in the Bool module, you could do Bool.isEq rather than Bool.Eq.isEq
Makes sense, thanks!
I moved this from the #ideas > Abilities thread, since that topic ended up going in a different direction
so in the initial draft of the abilities doc, I actually had something more like #1 above!
so like
Eq has {
isEq : val, val -> Bool,
isNotEq : val, val -> Bool
} where val has Eq
here's the problem though: how does that interact with exposes?
for example:
interface Eq
exposes [ Eq, isEq, isNotEq ]
imports []
Eq has {
isEq : val, val -> Bool,
isNotEq : val, val -> Bool
} where val has Eq
if I can write exposes [ isEq ] then that must mean isEq is in scope at the top level, right?
which in turn means I should be able to call it from the top level, like so:
interface Eq
exposes [ Eq, isEq, isNotEq ]
imports []
Eq has {
isEq : val, val -> Bool,
isNotEq : val, val -> Bool
} where val has Eq
blah = isEq foo bar
so at that point, we effectively have isEq being essentially a top-level declaration for all intents and purposes except syntactically :sweat_smile:
that said, maybe that's fine!
yeah that’s really the only downside of this approach i could think of
but tbh i feel like it’s fine, we only need to teach it once and it kind of aligns with type classes in haskell (though i’m not saying that’s the model we should shoot for)
I think the readability and refactoring advantages of this outweigh the downside of being “weird” that it’s on the top level
also another option is the indented form, which could syntactically look more like the functions are on the top level compared to the record-like syntax
Regarding this syntax:
Eq has {
isEq : val, val -> Bool,
isNotEq : val, val -> Bool
} where val has Eq
I suggest an alternative to the where clause - a scoped alias:
Eq has
alias val = has Eq
isEq : val, val -> Bool
isNotEq : val, val -> Bool
Here it aliases val to a type-declaration, in a way, but it can also do simple name aliasing (e.g. alias L = List).
The same syntax can be useful in a function, for example to shorten a long type name because you're gonna be mentioning it a lot.
It makes it feel and look more like a variable rather than a constraint on the definition, and I do think of it as a variable - not of data but of a type.
also the {}'s can be dropped, that's nice
I like this one from earlier:
Eq has
isEq : val, val -> Bool | val has Eq
isNotEq : val, val -> Bool | val has Eq
a nice thing about it is that we have the exact type signature that can be used in documentation
here's another possible direction:
{
isEq : val, val -> Bool | val has Eq.
isNotEq : val, val -> Bool | val has Eq,
} => Eq
kinda weird to have the thing you're defining at the end though :sweat_smile:
The more I think about it, the weirder it seems that we use Eq in the definition of Eq
recursion! :smiley:
But we aren't defining something recursive!
If it was recursive, I'd be okay with it
Is this a useful idea?
Eq a has
isEq : a, a -> Bool
isNotEq : a, a -> Bool
Here, a signifies "some type with the Eq ability"
(inspired by type parameters... is this an ability parameter? Idk why there would ever be any quantity other than one...)
haha in some sense it is recursive though!
the definition of Eq literally involves providing two functions that both involve the Eq ability
I think Eq a makes it look like Eq has a type parameter though :sweat_smile:
Syntax solution? Eq for a means ...? hehehe semi-joking
I like “for a” or “of a”
The | constraints syntax is great for a one-off or situations where constraints are just different across your methods, the alias syntax is great for when the same constraint is reused around a lot.
We can even take this a step further and say it aliases the whole signature (but then naming gets a bit hard):
Eq has
alias FnSig = val, val -> Bool | val has Eq
isEq : FnSig
isNotEq : FnSig
In any case it sounds like we’re roughly in agreement about the definition of function signatures within the ability definition so I’ll proceed with that. that’s the biggest impact to the language implementation/user ergonomics, the other stuff we can change pretty easily without impacting semantics
yeah I'd go with this syntax for now:
Eq has
isEq : val, val -> Bool | val has Eq
isNotEq : val, val -> Bool | val has Eq
I don't want to optimize for ability definition syntax being as concise as possible; I'd rather optimize for quality of documentation (e.g. the signature you write out is exactly what appears in the docs)
especially because defining new abilities should be one of the rarest things people do in Roc
I think the typical project should define 0 new abilities
and that's true whether it's a library or application!
(0 is the number for all Elm code bases, and that ecosystem is the nicest I've ever used...my #1 biggest fear with introducing Abilities to Roc is that the feature will be overused in a way that degrades the ecosystem!)
it adds information to the signature but it still doesn't contain all the information you might need, like what is Bool or Eq (not the best example here)
saying the constraints are more important for the docs than what data Bool has (imagine its Person instead) because they are specified on the method might not always be true
sorry, I don't follow - can you write out a code example?
sure
Hashable has
getHash : -> Hash # not sure if thats correct syntax for 0 arguments
HashableWithSauce has
getHash : sauce -> Hash | sauce has Hashable
The docs for HashableWithSauce would make me look at what is Hash and what is Hashable anyways. If Hash is a concept worth naming it is probably worth being intoduced to as well (per project). On the same logic, if the | sauce has Hashable constraint was something that was used a lot and was worth naming Sauce one way or another, one would need to be introduced to the Sauce concept. Then reading the docs you either know Sauce or you look it up, just like with Hash or Hashable. Here the concept is only used once so those details are indeed relevant to the method HashableWithSauce.getHash itself, so I would want to see the whole signature of it including the constraints. The docs are as specific as the signature.
but if the constraint IS used a lot throughout the ability, it would become noisy in the docs instead of being introduced to some named concept once
my point is that naming a constraint is useful
(or defining it for the whole scope, like with the where or alias, so it only has to be mentioned once)
supporting both things seems good
so 0-argument functions don't actually exist in Roc, because a pure function with 0 arguments always gives the same answer...so the only reason to ever have one is to delay evaluation in case the function takes a long time to compute that one answer :big_smile:
Hashable has
getHash : {} -> Hash
you could do that
but I don't understand what that ability would do :thinking:
importantly, there's no implicit this or self involved here - not sure if that was clear!
in fact I think the compiler should ideally give an error for any ability function that doesn't have a type variable with that ability constraining it (e.g. a in the Eq example above) because then the function isn't (and can't possibly be) using the ability at all!
yeah it wasn't clear to me hehe.
anyhow the logic of that ability isn't too relevant either (plus I'm horrible at examples), the point was how you'd interact with the docs around constraints, and how often its worth mentioning them.
sometimes its worth per function, but other times not
for example in the isEq and isNotEq example I would expect those functions to have the same constraints by nature, whatever they are. and I'm thinking about cases where more than just 2 functions would have some [partially] common constraints - if the coder decided to make things more concise, that decision should probably be carried out to the docs as well.
so supporting the where or alias as a way to cut down on repetition over constraints can be beneficial to the docs too
Last updated: Jun 16 2026 at 16:19 UTC