Richard Feldman said:
regarding knowing types - something I realized the doc doesn't address is how this would work:
user = Decode.decode(bytes, Json.utf8)?this is an example of wanting to do static dispatch, but where the subject type of the dispatch is in the return value of the function rather than its first argument
definitely possible for the type checker to do, and I think decoding is a valuable use case that abilities offer that is worth bringing over to static dispatch, but I hadn't thought about that scenario, what the syntax would look like, etc.
here's an idea for how the syntax could work:
change the where syntax to this:
List.isEq : List a, List a -> Bool
where a.{ isEq : a, a -> Bool }
crucially, the type uses the full function type, including all of its arguments, rather than the method style of where a.isEq(a) -> Bool as proposed in the doc.
This change makes it possible to specify a where in which the type being statically dispatched on does not have to appear in the first argument position.
a longer one would be e.g.
Dict.insert : Dict k v, k, v -> Dict k v
where
k.{ isEq : k, k -> Bool, hash : k, hasher -> Hash },
hasher.{ u8 : ..., etc. },
and then from there we can have functions where the type being dispatched on appears in the return position:
Json.decode : List U8 -> Result val Problem
where val.{ decode : List U8, JsonFmt -> Result val Problem }
a potential syntax for invoking static dispatch on return types could be _. like so:
Json.decodeUtf8 : List U8 -> Result val Problem
where val.{ decodeBytes : List U8, JsonFmt -> Result val Problem }
Json.decodeUtf8 = \input ->
_.decodeBytes(input, jsonUtf8Fmt)
for a minimal example, so if you put this into the repl...
\input -> _.decodeBytes(input)
...it would infer this type:
input -> output
where output.{ decodeBytes : input -> output }
I think a downside of this compared to Abilities is that in the Abilities world, the type is kind of mind-bending but the call site looks totally normal, because you're just calling what appears to be an ordinary function (in the Ability's module)
whereas in this one the mind-bendingness appears sooner, namely in the value position
like it's more obvious at the call site that something unusual is happening
(which I guess is in some ways an upside, but mostly still feels like a downside to me haha)
but I do think this is worth preserving in the static dispatch world, because I really enjoy how Roc today has the ergonomics of JavaScript's JSON.parse() but with eager validation, and I wouldn't want to give that up :big_smile:
also, I believe the above should work fine with @Brendan Hansknecht's #ideas > Revamped Encode and Decode design
because all that requires is the "dispatch can happen on return type instead of arg type" capability, which this design provides
Yeah, I think something like this would work well. Though I would very much like some form of alias for those records. Otherwise type signatures will get absolutely crazy
yeah totally
there's a section in the doc about that
"Type Aliases for where Constraints"
I'm not sure about the _. vs requiring a name for those types of used. I think it might be better to force a name so it would be Decode.decodeBytes
I don't think that can work
I guess depending on what you mean
like what would the implementation of Decode.decodeBytes look like?
the body of that function I mean
It wouldn't have an implementation
It would just be part of a interface/trait/implementes alias.
but in this proposal we don't have those :thinking:
I think it would help to show a code example of how someone would define decoding in userspace, provide two different implementations, and then call it to actually perform some decoding
I don't think it can be done via static dispatch without having something like _. but maybe I'm missing something! :big_smile:
I am suggesting that decode should still be explicit even if not a "ability" but maybe that is the same thing as an ability. I'm not sure.
hm, I don't follow
I think I need to see a code example, sorry :sweat_smile:
Json.decodeUtf8 : List U8 -> Result val Problem
where val.{ decoder : Decode.Decoder val }
Json.decodeUtf8 = \input ->
jsonFmter = json.formatter(input)
Decode.decodeWith(_.decoder, jsonFmter)
Would instead be:
# Decode Module
Decoding implements { decoder : Decode.Decoder val }
# Json Module
import Decode exposing Decoding
Json.decodeUtf8 : List U8 -> Result val Problem
where val implements Decoding
Json.decodeUtf8 = \input ->
jsonFmter = json.formatter(input)
Decode.decodeWith(Decoding.decoder, jsonFmter)
I think we still want something explicit that is similar to an ability for this.
It also would be used when you want an explicit contract with multiple methods
Static dispatch would still work the same for true methods that take the first argument of a specific type.
hm, I'm still missing something - in that code example, Decode.decodeWith is called but never defined - where would that be defined (and what would it look like)?
still would be the same as stuff here: https://github.com/bhansconnect/roc-msgpack/blob/efb6ab52b1cc71e5b7306910fa18296df5c75d0a/package/FutureDecode.roc#L121-L133
I can write up a full gist if you want
gotcha
so how would a custom type implement Decoding?
like in the proposal above, it's in the usual way of "if I want to implement custom decoding for my Foo type in the Foo.roc module, I expose Foo.decode and that's it"
I guess I am saying that I want explicit interfaces of some sort. I think that the method dispatch should just work, but the functions that are not methods should be required to go through an explicit interface
Probably would be opt in like abilities today. I mean we still need an opt in system so we can have auto derive for eq, hash, encode, decode, etc
But could also be implicit by just exposing Foo.decoder
I am realizing that I maybe arguing for essentially abilities through static dispatch or keeping both, but I think explicit interfaces are really important. I also am really not a fan of magic functions like _.decode. I think it should be tied back to something. This feels different than just static dispatch for methods.
hm, ok that feels like a pretty dramatically different proposal :sweat_smile:
seems worth a separate thread!
hmm. I guess I have to figure out how to formulate everything. Cause I feel like static dispatch makes sense for methods, but expanding it magically to _.decode feels like adding a totally unrelated feature into static dispatch
Like it is saying magically find the correct module and call the decode function in it essentially.
Wait, would this proposal mean that decode no longer supports structural types?
Cause only nominal types get static dispatch
no, we can infer it in the same way
So static dispatch sometimes works for structural types. But only with _.fn syntax?
no, that's unrelated
like for example it also would work for equality on records
{ a: "foo" }.isEq({ b: "foo" }) would work
Hmm....is that just compiler magic then? Feels weird to say static dispatch doesn't work with structural types, but I guess it does kinda.
Cause I thought the core of static dispatch was it is only for nominal rypes
Though maybe it is only for nominal types if the user is defining static dispatch, but the compiler itself is allowed exceptions
well I'd say the core idea is about looking up implementations based on type information
(which can also be said of abilities)
it's about looking them up using a different algorithm from abilities, with different tradeoffs
Just to confirm a user would never be able to add to static dispatch for a structural type, correct? Those all have to be 100% compiler auto derived (like abilities today)
right
ok, here's an alternate design idea based on #ideas > custom types
Decoder fmt val :=
List U8, fmt -> Result val DecodingErr
where
val.{ decode : List U8, fmt -> Result val DecodingErr },
fmt.{ ...etc },
decode : List U8, fmt -> Result val DecodingErr
where fmt.{ ...etc }
decode = \bytes, fmt -> Decoder.0(bytes, fmt)
so the idea is that if you have a custom type wrapper around a function, you can access that function with Decoder.0
and doing that would only be supported if it has a where clause
which includes the function type itself somewhere
this is kind of a sketch (syntax doesn't feel quite right haha) but the idea is that instead of _. doing inference at the call site, you use a custom type to explicitly write out the signature of the function you expect
and then dispatch on that
I guess a simpler version of that design would be to let you literally just write the type annotation of the function directly:
decode : List U8, fmt -> Result val DecodingErr
where
val.{ decode : List U8, fmt -> Result val DecodingErr },
fmt.{ ...etc },
and I guess the rule could be that if you write a function with no implementation, which has a where clause that requires one of the type variables in it to have a method with that exact type...
(so, val in this case, because this function's type is List U8, fmt -> Result val DecodingErr and val.{ decode : <that exact type> })
...then the implementation becomes inferred to be "go look up what val is and dispatch to that function"
and we could give an error (which would compile to a crash) if you have a standalone type annotation which doesn't fit this form, which would end up working the same way as today (in terms of using standalone type annotations for prototyping)
here's a more explicit idea:
decodeBytes : List U8, fmt -> Result answer DecodingErr
where
answer.{ decode : List U8, fmt -> Result answer DecodingErr },
fmt.{ ...etc },
decodeBytes = \bytes, fmt ->
import answer exposing [decode]
decode(bytes, fmt)
so basically you can import using static dispatch to figure out what module is being imported, and then call functions from it
import from a type variable, that is
so you'd get an error if the type variable you were importing from was unsupported, e.g. it wasn't involved in a where clause with a particular shape etc.
could also do like:
import answer as Decode
Decode.decode(bytes, fmt)
what I like about this is that it isolates where the lookup is happening: the import is taking the type variable as its input and then giving you access to a module based only on that information
and then everything before and after the import works as expected
I wonder if some extra syntax could make it look more distinct from a normal import, e.g.
import answer.* as Decode
Decode.decode(bytes, fmt)
oh actually maybe it could be as in the type signature:
decodeBytes : List U8, fmt -> Result answer DecodingErr
where
{ decode : List U8, fmt -> Result answer DecodingErr } as Decode,
DecodingFmt fmt,
decodeBytes = \bytes, fmt ->
Decode.decode(bytes, fmt)
so the idea would be that in the type signature's where you can use as to name the module where the function(s) get found, and then you can call them using that module qualifier in the function body
It's complex, but I don't see a better way to do it
i think when it comes to Decode you can't really have it figure the type out for itself. you'd need some kind of declaration to tell it what module to look for decode in, because there's no way to otherwise express "this function's implementation depends on the type of the first parameter of the Ok variant of the return type"
most of the other uses of static dispatch have been "this function's implementatino depends on the type of the first parameter" which is a lot simpler to do, and can be the default
yeah what I like about this design is that it's probably surprising that you could use as in this way, but it does generally fit with what as does (that is, give a name to something that was already in scope), and - like as in general - I think it would be pretty rarely used
I feel like thinking in a single function for isn't quite right either. You will want to be able to wrap and import a group of functions
sure, that definitely works in this design!
How?
here's an idea for how that could look:
Decoder val fmt := List U8, fmt -> DecodeResult val
where DecoderFormatting fmt
# how you specify a group of `where` constraints
DecoderFormatting fmt : where fmt.{
u8 : Decoder U8 fmt,
u16 : Decoder U8 fmt,
...etc
}
decodeBytes : List U8, fmt -> Result val DecodingErr
where
val.{ decode : List U8, fmt -> Result val DecodingErr } as Decode,
DecoderFormatting fmt, # how you add a group of `where` constraints
decodeBytes = \bytes, fmt ->
Decode.decode(bytes, fmt)
so the where fmt.{ ... } and where val.{ ... } essentially mean "all the functions in { ... } can be found in the module where the fmt type is defined (or where the val type is defined, in the other case)"
and the alias format of DecoderFormatting fmt : where fmt.{ would mean "this is a where alias, so adding where DecoderFormatting fmt to a given where clause essentially inlines everything in the alias into that where
and then a where alias is how you specify groups of related statically-dispatched functions, like we do today with Abilities
a simpler example:
Eq a : where a.{ isEq : a, a -> Bool }
List.isEq : List a, List a -> Bool
where Eq a
i like that syntax a lot actually
lot nicer than the implements stuff we have today
I wonder if we could also add default functions with ? in the where record :fear:
although maybe that isn't necessary
but it would allow default methods (written in terms of the other functions in the ability) that could be overwritten
yeah that sounds unnecessary :sweat_smile:
a way this could be taught:
First, teach that \val, arg -> val.foo(arg) has this inferred type:
val, arg -> ret
where val.{ foo : val, arg -> ret }
Then, explain that in this syntax, where val.{ foo : val, arg -> ret } means:
.{ foo : val, arg -> ret } part means that we're expecting a module to have a top-level foo function with this typeval. at the front means "the module we expect to have that function is the module where val is defined"as to name the module where val is defined.decode example).so at a type level, the whole thing is just about how we find modules
and it's just the method-call syntax of foo.bar(baz) that has anything to do with the first argument
which is to say, the first argument is the one that gets chosen for val.{ ... } in the type
completely agree with everything you just said
should we go with Eq a or a.Eq as a shorthand?
personally I'm liking the second one
fits in with the module theme of using dots
that could work!
so then multiples would be like where a.Eq, a.Hash?
yeah, that sounds good
oh, I realized a problem: the type alias could have other type variables
e.g. Container a elem
hm, although actually I'm not sure if we want to support that :thinking:
so maybe that's a feature, not a bug haha
too complex I think. it should only have one type variable which is the type in question from the same module
Richard Feldman said:
here's a more explicit idea:
here's a variation on this design:
decodeBytes : List U8, fmt -> Result answer DecodingErr
where
answer -> { decode : List U8, fmt -> Result answer DecodingErr },
fmt -> { ...etc },
decodeBytes = \bytes, fmt ->
import answer as Answer
Answer.decode(bytes, fmt)
so this is kind of similar to how we use -> in module params:
where
answer -> { decode : List U8, fmt -> Result answer DecodingErr },
it's like "given this answer type variable, we get back a module with decode which has this type
and maybe we should have the "method type in where" option available as syntax sugar, because it looks more self-explanatory and might be helpful for teaching. So this:
where
elem.equals(elem) -> Bool
...would mean the same thing as this:
where
elem -> { equals : elem, elem -> Bool }
there's an argument for having just one syntax for it, so you don't have to learn two. The counterargument is that the first one is a lot nicer to read, and easier to understand (especially for a beginner), and is the one that will come up way more often. The second one only really needs to exist for uncommon use cases like decoding.
a potential counterargument to that is that you'd usually have type aliases anyway (e.g. where elem.Eq), but a counterargument to that is that this is the syntax that would be used to define those type aliases, so you'd still be exposed to it when interacting with the definition of the aliases (or creating your own)
I guess another potential syntax that combines the above two:
where
elem -> equals : elem, elem -> Bool
so in that design, these would mean the same thing:
Eq a : where
a -> equals : a, a -> Bool
Eq a : where
a.equals(a) -> Bool
Couldn’t we let you do many of the method syntax and have only that variant?
The variable on the left of . already tells you which type it’s on
I think we could just let you define as many methods as you want on the same or different type variable
the problem is that decode doesn't take the dispatched type as its first argument:
where
answer -> { decode : List U8, fmt -> Result answer DecodingErr },
so you can't just write where answer.(...) -> ...
because that's the syntax for answer being the first argument
but in the case of decode it's not the first argument, it's in the return type
Ah right, sorry
yeah if we only ever had these in the "first argument" position, then we would totally just do the nicer syntax
but then we lose out on being able to decode directly into things, which would be a way bigger downside than having some less-nice syntax some of the time :big_smile:
I guess this would work if we used :: instead of . for module qualification:
where
answer::decode(List U8, fmt) -> Result answer DecodingErr,
yeah, although then we miss out on . being the universal autocomplete syntax
feels like a pretty invasive solution to a really niche problem :thinking:
where
elem.equals(elem) -> Bool
where
answer -> decode(List U8, fmt) -> Result answer DecodingErr
I don't like the double -> in that :sweat_smile:
where
elem.equals(elem) -> Bool
where
decode(List U8, fmt) -> Result answer DecodingErr for answer
where
module [answer] as m,
m.decode(List U8, fmt) -> Result answer DecodingErr,
:sweat_smile:
where
elem.equals(elem) -> Bool
where
(module answer).decode(List U8, fmt) -> Result answer DecodingErr
Not bad for a niche feature
yeah the module keyword makes it really clear that it's a module :big_smile:
Should it be (module [foo, bar]). in case you need two types?
hm I don't think we can support that
the key is that we have to figure out which module to look in to find the decode function
Yeah, I mean the case where both types are in the same module
(module answer).decode would mean "go find the module where answer is defined; that's where the decode function can be found
I guess it would resolve to the same module anyway
yeah
and then I guess you could do:
where
elem.equals(elem) -> Bool
where
(module answer as M).decode(List U8, fmt) -> Result answer Bad
...and then call M.decode in the function body
hm actually no
because if there's more than one, it gets awkward
Yeah, that’s what I was going for with the separate module “entry”
where
(module answer as M).decode(List U8, fmt) -> Result answer Bad
(module answer as M).other(List U8) -> Result answer Bad
vs like
where
(module answer).decode(List U8, fmt) -> Result answer Bad
(module answer).other(List U8) -> Result answer Bad
Agus Zubiaga said:
where module [answer] as m, m.decode(List U8, fmt) -> Result answer DecodingErr,:sweat_smile:
This
I think it's fine if it's a bit more verbose
like repeating the (module answer). each time
because if you're doing that, you'll prob put it in a type alias, so it'll only be verbose once
and then inside the function body you do:
import answer as M
Yeah, that’s probably fine
a thing I like about this:
where
(module answer).decode(List U8, fmt) -> Result answer Bad
where fmt.DecodeFormatting,
(module answer).other(List U8) -> Result answer Bad,
...is that it (correctly) suggests that each of these has their own type variable namespaces, and that (for example) the name fmt is local to that where clause and isn't shared across clauses.
as opposed to like (module answer).{ decode : ..., other : ... } where I'd assume all type variables inside the curly braces share one namespace, like they do in record types
...although I guess this could also achieve that:
where
module answer as M,
M.decode(List U8, fmt) -> Result answer Bad
where fmt.DecodeFormatting,
M.other(List U8) -> Result answer Bad,
Yeah, I find that last one nicer overall, but this is pretty niche so I would be ok with either
I think something that is important is grouping all functions from a module. I think we should design the syntax to aggregate by module (aka type variable).
This is mostly important when you have multiple type variables that import multiple functions. Grouping helps with recognizing what exact variable can or can't do
there was a concern previously about type definitions that looked like
func : a -> f where
a implements { reserve: a, Num b -> c },
c implements { append: c, Num d -> e },
e implements { reverse: e -> f }
I wonder if this could also be written as
func : a -> d where
a.reserve(Num b).append(c).reverse() -> d
maybe not, but...
That works as long as there is only one chain.
Richard Feldman said:
...although I guess this could also achieve that:
where module answer as M, M.decode(List U8, fmt) -> Result answer Bad where fmt.DecodeFormatting, M.other(List U8) -> Result answer Bad,
just to clarify, it seems like this is the latest suggestion, one which people are positive on, and a function that uses it would look like:
decode_bytes : List U8, fmt -> Result answer [DecodeFail DecodingErr, GenFail GenErr]
where
module answer as M,
M.decode(List U8, Str, fmt) -> Result answer DecodingErr
where fmt.DecodeFormatting,
M.gen_title(List U8) -> Result Str GenErr
decode_bytes = |bytes, format|
import answer as Ans # I presume it doesn't have to be named M?
title = Ans.gen_title(bytes)?
Ans.decode(bytes, tytle, format)
Is anyone unhappy with this syntax?
syntax lgtm but i wonder if there's a way to make this less verbose.. don't have any good ideas off the top of my head other than abilities/aliases but will think
Sounds great! Make sure to wear a helmet when you think that hard
whats the fun in that
You have a point (where your head used to be)
Last updated: Jun 16 2026 at 16:19 UTC