Originally posted in Github: https://github.com/rtfeldman/roc/pull/2721#issuecomment-1069279849
I have a bad feeling about this syntax.
declaration : field, field -> Type field | field supports Ability
Looking from far away it reads a bit weird ...
Maybe it's the fact that | is so thin, or that I instantly read it as: "this pipes to that" but skimming trough definitions with abilities, my eyes hurt.
Maybe it's because in Haskell typeclasses are written in front, and in Elm we usually do
stuff : Config data msg -> data -> Things data msg
Meaning, we we treat a type variable inside this function is specified at the start of the function declaration.
My proposal:
declaration : field supports Ability @ field, field -> Type field
and sortBy would read like this
sortBy : field supports Ordering @ List elem, (elem -> field) -> List elem
As a final suggestion I would remove supports and replace it with a symbol too. Why? Its a letter construct that is not essential for the reading of that line.
Now, Editor is the reason why I am writing this. Roc's syntax so far was really unambiguous and straight forward in a sense that the decision tree for the user was really simple. With each key we could know that you have done with that and moved to another thing (from writing a function name to listing arguments, from listing arguments to specifying return type or adding another argument etc.).
With the ability declaration at the end I am not sure how to lend it as a easy affordance that you are done specifying return type, or that you'd like to add or modify one of the ability specifications. I am not saying it is impossible, and that language must bend to the needs of the Editor.
I am just stating my concern that A E S T H E T I C S are off with current design of the abilities syntax. :)
Please feel free to chime in with the ideas on the syntax!
None of these look great
sortBy : List elem, (elem -> field) -> List elem | field supports Ordering
sortBy : Ordering field => List elem, (elem -> field) -> List elem
sortBy : field has Ordering => List elem, (elem -> field) -> List elem
Though I generally prefer to have the constraint first, and then the type. compare haskell's constraint syntax
sortBy : Ord field => List elem, (elem -> field) -> List elem
also how do we format any of these on multiple lines?
also, how do we deal with multiple abilities on the same type variable, or multiple variables, each with separate abilities
| foo supports Bar, Baz, blah supports X, Y
that's the current proposed syntax, at any rate
it works because type variable names are always lowercase and ability names are always uppercase
I'm personally not a fan of the "constraint comes first" syntax because - compare these two:
Dict.update : Dict k v, k, (v -> v) -> Dict k v | k supports Hashing, Equating
Dict.update : k supports Hashing, Equating => Dict k v, k, (v -> v) -> Dict k v
it's a dictionary; I know the key needs to support hashing and equating!
I'm reading the docs for Dict.update because I want to know the shape of the function I'm going to be calling
because I use dictionaries all the time but not Dict.update very often
so with the constraint up front, I have to read past the least useful information every time before getting to the part of the type definition that actually has information I didn't know already
technically to fully understand the type, both pieces of information need to be in there somewhere, but the constraints are rarely what I came to the function to look up
so I prefer having them at the end, so I can learn what I need to learn first and then stop reading
in the common case where I already know what the constraint is going to be based on the operation
Maybe we could take some inspiration from Rust's multiline where syntax?
pub fn insert_all<K, V, I>(map: &mut SendMap<K, V>, elems: I)
where
K: Clone + Eq + Hash,
V: Clone,
I: Iterator<Item = (K, V)>,
{
for (k, v) in elems {
map.insert(k, v);
}
}
Although we don't have the curly braces, just indentation
update : Dict k v, k, (v -> v) -> Dict k v
where k supports Hashing, Equating
update = \dict, key, func ->
# function body
Personally I like the multiline syntax with where + supports, because it makes the type signature itself easy to read (to Richard's point, that's probably what you care about more than the abilities supported most of the time) and it doesn't look like syntactical gobbledygook to someone who is first seeing the language.
Editor is the reason why I am writing this. Roc's syntax so far was really unambiguous and straight forward in a sense that the decision tree for the user was really simple. With each key we could know that you have done with that and moved to another thing (from writing a function name to listing arguments, from listing arguments to specifying return type or adding another argument etc.).
With the ability declaration at the end I am not sure how to lend it as a easy affordance that you are done specifying return type, or that you'd like to add or modify one of the ability specifications.
this is a fair point, although once you've finished autocompleting the return value, we could make the w key show an autocomplete for adding a where clause
update : Dict k v, k, (v -> v) -> Dict k v
update : k supports Hashing, Equating
update = \dict fn ->
_
Just writing it out it seems that even where is not needed if we allow it to be written (indented) in the next line?
update : Dict k v, k, (v -> v) -> Dict k v
k supports Hashing, Equating
update = \dict fn ->
_
view : Config state render, state -> UI state renderer
state supports Rendering, Memoising
renderer supports LowLevelGraphics
view = \cfg s ->
_
Yes, I think that new line looks great! :)
I like the new line with or without where. I think the new line in general is really important for readability
better still we keep out the supports at all.
update : Dict k v, k, (v -> v) -> Dict k v
k | Hashing, Equating
update = \dict fn ->
_
view : Config state render, state -> UI state renderer
state | Memoising, Encoding
renderer | LowLevelGraphics
view = \cfg s ->
_
I totally agree that we should read this as that as X supports Y, but I'd remove a whole word from definitions.
I see that is _harder_ to explain, but one of the Roc's beauties is that you are not writing English prose that we have to interpret, as in "type alias XY, data Y" but straight any letter is a Symbol (except header thingy).
Finding good symbol for being association for supports and easy one to reach on the keyboard would help reading and writing Roc.
Is it true that whenever you have a type variable that you need to specify what that type variable does, unless you are passing it back as a result.
foo : Some a -> Str
foo ... .
Basically result of foo doesn't depend on a so we can safely way that a = *
foo : Some * -> Str
foo = \s -> _
If we return a then also we might not care what it does, as in
baz : Some a -> Result Errors a
baz = \s -> _
But if foo uses a it has to state which ability it uses.
foo : Some a -> Str
a | StrEquating
foo = \ s -> Str.append "foo" (equate a)
Yes - * is a synonym for a type variable that only appears once, and hence is unbound
yes I am trying to see what are the cases when you actually write out abilities.
I think needing to state an ability is kind of a logistical requirement. How much can you do with an unknown type?
Quick comment on syntax:
I like the new line with or without where. I think the new line in general is really important for readability.
what would a multiline type annotation look like without a when equivalent?
e.g.
update :
Dict k v,
k,
(v -> v)
-> Dict k v
where
k supports Hashing, Equating
update = ...
how would that look if it didn't have a separator like when?
or what if the return type itself was so long it needed to be multiline, e.g. instead of -> Dict k v it was something like
->
Dict
k
v
update :
Dict k v,
k,
(v -> v)
-> Dict k v
Is surprisingly painful to read by itself. I think the where doesn't help or hurt it.
Oh, I think I know the issue. is this nicer?
update :
Dict k v,
k,
(v -> v)
-> Dict k v
Hmmm....maybe not quite right either.....
Anyway. I guess I am just pointing out that I dont think the where really changes the readability of theses examples. I think the default syntax is just rough to read.
I wouldn't actually use it in a case like this - it comes up more if the types are really long and if it's all on one line the line is huge
but the bigger question is more about what would it look like without where?
Just put the spec where you would put the where.
update :
Dict k v,
k,
(v -> v)
->
Dict
k
v
k supports Hashing, Equating
This is pretending the return type had a super long name
With shorter names:
update :
Dict k v,
k,
(v -> v)
-> Dict k v
k supports Hashing, Equating
Not sure why the where matters
Just the tabbing should matter
:thinking: here's an idea - what if instead of where it was a comma?
update : Dict k v, k, (v -> v) -> Dict k v,
k supports Hashing, Equating
update :
Dict k v,
k,
(v -> v)
-> Dict k v,
k supports Hashing, Equating
so then there's no need for an indentation rule
and comma pretty universally indicates "there's more here, we're not done yet" so it's pretty clear to me that what comes after the comma is more information that goes with the type annotation
also works on a single line, so you can write it that way even if the formatter will change it to multi-line by convention:
update : Dict k v, k, (v -> v) -> Dict k v, k supports Hashing, Equating
(I agree with the idea that we should have multiline be the formatting convention!)
That looks off due to the comma between Hashing and Equating.
Makes it look like Equating is just it's own thing.
could do like k supports Hashing & Equating or k supports Hashing + Equating
That works. Though I'm sure it will cause some new users to think of tuples.
Just the fact that there is a comma in general. Not a big deal, just a note
all the ML family languages that have tuples use parens around them, so should be ok I think!
Richard Feldman said:
also works on a single line, so you can write it that way even if the formatter will change it to multi-line by convention:
update : Dict k v, k, (v -> v) -> Dict k v, k supports Hashing, Equating
This could be read as: update is getting three parameters and returning two values :thinking:
How about update : Dict k v, k, (v -> v) -> Dict k v & k supports Hashing, Equating ?
Very hard to distinguish what where is it k & v
The root of the problem is that we are tying to write two orthogonal type definitions.
When we join them we end-up with a sausage long definition. Our eyes are maybe not trained to look for anything beyond return type.
I agree, convention should be to put it on its own line, how about this:
update : Dict k v, k, (v -> v) -> Dict k v
& k supports Hashing, Equating
Me gusta newline mucho!
but that '&' maybe can be '@'
as in ~a~bility
update : Dict k v, k, (v -> v) -> Dict k v
@ k supports Hashing, Equating
That looks good, one small disadvantage is that @ is a bit harder to type on azerty keyboards
fair point!
how about
update : Dict k v, k, (v -> v) -> Dict k v
: k supports Hashing, Equating
I'm leaning towards using a different symbol to convey a different meaning but it's a good contender.
update : Dict k v, k, (v -> v) -> Dict k v
| k supports Hashing, Equating
update : Dict k v,
k,
(v -> v) ->
Dict k v
| k supports Hashing, Equating
# Curses on thou who formats code like this! Let the editor do its bidding, ye!
Maybe all along it was just enforcing the newline rule?
update : Dict k v, k, (v -> v) -> Dict k v
| k supports Hashing, Equating
| v support Modulating
update = \dict key value ->
This breaks another thing, and that is direct connection of the parameter and its declaration.
They are little too separated like this ...
I'm not sure it is possible to improve on that aspect without making bigger sacrifices.
Yes, something gotta give :)
I like this form quite a bit. Find it very readable.
update : Dict k v, k, (v -> v) -> Dict k v
| k supports Hashing, Equating
| v support Modulating
update = \dict key value ->
Almost like abilities are like a back of the envelope of the products.
Where you check facts of the thing, almost as a necessary supplement.
Agreed, the compact multiline form Brendan Hansknecht quoted is pretty readable.
Presumably there shouldn't be a pluralization difference in the example (supports not support)?
When using types which require abilities (as Dict may require Hashing and Equating), do those need to be spelled out if the function is not using them directly? Can Roc accurately infer required abilities even if they're not declared?
Could we also do something like the following?
update : d, k, f -> d
| d is Dict k v
| f is v -> v
| k supports Hashing, Equating
| v supports Modulating
update = \dict key value -> ...
I'm not sure it improves readability, but I like it better than the one-argument-per-line style
Kevin Gillette said:
When using types which require abilities (as Dict may require Hashing and Equating), do those need to be spelled out if the function is not using them directly? Can Roc accurately infer required abilities even if they're not declared?
Yes, that should be derived.
Abilities need to be specified if you are using type variable and somewhere inside the expression you are using some of the ability functions or someother function which specifies that you need to have certain abilities, then you also need to spell out those abilities in your definition, if you are using type variables.
Thanks for poking at this syntax @Zeljko Nesic! The "add newlines" syntax in @Brendan Hansknecht's latest post looks great, and most importantly I don't want to replace supports with a symbol. Since the ability syntax might be read by noobs even more often than by experts, it should prioritize clarity over terseness (even if that costs us a reserved word).
ok, here is a concrete proposal for what syntax to try next:
Dict.insert : Dict k v, k, v -> Dict k v
| k has Hash & Eq
Dict.insert : Dict k v, k, v -> Dict k v
where k implements Hash & Eq
## Definition of the [DecoderFormatting] ability
DecoderFormatting has
u8 : Decoder U8 fmt | fmt has DecoderFormatting
u16 : Decoder U16 fmt | fmt has DecoderFormatting
u32 : Decoder U32 fmt | fmt has DecoderFormatting
## Definition of the [DecoderFormatting] ability
DecoderFormatting implements {
u8 : Decoder U8 fmt where fmt implements DecoderFormatting,
u16 : Decoder U16 fmt where fmt implements DecoderFormatting,
u32 : Decoder U32 fmt where fmt implements DecoderFormatting,
}
Json := JsonState has [
EncoderFormatting {
u8: encodeU8,
u16: encodeU16,
},
DecoderFormatting {
u8: decodeU8,
u16: decodeU16,
},
]
Json := JsonState implements [
EncoderFormatting {
u8: encodeU8,
u16: encodeU16,
},
DecoderFormatting {
u8: decodeU8,
u16: decodeU16,
},
]
I think we should try this because:
implements is a fine keyword to reserve; seems very unlikely to cause conflicts with userspace. It's less concise than has but much more self-descriptive. Considering how infrequently abilities appear in type signatures in practice at this point, this seems like a good trade-off to make.| is more concise than when, some have found it harder to read and I'd like to try out the more verbose but explicit way and see how it feels in comparison to the current | syntax.{ ... } when defining new abilities is meant to mirror the syntax of how they're specified on opaque types, so you don't have to remember that one of them uses braces and the other doesn't; instead, they both do.I also prefer implements over supports (the runner-up) because I don't like how this reads:
MyOpaqueType := [Foo, Bar Baz] supports { Hash { hash : ... } }
it suggests that it's about to provide a complete list of operations the opaque type supports, but that's inaccurate - the complete list of operations the opaque type supports includes whatever functions this module exposes which involve that type!
in contrast:
MyOpaqueType := [Foo, Bar Baz] implements { Hash { hash : ... } }
I'm not sure why, but this to me feels more additive - like "hey it implements these things, but other implementations may exist too" whereas "supports" feels more exhaustive - like "this is everything it supports"
what are peoples' thoughts on this specific design? How would you feel about our adopting it? Anything you'd change?
(keep in mind that we've already had a ton of discussion about this, so I'd like to avoid having the perfect be the enemy of the good since there's a strong consensus that the status quo isn't good, and if we want to change it we do actually have to pick a specific design to change it to!)
I think this reads much more naturally. I would be a little sad to lose where to the language but that’s nbd.
I like it, though I would argue that in some classes of problems it would be pretty common to use types with abilities, so it would be quite common to see where ... implements ... in type signatures. You might even see it in every function in a file. So if we care about verbosity, i think implements is the wrong choice.....all that a said, i prefer implements and the added verbosity.
oh where would only be used in type variables - unlike implements, it doesn't appear in value definitions
like DecoderFormatting implements { ... } means that implements has to be a reserved name for identifiers in general (since otherwise it would look to the parser like the beginning of a pattern match on a tag named DecoderFormatting), but since where only appears in type annotations, the only restriction we need there is that you can't choose where as the name for a type variable
Ah ok, that’s great
Richard Feldman said:
- the addition of the
{...}when defining new abilities is meant to mirror the syntax of how they're specified on opaque types, so you don't have to remember that one of them uses braces and the other doesn't; instead, they both do.
I think this is easiest to justify change. :100:
Could we do this?
Dict.insert : Dict k v, k, v -> Dict k v
where k implements [ Hash, Eq ]
we could, but it's most common to have exactly 1 ability restriction like that
in fact Dict and Set are the only examples I can think of that use more than one! :big_smile:
so then I'd expect all the others to have implements [Eq]
which would seem a little unnecessary to me, but maybe not?
Well, I could imagine a lot of things having something like Show as well
functions having that restriction? :thinking:
that would surprise me, honestly
Sorry, nvm I was thinking about types
ahh gotcha!
yeah there we do use braces, because it's definitely the norm to have more than 1 :laughing:
There might a point about uniformity there
but yeah, I guess & is fine for restrictions
I also prefer the clarity of your suggested syntax over the brevity of the alternatives.
If verbosity becomes a legitimate issue, I wonder if we couldn’t get around it using the editor instead of using less clear syntax. Perhaps an option or a plugin that collapses the words to symbols. Or maybe an optional mode that only shows type annotations on hover. Not sure either of those are good ideas, but maybe there’s something along those lines to help with verbosity if it’s a concern. Even if Roc was meant to be edited with a normal text editor though, I think I’d still prefer the clearer syntax.
I like it. :+1:
I wonder if we couldn’t get around it using the editor instead of using less clear syntax
Yeah, there's been several cases where I imagined it would be nice to have a view plugin to display code nicely organized/transformed according to your personal preferences.
I really like the new syntax. i would expect abilities to be used most of the time in packages and platforms and therefore been read more often than written. (of course i might be wrong here) so i would prefere a verbose but clear syntax over a concise one, that would require you to "learn" it first, before you can read it.
I bet even someone who is new to the language and / or hase never worked with abilities before, would have at least a general idea, what where k implements Stuff means immediately.
although | is more concise than when, some have found it harder to read and I'd like to try out the more verbose but explicit way and see how it feels in comparison to the current | syntax.
at least for this reads a lot clearer. i'm quite new to writing roc and the | syntax always confused me a little. in my head it always read like a weird Or-statement at first and i had to decompose it, to understand it. meanwhile the where syntax reads a lot clearer for me. and i don't have to think about it really.
Last updated: Jun 16 2026 at 16:19 UTC