Stream: ideas

Topic: static dispatch - decoding


view this post on Zulip Richard Feldman (Nov 10 2024 at 20:27):

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:

view this post on Zulip Richard Feldman (Nov 10 2024 at 20:27):

change the where syntax to this:

List.isEq : List a, List a -> Bool
    where a.{ isEq : a, a -> Bool }

view this post on Zulip Richard Feldman (Nov 10 2024 at 20:28):

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.

view this post on Zulip Richard Feldman (Nov 10 2024 at 20:28):

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. },

view this post on Zulip Richard Feldman (Nov 10 2024 at 20:29):

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 }

view this post on Zulip Richard Feldman (Nov 10 2024 at 20:31):

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)

view this post on Zulip Richard Feldman (Nov 10 2024 at 20:32):

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 }

view this post on Zulip Richard Feldman (Nov 10 2024 at 20:33):

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)

view this post on Zulip Richard Feldman (Nov 10 2024 at 20:33):

whereas in this one the mind-bendingness appears sooner, namely in the value position

view this post on Zulip Richard Feldman (Nov 10 2024 at 20:34):

like it's more obvious at the call site that something unusual is happening

view this post on Zulip Richard Feldman (Nov 10 2024 at 20:34):

(which I guess is in some ways an upside, but mostly still feels like a downside to me haha)

view this post on Zulip Richard Feldman (Nov 10 2024 at 20:35):

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:

view this post on Zulip Richard Feldman (Nov 10 2024 at 20:36):

also, I believe the above should work fine with @Brendan Hansknecht's #ideas > Revamped Encode and Decode design

view this post on Zulip Richard Feldman (Nov 10 2024 at 20:36):

because all that requires is the "dispatch can happen on return type instead of arg type" capability, which this design provides

view this post on Zulip Brendan Hansknecht (Nov 10 2024 at 20:57):

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

view this post on Zulip Richard Feldman (Nov 10 2024 at 20:58):

yeah totally

view this post on Zulip Richard Feldman (Nov 10 2024 at 20:58):

there's a section in the doc about that

view this post on Zulip Richard Feldman (Nov 10 2024 at 20:58):

"Type Aliases for where Constraints"

view this post on Zulip Brendan Hansknecht (Nov 10 2024 at 20:58):

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

view this post on Zulip Richard Feldman (Nov 10 2024 at 20:58):

I don't think that can work

view this post on Zulip Richard Feldman (Nov 10 2024 at 20:59):

I guess depending on what you mean

view this post on Zulip Richard Feldman (Nov 10 2024 at 20:59):

like what would the implementation of Decode.decodeBytes look like?

view this post on Zulip Richard Feldman (Nov 10 2024 at 20:59):

the body of that function I mean

view this post on Zulip Brendan Hansknecht (Nov 10 2024 at 22:11):

It wouldn't have an implementation

view this post on Zulip Brendan Hansknecht (Nov 10 2024 at 22:12):

It would just be part of a interface/trait/implementes alias.

view this post on Zulip Richard Feldman (Nov 10 2024 at 22:20):

but in this proposal we don't have those :thinking:

view this post on Zulip Richard Feldman (Nov 10 2024 at 22:40):

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

view this post on Zulip Richard Feldman (Nov 10 2024 at 22:40):

I don't think it can be done via static dispatch without having something like _. but maybe I'm missing something! :big_smile:

view this post on Zulip Brendan Hansknecht (Nov 10 2024 at 23:13):

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.

view this post on Zulip Richard Feldman (Nov 10 2024 at 23:21):

hm, I don't follow

view this post on Zulip Richard Feldman (Nov 10 2024 at 23:21):

I think I need to see a code example, sorry :sweat_smile:

view this post on Zulip Brendan Hansknecht (Nov 10 2024 at 23:28):

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)

view this post on Zulip Brendan Hansknecht (Nov 10 2024 at 23:29):

I think we still want something explicit that is similar to an ability for this.

view this post on Zulip Brendan Hansknecht (Nov 10 2024 at 23:29):

It also would be used when you want an explicit contract with multiple methods

view this post on Zulip Brendan Hansknecht (Nov 10 2024 at 23:30):

Static dispatch would still work the same for true methods that take the first argument of a specific type.

view this post on Zulip Richard Feldman (Nov 10 2024 at 23:30):

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)?

view this post on Zulip Brendan Hansknecht (Nov 10 2024 at 23:31):

