based on #ideas > nominal types, I wrote up a concrete proposal - any feedback welcome!
https://docs.google.com/document/d/10OFeNl9KAYAErajE0Wio4AAR66yM2u13bku0mTUawVk/edit?usp=sharing
This looks great! It seems very useful and cohesive and like it will be fairly easy to communicate.
It looks really good overall. I'm a little concerned about the fact that it's forcing you to use parentheses, though, for the one-tuples. That might make some custom types containing lambdas look weird. The @ syntax didnt have that issue
you mean for constructing and destructuring them?
or in the type
Why can't we just remove the parenthesis and dot notation? Would this make the syntax ambiguous in some edge cases?
empty : {} -> Email
empty = \{} -> Email.("")
vs
empty : {} -> Email
empty = \{} -> Email "" #Type cast "" Str to Email type
With tuple type, you would keep the parenthesis
matrix = Matrix3d (
-1, 1, 0, 0,
-2, -1, 0, 0,
0, 0, 1, 0,
0, 0, 1, 0,
)
yeah Email "" already means something today :big_smile:
the parens and dot are necessary to signal that it's a custom type and not a structural type
What about
Email."" instead of Email.("")
well but what if it's not a string literal?
like I have a string named str
Email.str already means something too
Would that still be okay though?
Yes, str is a variable name
"" is of type Str
[] is of type List
{} is of type Record
() is of type Tuple
CustomType.(1,2,3)
CustomType.[1,2,3]
CustomType.{a:1,b:2}
CustomType."Hello World!"
Maybe just a different character?
CustomType:(1,2,3)
CustomType:[1,2,3]
CustomType:{a:1,b:2}
CustomType:"Hello World!"
CustomType:str #Cast Str str variable to CustomType
Does the compiler know that Email is a custom type instead of a record type?
So Email.str would just be casting str variable to a Str type instead of thinking Email is a record with value str?
Email.str is like Result.map
Email would be a module name there
From the proposed example, would Email module look like this?
module [Email, empty, fromStr, toStr]
Since str is not exposed, would the compiler be able to infer it is type casting for a custom type?
I'm not against wrapping (""), I'm just trying to think of a simpler approach to not forcing basic types inside a tuple.
Brian Teague said:
I'm not against wrapping (""), I'm just trying to think of a simpler approach to not forcing basic types inside a tuple.
Isn't the inside a tuple thing an internal compiler implementation detail?
edit nvm I see now
I think using a colon : instead of period . when initializing or destructuring custom types would resolve this issue.
Email := Str
empty : {} -> Email
empty = \{} -> Email:""
fromStr : Str -> Email
fromStr = \str -> Email:str
toStr : Email -> Str
toStr = \Email:str -> str
# Treat builtin types as CustomTypes? Converting Email back to Str
toStr : Email -> Str
toStr = \email -> Str:email
I dunno, the parens feel fine to me haha
like tuples have parens around them, so it feels pretty natural for something with the semantics of a "1-tuple" to have parens too
e.g. how they have .0 working
if .0 works on it, I kinda expect parens :big_smile:
I think either is fine, but dots are more consistent with the rest of our syntax, so I'd prefer that
uh, what if you want to turn an object into a custom typed (opaque) object?
Yeah I would prefer to have the parens. It seems most expected
if you had
Person := {name: String, age: U8}
rawPerson = {name: "Derin", age: 100}
how would you turn the rawPerson into a Person with this syntax??
by default, it would be something like Person.{ name: rawPerson.name, age: rawPerson.age }
if there's demand for doing that sort of thing, we could discuss having a way to generate a fromRaw implementation for you
but I'm not sure it would actually come up in practice much (if at all)
so I'd want to see if there was actually demand for it in practice before talking about something like that
So the tricky thing is interpreting patterns with Person.rawPerson. Currently opaque types are easy to parse:
when person is
@Person inner -> inner.doStuff
But this is ambiguous
when person is
Person.rawPerson -> # rawPerson is the internal type
Person.janeDoe -> # janeDoe is a constant in the module Person
I think that since functions within the module that use the internal data are so popular, we'd want to always interpret Person.rawPerson as a destructuring to the internal data so that you can write
usePerson = \Person.inner ->
doStuffWith inner
And if patterns look like that, then we should support creation with
newPerson = Person.inner
But of course, this is now syntactically equivalent to Person.janeDoe
All this to say that custom type instantiation and destructuring should use the same syntax
And since instantiation and Module-qualified constant usage are the exact same if we don't require parens
Then we should require parens
For both custom type instantiation and destructuring
Sam Mohr said:
But this is ambiguous
when person is Person.rawPerson -> # rawPerson is the internal type Person.janeDoe -> # janeDoe is a constant in the module Person
this is a good point. I hadn't thought of that...but I think since it's so rare - and this proposal would eliminate Bool.true, which is most of the demand for it today, as well as making it possible to use custom tag unions for enumerated constants (e.g. http status codes) that map to a particular number or string - it's probably ok to just change what this parses as?
oh wait, oops
it would actually be one of these:
Person.{ rawPerson } -> # rawPerson is a field on a custom record
Person.(rawPerson) -> # rawPerson is a field on a custom 1-tuple
so there would always be punctuation in a destructuring pattern
and Person.rawPerson would unambiguously continue to mean what it does today
Under the assumption that you can't do Person := Alias, but instead need to do Person := (Alias,)
Which I'm fine with, because there's no runtime cost for size or speed
Wait, wouldn't
Person := {name: String, age: U8}
Be the same as
Person := ({name: String, age: U8})
And therefore you can use
rawPerson = {name: "Derin", age: 100}
realPerson = Person.(rawPerson)
I think Person := Alias would mean "this is a 1-tuple"
so that's a fix I guess
Yep, you'd have to do realPerson = Person.(rawPerson) in my eyes
basically the rule I'm thinking of is based on syntax, specifically whether you use square brackets, curly braces, parens, or none of those:
:= { ... } is a custom record:= [ ... ] is a custom tag union:= ( ... ) is a custom tuple:= ... is a custom 1-tuplestill, personally, I did like the @ syntax better because it fit well with the rest of the language. I feel like the only thing this fixes is enums, for which maybe different syntax could be considered. I feel like polymorphic variants are one of the most interesting things about this language, and what really makes it such a joy to use, so I don't know what place opaque variants have here. What was wrong with the old (opaque) syntax for making enums, anyway?
the example in the doc of tag unions that map directly to/from numbers has come up a few times - for example, #ideas > Request for ideas on enum decoding
for what it's worth, I actually think removing @ as a symbol is a selling point of this proposal
it's the only place it appears in the language, and aesthetically I'd rather not have it
plus (and this is admittedly a very minor concern) if we end up doing SIMD lexing, it would need its own pass, which would feel kinda wasteful :stuck_out_tongue:
Arguably, a downside of this idea is that True, False, Ok, and Err would become reserved; you could no longer use them as normal tag names. This sounds like more of a feature than a bug to me, because if you're choosing to give one of your userspace tags one of these names, that sounds almost certain to be the wrong design choice.
Totally agree on this, I think these 4 tags being reserved would be a better user experience and also lead to better code.
And overall: I could type out my thoughts on how we'd compile this, but I think it suffices to just say that everything seems to work with our long-term plans for the compiler.
I understand some of the problems this tries to solve, don't have better ideas, but am still a bit sad because of the amount of new language this adds (new versions of records and tags, plus an enum construct).
To fix recusive types, I liked the idea of requiring them to contain an opaque type and letting the platform and host exchange opaque types. I appreciate this would enable platform writers to make a new type of mistake, but not a type of mistake that I think is fundamentally different from the ones platform authors are already able to make, given how low they are in the stack.
The aspect I like best of the proposal are the better type annotations for booleand and results. Though if structural tags are still what we'd recommend folks reach as a default, then a possible extra hurdle for beginners will be that the unions they first encounter (Bool and Result, custom types) behave differently than the ones tutorials recommend they construct for their own types.
For the enums in the proposal specifically, I actually did get another idea. think the following alternative in today's roc might have the same ergonomics without requiring any new language constructs. Curious if you agree:
module [Order, lt, gt, eq]
Order := I8 implemens [Eq, Sort, Decoding, Encoding]
lt = @Order -1
gt = @Order 1
eq = @Order 0
And this will also get you an enum which:
when statements except prefixed with module name instead of type nameThe syntax for non-exhaustive types (if we go through with this custom types proposal) should probably be
Type := [
Variant,
OtherVariant,
_
]
or
Type := [
Variant,
OtherVariant,
..
]
Similarly to custom records and tag unions, custom tuples would also exist:
Matrix3d := (
F32, F32, F32, F32,
F32, F32, F32, F32,
F32, F32, F32, F32,
F32, F32, F32, F32,
)We could use the normal tuple field access syntax of .0, .1, etc. to access different elements of this matrix. Unlike a normal tuple, you'd use the name of the custom tuple when constructing one...
I don't know if this would even be possible to do. Imagine the following:
Thing a := a
inner = \j -> j.0
thing1 = Thing (U32, U32)
thing1 = Thing.((1, 2))
thing2 = Thing U32
thing2 = Thing.(1)
inner thing1 can't return a U32, because then inner's type would be Thing (a)* -> a (I think?) but the type of inner in inner thing2would be Thing a -> a which is different. So for tuple accesses you'd need .0.0, .0.1 etc.
(Edit: Actually--maybe this makes some sense. Still, I don't think this was an issue with the old design.)
I overall really like this proposal. I wanted to write a proposal from "non opaque" types for a while but didn't quite find the time when I was thinking about it a lot.
The one area I'd like to investigate and explore more in the design is how it relates to encoding and decoding data.
The main pain point with the existing opaque types is casting them too and from existing data structures.
Something I think should be considered is a method for specifying that a custom type can cast to and from it's non-custom equivalent. This could either be done by just implementing a "castFrom/castTo" ability which can also be autoderived.
The main reason I want this, is that mostly when using custom types from decoding they are often only something I want during the decode. After the decode is done i want them to just be regular data types.
The main reasoning for this is that custom types then pollute the system from that point onwards. They render a lot of rocs nicesest features unusable: the structural typing. Without a way to auto convert I have to write ever more complex manual converters as my custom type becomes more complex.
I would imagine there being two new builtin features/abilities.
castFrom, castTo, and recursiveCastFrom
(Recursive is needed because sometimes you might want to unwrap all the layers of custom types that can be unwrapped.
I believe in the autoderived case they should be able to be implimented in an extremely efficient way, no actual data transformation should be needed right?
Some concrete examples:
I've built up a response for a custom web request bit by bit, and I've got a normal record. I now need to encode it, but my web API must comply with a certain spec I now need to convert it to my custom type that has the custom encoding. Without this I need to hand write a potentially very long custom conversion function, it's menial and boring. With this I just call castTo and it works.
I've written a program and now accept data in json but the json encoding is complex and encodes tagged unions so it needs a lot of custom decoding logic. All my code operates on standard roc types.
After I've decided the json I'm left with a deeply nested custom type. I must either hand convert this to normal to roc types, or change all my existing code to use these types loosing many of the advantages of structural typing.
With this proposal I would decode and then call castFromRecusive.
I also intend to write out a little overview of the current situation decoding the various types of data that exist in complex json APIs.
I think we should have a conscious understanding of what data types we can/can't decode easily so we can be aware of what language features can help that number/string enums etc.
Obviously that can be done after this custom types feature, but I do think encode/decode is one of the key areas this proposal will improve so it's worth having in mind.
I'll get to that this weekend :)
I believe in the autoderived case they should be able to be implimented in an extremely efficient way, no actual data transformation should be needed right?
yeah, that should be doable in some form!
seems like a reasonable thing to consider as a separate idea that builds on this one
Richard Feldman said:
like tuples have parens around them, so it feels pretty natural for something with the semantics of a "1-tuple" to have parens too
if
.0works on it. I kinda expect parens :big_smile:
What if you don't have to do a tuple lookup at all?
toStr : Email -> Str
toStr = \email -> email.0
vs
toStr : Email -> Str
toStr = \email -> email
Since Email is just a type alias for Str
toStr:Str -> Str
is equivalent to
toStr:Email -> Str
it's not a type alias though, it's a nominal type
if it were a type alias then yeah, that already works!
Thank you Richard, I had to look up alias vs nominal types. (learning new things awesome!)
Alias types -> Just a pointer to the same data type (Email : Str as in my example above)
Nominal types -> Distinct separate types even if the use the same data type (so can't pass Email to a function expecting Str type)
For nominal types, type casting would then be required by the compiler, but I guess the problem in the proposal is how to infer the correct types which the tuple lookup resolves?
# Compiler infers tuple.0 is type Str
Email := Str
toStr : Email -> Str
toStr = \email -> email.0
vs
#Explicit type casting, tells the compiler to convert Email to Str since data type matches
Email := Str
toStr : Email -> Str
toStr = \email -> Str:email
vs
# Explicit tuple definition equivalent to first example?
Email := (Str)
toStr : Email -> Str
toStr = \email -> email.0
vs
#Should fail to compile since email is type Email and nominal types are supposed to be different
Email := Str
toStr : Email -> Str
toStr = \email -> email
What's the issue with the examples?
Not really an issues, more of a realization.
One of ROC's philosophies, if I understand correctly, is that type definitions can be inferred from the source code.
My initial proposal of using ":" instead of "." would require explicit type casting "Str:email" while easier to understand, is more verbose then tuple lookup "email.0"
Both solutions work for nominal types, just programmer preference at this point.
You could say this example is type casting a Record type to a User type:
user1 = User.{ firstName: "a", lastName: "b" }
This leads to another question, is there a use case why you would want Email to be a custom nominal type instead of just a type alias when it's literally only using Str type?
Brian Teague said:
This leads to another question, is there a use case why you would want Email to be a custom nominal type instead of just a type alias when it's literally only using Str type?
You're getting to the main point of custom types: imagine you want a function that acts upon emails and only emails. You don't want to have to validate it every time. So you make it take an Email, and whatever module defines Email gets to control how emails are created (i.e. which Strs can be turned into Emails). As long as you trust that module to prevent invalid Email instances from getting created, Email doesn't mean string anymore--it means only strings that are valid emails
That said, doing Email := Str would be weird design, and it would probably make more sense to have it be something like
Email := { username: Str, domain: Str}
instead. But you can imagine a case like NonZeroNum := U32 where custom types would wrap only a single other type--no tuples or records involved
Please correct me if I'm wrong, but if I understood the proposal correctly, NonZeroNum := U32 would not be a simple wrapper but a 1-tuple and had to get accessed like nonZeroNum.0
it's correct that that is how you'd access it, but personally I'd consider a 1-tuple to be a very simple wrapper! :big_smile:
I like the idea of nominal types presented here.
The one-tuple thing however confuses me slightly. The newtype pattern itself is laudable, but in general I am slightly confused about 1-tuples in Roc and may have a suggestion.
For example the Roc gives me 1, (1) and (1,) as all being the same thing. In some languages (I'm thinking Julia in particular, but it isn't unique in this regard) a trailing comma to a 1-tuple is required to disambiguate between tuples and parentheses. Having used this a fair bit, I think it is great.
Personally I don't see how T1 := T2 automatically making a 1-tuple is helpful. I feel that a newtype like Email := Str where my value of type Email can be used everywhere as if were a Str is really useful, just so long as the reverse isn't true (I can't assign a Str to a place a Email is expected).
I feel it would be very consistent to:
(1,) or (x,), as well as structural type syntax like (Str,) for one-tuple types.:= inherit the behavior of the underlying value (just like we have for compound structural types - records inhereting getfield, for example).Email := (Str,)does the obvious thing based on the first point.Derin Eryilmaz said:
That said, doing
Email := Strwould be weird design
That said, I'm not necessarily disagreeing with this, yet the choice of design (weird or otherwise) should be in the hands of the user, and the language should be as simple and clear and consistent as possible.
Andy Ferris said:
- Bare non-compound structural (i.e. builtin primitive) types on the RHS of
:=inherit the behavior of the underlying value (just like we have for compound structural types - records inhereting getfield, for example).
This is a neat idea--I wonder if there are any downsides to allowing functions that wrap strings to get string functions, etc? I can't think of any.
although it might actually make type inference somewhat tricky because Str.contains is no longer a Str, Str -> Bool; it can also take custom types that extend strings as arguments
(I'm thinking Julia in particular, but it isn't unique in this regard)
I think it is from python, but might be even older
a trailing comma to a 1-tuple is required to disambiguate between tuples and parentheses. Having used this a fair bit, I think it is great.
Personally I think it is kinda silly when I end up using it in python. 1 tuples really shouldn't be a thing in most languages. Python really only needs them cause tuples are iterable, but single items are not. At least that is essentially the only place I see them in python.
Personally I don't see how
T1 := T2automatically making a 1-tuple is helpful. I feel that a newtype likeEmail := Strwhere my value of typeStris really useful, just so long as the reverse isn't true (I can't assign aStrto a place a
It is very important that using a nominal type as the underlying type is explicit not implicit. Imagine instead of Email, it was HttpSecret. Passing that to a function that accepts Str might be totally insecure.
Also, part of the goal of the nominal/opaque type is to make them type safe such that they are explicitly not the structural type. That would be equivalent to adding implicit casting to roc.
Introduce one-tuple value synax to Roc like
(1,)or(x,), as well as structural type syntax like(Str,)for one-tuple types.
Yeah, I do agree that it is kinda silly to have a one tuple in nominal types but impossible otherwise in roc. I think they are only in nominal types cause a better syntax wasn't found. I think it would be best if they just didn't exist anywhere in roc.
yeah I don't like the idea of (foo,) being valid Roc syntax
that obviously looks like a mistake, plus it suggests that (foo, bar,) should be valid as well for a 2-tuple
@Richard Feldman I wonder if there could be a consistent wrap/unwrap function still with nominal types. That way you don't need to pattern match the entire record to construct/destruct it.
Like if I have a record that is structural and want to convert it to nominal it is very inconvenient to write:
nominal = Record.{
field1: struct.field1,
field2: struct.field2,
field3: struct.field3,
...
}
And unwrapping is even worse:
{ field1, field2, field3, ...} = nominal
strict = { field1, field2, field3, ...}
we could have a method for that
for both, really
like an automatically generated one
Then for single value nominal types instead of using .0, it could just use that. Though maybe it would be too verbose/inconvenient in comparison.
agreed, that would be a useful feature. this functionality exists in the current implementation of opaque types with @Type pattern matching and i don't want it to be removed.
the current implementation treats all wrapped types the same, whether they're tuples, structs, single types, etc and i like that a lot
This does that as well, assuming you ban single types.
The only inconsistency currently
I do think it is really nice to generally not need to unwrap
Current roc code with opaque records is just constantly unwrapping and rewrapping due wanting to access and update the inner record. It is pretty messy because of this
Can you record update a nominal type?
User.{ kind: Admin, ..user }
If not, I think there will be a ton of unwrapping and inconvenience like current opaque types
Also, it is kinda interesting to compare this to other languages where there are no structural types and thus never a need to unwrap. Maybe if we put enough power into nominal types, unwrapping would be exceptionally rare and not something that needs to be first class like with current opaque types. Not sure in practice what would make the distinction though
yeah arguably the 1-tuple idea is silly, and if you want to do that you should just use a 1-field record
maybe remove tuples from the language? :smiley:
just kidding of course
it felt kinda verbose, but this could certainly work:
Email := { str : Str }
to_str : Email -> Str
to_str = \wrapped -> wrapped.str
from_str : Str -> Email
from_str = \str -> Email.{ str }
certainly it's trivial to learn haha
compared to "one-tuples"
to be fair, this is often how it's done in Rust
what would the type of to_str be there if there was no type annotation?
are we going to treat emails as records? (fair enough, I guess)
yeah I think they have to unify with structural records
my thinking there is that open (structural) records can unify with other structural records, or with nominal records
but only within the file they're defined in, ig
but then once you unify with a nominal record, that's it - you're now that nominal type
right, with access controls (based on whether or not you exposed it)
makes sense
Richard Feldman said:
my thinking there is that open (structural) records can unify with other structural records, or with nominal records
this is necessary for a.b to work in general with nominal records, and it would be similar with nominal tuples and a.0
so will we get an operator to get the entire wrapped value (record, tuple, whatever)?
I don't think it could be dot syntax because that would conflict
and i don't think it could be a function either because there's not really a way to express that with the type system
Brendan Hansknecht said:
Can you record update a nominal type?
User.{ kind: Admin, ..user }
like maybe you could have
User := { kind: UserKind, username: Str, date_joined: Date }
make_admin : User -> User
make_admin = \user -> User.{ kind: Admin, ..user^ }
is it super elegant? no. does it work? i think so
so then i guess a.b would be the same as a^.b
what's wrong with User.{ kind: Admin, ..user } ?
why would it need separate syntax?
you're right, i just realized that
is there any case where we'd actually need to get the whole inner record
I guess we could use .. as an easy unwrap technically out = { ..user }
Same for easy construction:
user = User.{ ..in }
oh yeah that's nice
it would work for lists too right?
but probably not tuples...?
Should work for tuples
I'm not sure if it would work for lists though
I guess it could
Cause wrapping list alone would be the same as the one-tuple case
This is specifically in the context of nominal types. Works with lists outside of nominal types for sure. (Though not sure the current implementation status)
Oh wow I hadn’t thought about the fact that you could pass a nominal record into a function that is defined on a structural one! That makes this whole setup much nicer if I’m understanding correctly
I wonder if allowing that would be too bug prone
certainly it would work if you accepted an open record as the argument (e.g. foo : { name : Str }* today, possibly in the future with .. syntax it might be foo : { name : Str, .. })
we've talked in the past about not making that be something you have to opt into, so by default as long as you pass in a record with at least those fields and those types, it Just Works
Rich Hickey would approve of that design, but the counterargument is that in practice it seems like people basically never opt into open records even though it only requires adding one character to the type signature (namely *), so it seems like the upside would be minimal
and then it could of course open the door to potential mistakes not being reported as errors because the types happen to line up
to me it makes sense to continue with the current design - that is, it's opt-in but very easy to opt in - and then if there's demand for making it even easier (that is, making it Just Work without opting in) we can discuss that in the context of that demand :big_smile:
Yeah starting with it opt in seems reasonable
Well I’m not sure if it’s right to call it opt in because you get open records when you omit type annotations, but I think that’s good. I like the flexibility when I omit type annotations and more rigidity when I add them
8 messages were moved from this topic to #ideas > 1-tuples by Richard Feldman.
Would you be able to define method constraints on a custom type? Something like
InspectableData x := [Data x] where x.to_str() -> Str
I'm not seeing any examples of it in the doc and didn't see any in Zulip
But I'm not sure why we wouldn't want to allow this
why would you want this on the actual type? it only matters on functions right
I'm not sure. It's a thing you can do in Rust
Let me look for an example that I think would be worth implementing this feature
I think in general, it's useful for making sure that users of a struct use it for a specific set of purposes
Seems like they're probably more trouble than they're worth
Just wanted to check before I avoided implementing them for custom types
We can always add them after, anyway
this is easier to add later than remove later
i don't see the immediate benefit, if you need this you can add the bound on the relevant function
The rust example is
struct IteratorThing<I>
where
I: Iterator,
{
a: I,
b: Option<I::Item>,
}
It lets you make an iterator efficiently
But yes, you could always just add that restraint to all methods
I don't think we need anything like this any time soon
This is already used in current roc. For example the key in dict
Dict specifies that its key must have eq and hash
This avoids needing to specify it on essentially every single function definition in the Dict.roc file
Sadly, you still have to specify Inspect whenever you want to add a dbg into the file and that gets painful cause you have to add it all over the place and in Set.roc which calls into Dict.roc
yeah I'd like to remove that Inspect restriction
Brendan Hansknecht said:
This avoids needing to specify it on essentially every single function definition in the
Dict.rocfile
I actually think we should do that anyway (like Elm does) - yeah it's a little repetitive, but I don't like there being secret invisible type constraints in the function type :grimacing:
like if the function only accepts an argument with Hash and you'll get a type mismatch otherwise, the function type should tell you that!
also I just realized that static dispatch cleanly solves the "List has Eq only if its element type has Eq" problem
you just put the constraint on the equals function, bam, done
no need for any separate syntax or rules
Richard Feldman said:
also I just realized that static dispatch cleanly solves the "
ListhasEqonly if its element type hasEq" problem
This already just works with abilites
Richard Feldman said:
Brendan Hansknecht said:
This avoids needing to specify it on essentially every single function definition in the
Dict.rocfileI actually think we should do that anyway (like Elm does) - yeah it's a little repetitive, but I don't like there being secret invisible type constraints in the function type :grimacing:
Fair enough, guess it means we are actually removing a feature.
Personally, I much prefer the hidden type variable. If I know how Dict works, I don't need all of the repeated noise. But I understand wanting it anyway.
Also, if you don't put it in the type, I guess you need to add the constraint to the empty init function. Otherwise, it will be confusing when you create a dict with a key type without hash and then it fails on first use.
Elm used to do that, but then removed the constraint
as an aside, I definitely prefer Dict.empty() over today's Dict.empty {} :big_smile:
in practice it didn't really help anything, just made more work for the type checker haha
bc you never just make an empty dictionary and leave it alone, you always insert stuff info it almost immediately
and insert has the constraint
What makes comparable have a constraint in those elm docs? Just looks like a long type variable name.
Asking cause it would nice if we could have something so short and succinct as that.
Elm just has a few hardcoded special type variables like comparable, they aren't extensibile in userspace
they just silently are constrained based on their names being language keywords
Ah
Does make it read nice
Like in the new static dispatch world, I feel like I am going to be making aliases all the time to avoid verbosity. I would rather see where Eq(k) than where k.eq() -> Bool
And even more so for more complex types
yeah I think we should allow type aliases for them
I think those were in the doc, but I could be misremembering
Yeah, I think it is practically required for complex types like hashers
it might be, although we could also have a Hasher type which specifies all the functions, and then you just need like a to_hasher function which returns one of those
Fair. Not sure which is better form. Feels like a natural interface, but could always use a struct of closures instead. Struct of closures is theoretically less efficient, but due to lambdasets, may be generally equivalent in practice.
I guess that means we kinda have implicit and explicit interfaces.
Implicit is static dispatch. Explicitly is returning a struct of closures.
Yep, Richard has already suggested support for a.func(Str) -> U64 where a.Hasher as a postfix application of Hasher a
Beyond that, we should support where HashWith a Str, ... for multi-arg aliases that alias over HashWith a b : where a.hash_with(b) -> U64
While I'm bringing over the Num builtin module, I had some questions about the current "spec' for Custom Types.
In places where we have something like:
Integer range := range
We would need to wrap that in a single-member tagged untion like this, no?
Integer(range) := [Integer(range)]
Last updated: Jun 16 2026 at 16:19 UTC