still would be the same as stuff here: https://github.com/bhansconnect/roc-msgpack/blob/efb6ab52b1cc71e5b7306910fa18296df5c75d0a/package/FutureDecode.roc#L121-L133

view this post on Zulip Brendan Hansknecht (Nov 10 2024 at 23:32):

I can write up a full gist if you want

view this post on Zulip Richard Feldman (Nov 10 2024 at 23:32):

gotcha

view this post on Zulip Richard Feldman (Nov 10 2024 at 23:32):

so how would a custom type implement Decoding?

view this post on Zulip Richard Feldman (Nov 10 2024 at 23:33):

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"

view this post on Zulip Brendan Hansknecht (Nov 10 2024 at 23:33):

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

view this post on Zulip Brendan Hansknecht (Nov 10 2024 at 23:34):

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

view this post on Zulip Brendan Hansknecht (Nov 10 2024 at 23:35):

But could also be implicit by just exposing Foo.decoder

view this post on Zulip Brendan Hansknecht (Nov 10 2024 at 23:36):

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.

view this post on Zulip Richard Feldman (Nov 10 2024 at 23:38):

hm, ok that feels like a pretty dramatically different proposal :sweat_smile:

view this post on Zulip Richard Feldman (Nov 10 2024 at 23:38):

seems worth a separate thread!

view this post on Zulip Brendan Hansknecht (Nov 10 2024 at 23:40):

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

view this post on Zulip Brendan Hansknecht (Nov 10 2024 at 23:40):

Like it is saying magically find the correct module and call the decode function in it essentially.

view this post on Zulip Brendan Hansknecht (Nov 10 2024 at 23:41):

Wait, would this proposal mean that decode no longer supports structural types?

view this post on Zulip Brendan Hansknecht (Nov 10 2024 at 23:41):

Cause only nominal types get static dispatch

view this post on Zulip Richard Feldman (Nov 10 2024 at 23:42):

no, we can infer it in the same way

view this post on Zulip Brendan Hansknecht (Nov 10 2024 at 23:43):

So static dispatch sometimes works for structural types. But only with _.fn syntax?

view this post on Zulip Richard Feldman (Nov 10 2024 at 23:44):

no, that's unrelated

view this post on Zulip Richard Feldman (Nov 10 2024 at 23:44):

like for example it also would work for equality on records

view this post on Zulip Richard Feldman (Nov 10 2024 at 23:45):

{ a: "foo" }.isEq({ b: "foo" }) would work

view this post on Zulip Brendan Hansknecht (Nov 10 2024 at 23:45):

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.

view this post on Zulip Brendan Hansknecht (Nov 10 2024 at 23:45):

Cause I thought the core of static dispatch was it is only for nominal rypes

view this post on Zulip Brendan Hansknecht (Nov 10 2024 at 23:46):

Though maybe it is only for nominal types if the user is defining static dispatch, but the compiler itself is allowed exceptions

view this post on Zulip Richard Feldman (Nov 10 2024 at 23:47):

well I'd say the core idea is about looking up implementations based on type information

view this post on Zulip Richard Feldman (Nov 10 2024 at 23:47):

(which can also be said of abilities)

view this post on Zulip Richard Feldman (Nov 10 2024 at 23:47):

it's about looking them up using a different algorithm from abilities, with different tradeoffs

view this post on Zulip Brendan Hansknecht (Nov 10 2024 at 23:49):

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)

view this post on Zulip Richard Feldman (Nov 10 2024 at 23:51):

right

view this post on Zulip Richard Feldman (Nov 11 2024 at 01:33):

ok, here's an alternate design idea based on #ideas > custom types

Decode.roc

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)

view this post on Zulip Richard Feldman (Nov 11 2024 at 01:34):

so the idea is that if you have a custom type wrapper around a function, you can access that function with Decoder.0

view this post on Zulip Richard Feldman (Nov 11 2024 at 01:35):

and doing that would only be supported if it has a where clause

view this post on Zulip Richard Feldman (Nov 11 2024 at 01:36):

which includes the function type itself somewhere

view this post on Zulip Richard Feldman (Nov 11 2024 at 01:38):

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

view this post on Zulip Richard Feldman (Nov 11 2024 at 01:38):

and then dispatch on that

view this post on Zulip Richard Feldman (Nov 11 2024 at 01:42):

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 },

view this post on Zulip Richard Feldman (Nov 11 2024 at 01:43):

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...

view this post on Zulip Richard Feldman (Nov 11 2024 at 01:43):

(so, val in this case, because this function's type is List U8, fmt -> Result val DecodingErr and val.{ decode : <that exact type> })

view this post on Zulip Richard Feldman (Nov 11 2024 at 01:44):

...then the implementation becomes inferred to be "go look up what val is and dispatch to that function"

view this post on Zulip Richard Feldman (Nov 11 2024 at 01:46):

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)

view this post on Zulip Richard Feldman (Nov 11 2024 at 02:46):

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)

view this post on Zulip Richard Feldman (Nov 11 2024 at 02:46):

so basically you can import using static dispatch to figure out what module is being imported, and then call functions from it

view this post on Zulip Richard Feldman (Nov 11 2024 at 02:47):

import from a type variable, that is

view this post on Zulip Richard Feldman (Nov 11 2024 at 02:48):

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.

view this post on Zulip Richard Feldman (Nov 11 2024 at 02:49):

could also do like:

    import answer as Decode

    Decode.decode(bytes, fmt)

view this post on Zulip Richard Feldman (Nov 11 2024 at 02:50):

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

view this post on Zulip Richard Feldman (Nov 11 2024 at 02:51):

and then everything before and after the import works as expected

view this post on Zulip Richard Feldman (Nov 11 2024 at 12:43):

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)

view this post on Zulip Richard Feldman (Nov 11 2024 at 12:57):

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)

view this post on Zulip Richard Feldman (Nov 11 2024 at 13:02):

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

view this post on Zulip Derin Eryilmaz (Nov 11 2024 at 16:24):

It's complex, but I don't see a better way to do it

view this post on Zulip Derin Eryilmaz (Nov 11 2024 at 16:27):

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"

view this post on Zulip Derin Eryilmaz (Nov 11 2024 at 16:29):

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

view this post on Zulip Richard Feldman (Nov 11 2024 at 17:39):

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

view this post on Zulip Brendan Hansknecht (Nov 11 2024 at 17:41):

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

view this post on Zulip Richard Feldman (Nov 11 2024 at 17:52):

sure, that definitely works in this design!

view this post on Zulip Brendan Hansknecht (Nov 11 2024 at 17:59):

How?

view this post on Zulip Richard Feldman (Nov 11 2024 at 18:27):

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)

view this post on Zulip Richard Feldman (Nov 11 2024 at 18:35):

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)"

view this post on Zulip Richard Feldman (Nov 11 2024 at 18:36):

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

view this post on Zulip Richard Feldman (Nov 11 2024 at 18:36):

and then a where alias is how you specify groups of related statically-dispatched functions, like we do today with Abilities

view this post on Zulip Richard Feldman (Nov 11 2024 at 18:38):

a simpler example:

Eq a : where a.{ isEq : a, a -> Bool }

List.isEq : List a, List a -> Bool
    where Eq a

view this post on Zulip Derin Eryilmaz (Nov 11 2024 at 18:46):

i like that syntax a lot actually

view this post on Zulip Derin Eryilmaz (Nov 11 2024 at 18:57):

lot nicer than the implements stuff we have today

view this post on Zulip Derin Eryilmaz (Nov 11 2024 at 18:59):

I wonder if we could also add default functions with ? in the where record :fear:

view this post on Zulip Derin Eryilmaz (Nov 11 2024 at 18:59):

although maybe that isn't necessary

view this post on Zulip Derin Eryilmaz (Nov 11 2024 at 18:59):

but it would allow default methods (written in terms of the other functions in the ability) that could be overwritten

view this post on Zulip Richard Feldman (Nov 11 2024 at 19:02):

yeah that sounds unnecessary :sweat_smile:

view this post on Zulip Richard Feldman (Nov 11 2024 at 19:36):

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:

view this post on Zulip Richard Feldman (Nov 11 2024 at 19:36):

so at a type level, the whole thing is just about how we find modules

view this post on Zulip Richard Feldman (Nov 11 2024 at 19:36):

and it's just the method-call syntax of foo.bar(baz) that has anything to do with the first argument

view this post on Zulip Richard Feldman (Nov 11 2024 at 19:36):

which is to say, the first argument is the one that gets chosen for val.{ ... } in the type

view this post on Zulip Derin Eryilmaz (Nov 11 2024 at 19:38):

completely agree with everything you just said

view this post on Zulip Derin Eryilmaz (Nov 11 2024 at 19:40):

should we go with Eq a or a.Eq as a shorthand?

view this post on Zulip Derin Eryilmaz (Nov 11 2024 at 19:41):

personally I'm liking the second one

view this post on Zulip Derin Eryilmaz (Nov 11 2024 at 19:41):

fits in with the module theme of using dots

view this post on Zulip Richard Feldman (Nov 11 2024 at 20:34):

that could work!

view this post on Zulip Richard Feldman (Nov 11 2024 at 20:34):

so then multiples would be like where a.Eq, a.Hash?

view this post on Zulip Derin Eryilmaz (Nov 11 2024 at 20:47):

yeah, that sounds good

view this post on Zulip Richard Feldman (Nov 11 2024 at 21:18):

oh, I realized a problem: the type alias could have other type variables

view this post on Zulip Richard Feldman (Nov 11 2024 at 21:18):

e.g. Container a elem

view this post on Zulip Richard Feldman (Nov 11 2024 at 21:19):

hm, although actually I'm not sure if we want to support that :thinking:

view this post on Zulip Richard Feldman (Nov 11 2024 at 21:19):

so maybe that's a feature, not a bug haha

view this post on Zulip Derin Eryilmaz (Nov 11 2024 at 21:25):

too complex I think. it should only have one type variable which is the type in question from the same module

view this post on Zulip Richard Feldman (Nov 13 2024 at 15:37):

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)

view this post on Zulip Richard Feldman (Nov 13 2024 at 15:38):

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

view this post on Zulip Richard Feldman (Nov 13 2024 at 15:45):

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 }

view this post on Zulip Richard Feldman (Nov 13 2024 at 15:49):

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.

view this post on Zulip Richard Feldman (Nov 13 2024 at 15:52):

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)

view this post on Zulip Richard Feldman (Nov 13 2024 at 15:54):

I guess another potential syntax that combines the above two:

where
    elem -> equals : elem, elem -> Bool

view this post on Zulip Richard Feldman (Nov 13 2024 at 15:58):

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

view this post on Zulip Agus Zubiaga (Nov 13 2024 at 16:56):

Couldn’t we let you do many of the method syntax and have only that variant?

view this post on Zulip Agus Zubiaga (Nov 13 2024 at 16:57):

The variable on the left of . already tells you which type it’s on

view this post on Zulip Agus Zubiaga (Nov 13 2024 at 16:58):

I think we could just let you define as many methods as you want on the same or different type variable

view this post on Zulip Richard Feldman (Nov 13 2024 at 16:58):

the problem is that decode doesn't take the dispatched type as its first argument:

where
    answer -> { decode : List U8, fmt -> Result answer DecodingErr },

view this post on Zulip Richard Feldman (Nov 13 2024 at 16:58):

so you can't just write where answer.(...) -> ...

view this post on Zulip Richard Feldman (Nov 13 2024 at 16:59):

because that's the syntax for answer being the first argument

view this post on Zulip Richard Feldman (Nov 13 2024 at 16:59):

but in the case of decode it's not the first argument, it's in the return type

view this post on Zulip Agus Zubiaga (Nov 13 2024 at 16:59):

Ah right, sorry

view this post on Zulip Richard Feldman (Nov 13 2024 at 16:59):

yeah if we only ever had these in the "first argument" position, then we would totally just do the nicer syntax

view this post on Zulip Richard Feldman (Nov 13 2024 at 17:00):

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:

view this post on Zulip Agus Zubiaga (Nov 13 2024 at 17:08):

I guess this would work if we used :: instead of . for module qualification:

where
    answer::decode(List U8, fmt) -> Result answer DecodingErr,

view this post on Zulip Richard Feldman (Nov 13 2024 at 17:09):

yeah, although then we miss out on . being the universal autocomplete syntax

view this post on Zulip Richard Feldman (Nov 13 2024 at 17:09):

feels like a pretty invasive solution to a really niche problem :thinking:

view this post on Zulip Richard Feldman (Nov 13 2024 at 17:12):

where
    elem.equals(elem) -> Bool
where
    answer -> decode(List U8, fmt) -> Result answer DecodingErr

view this post on Zulip Richard Feldman (Nov 13 2024 at 17:13):

I don't like the double -> in that :sweat_smile:

view this post on Zulip Richard Feldman (Nov 13 2024 at 17:15):

where
    elem.equals(elem) -> Bool
where
    decode(List U8, fmt) -> Result answer DecodingErr for answer

view this post on Zulip Agus Zubiaga (Nov 13 2024 at 17:17):

where
    module [answer] as m,
    m.decode(List U8, fmt) -> Result answer DecodingErr,

:sweat_smile:

view this post on Zulip Richard Feldman (Nov 13 2024 at 17:18):

where
    elem.equals(elem) -> Bool
where
    (module answer).decode(List U8, fmt) -> Result answer DecodingErr

view this post on Zulip Agus Zubiaga (Nov 13 2024 at 17:19):

Not bad for a niche feature

view this post on Zulip Richard Feldman (Nov 13 2024 at 17:19):

yeah the module keyword makes it really clear that it's a module :big_smile:

view this post on Zulip Agus Zubiaga (Nov 13 2024 at 17:21):

Should it be (module [foo, bar]). in case you need two types?

view this post on Zulip Richard Feldman (Nov 13 2024 at 17:21):

hm I don't think we can support that

view this post on Zulip Richard Feldman (Nov 13 2024 at 17:21):

the key is that we have to figure out which module to look in to find the decode function

view this post on Zulip Agus Zubiaga (Nov 13 2024 at 17:22):

Yeah, I mean the case where both types are in the same module

view this post on Zulip Richard Feldman (Nov 13 2024 at 17:22):

(module answer).decode would mean "go find the module where answer is defined; that's where the decode function can be found

view this post on Zulip Agus Zubiaga (Nov 13 2024 at 17:23):

I guess it would resolve to the same module anyway

view this post on Zulip Richard Feldman (Nov 13 2024 at 17:23):

yeah

view this post on Zulip Richard Feldman (Nov 13 2024 at 17:24):

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

view this post on Zulip Richard Feldman (Nov 13 2024 at 17:25):

hm actually no

view this post on Zulip Richard Feldman (Nov 13 2024 at 17:25):

because if there's more than one, it gets awkward

view this post on Zulip Agus Zubiaga (Nov 13 2024 at 17:25):

Yeah, that’s what I was going for with the separate module “entry”

view this post on Zulip Richard Feldman (Nov 13 2024 at 17:25):

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

view this post on Zulip Agus Zubiaga (Nov 13 2024 at 17:25):

Agus Zubiaga said:

where
    module [answer] as m,
    m.decode(List U8, fmt) -> Result answer DecodingErr,

:sweat_smile:

This

view this post on Zulip Richard Feldman (Nov 13 2024 at 17:26):

I think it's fine if it's a bit more verbose

view this post on Zulip Richard Feldman (Nov 13 2024 at 17:26):

like repeating the (module answer). each time

view this post on Zulip Richard Feldman (Nov 13 2024 at 17:26):

because if you're doing that, you'll prob put it in a type alias, so it'll only be verbose once

view this post on Zulip Richard Feldman (Nov 13 2024 at 17:26):

and then inside the function body you do:

import answer as M

view this post on Zulip Agus Zubiaga (Nov 13 2024 at 17:30):

Yeah, that’s probably fine

view this post on Zulip Richard Feldman (Nov 13 2024 at 17:37):

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

view this post on Zulip Richard Feldman (Nov 13 2024 at 17:43):

...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,

view this post on Zulip Agus Zubiaga (Nov 13 2024 at 17:53):

Yeah, I find that last one nicer overall, but this is pretty niche so I would be ok with either

view this post on Zulip Brendan Hansknecht (Nov 13 2024 at 17:54):

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).

view this post on Zulip Brendan Hansknecht (Nov 13 2024 at 17:56):

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

view this post on Zulip Derin Eryilmaz (Nov 13 2024 at 18:01):

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...

view this post on Zulip Brendan Hansknecht (Nov 13 2024 at 18:06):

That works as long as there is only one chain.

view this post on Zulip Sam Mohr (Jan 07 2025 at 05:23):

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)

view this post on Zulip Sam Mohr (Jan 07 2025 at 05:23):

Is anyone unhappy with this syntax?

view this post on Zulip Ayaz Hafiz (Jan 07 2025 at 05:27):

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

view this post on Zulip Sam Mohr (Jan 07 2025 at 05:27):

Sounds great! Make sure to wear a helmet when you think that hard

view this post on Zulip Ayaz Hafiz (Jan 07 2025 at 05:27):

whats the fun in that

view this post on Zulip Sam Mohr (Jan 07 2025 at 05:28):

You have a point (where your head used to be)


Last updated: Jun 16 2026 at 16:19 UTC