Stream: ideas

Topic: custom types


view this post on Zulip Richard Feldman (Nov 07 2024 at 00:35):

based on #ideas > nominal types, I wrote up a concrete proposal - any feedback welcome!

https://docs.google.com/document/d/10OFeNl9KAYAErajE0Wio4AAR66yM2u13bku0mTUawVk/edit?usp=sharing

view this post on Zulip Isaac Van Doren (Nov 07 2024 at 01:19):

This looks great! It seems very useful and cohesive and like it will be fairly easy to communicate.

view this post on Zulip Derin Eryilmaz (Nov 07 2024 at 01:31):

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

view this post on Zulip Richard Feldman (Nov 07 2024 at 01:32):

you mean for constructing and destructuring them?

view this post on Zulip Richard Feldman (Nov 07 2024 at 01:32):

or in the type

view this post on Zulip Brian Teague (Nov 07 2024 at 01:34):

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

view this post on Zulip Brian Teague (Nov 07 2024 at 01:35):

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

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

yeah Email "" already means something today :big_smile:

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

the parens and dot are necessary to signal that it's a custom type and not a structural type

view this post on Zulip Brian Teague (Nov 07 2024 at 01:36):

What about

Email."" instead of Email.("")

view this post on Zulip Richard Feldman (Nov 07 2024 at 01:37):

well but what if it's not a string literal?

view this post on Zulip Richard Feldman (Nov 07 2024 at 01:37):

like I have a string named str

view this post on Zulip Richard Feldman (Nov 07 2024 at 01:37):

Email.str already means something too

view this post on Zulip Brian Teague (Nov 07 2024 at 01:42):

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

view this post on Zulip Brian Teague (Nov 07 2024 at 01:44):

CustomType.(1,2,3)
CustomType.[1,2,3]
CustomType.{a:1,b:2}
CustomType."Hello World!"

view this post on Zulip Brian Teague (Nov 07 2024 at 01:46):

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

view this post on Zulip Brian Teague (Nov 07 2024 at 01:50):

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?

view this post on Zulip Richard Feldman (Nov 07 2024 at 01:51):

Email.str is like Result.map

view this post on Zulip Richard Feldman (Nov 07 2024 at 01:51):

Email would be a module name there

view this post on Zulip Brian Teague (Nov 07 2024 at 01:55):

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?

view this post on Zulip Brian Teague (Nov 07 2024 at 01:55):

I'm not against wrapping (""), I'm just trying to think of a simpler approach to not forcing basic types inside a tuple.

view this post on Zulip Luke Boswell (Nov 07 2024 at 01:57):

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

view this post on Zulip Brian Teague (Nov 07 2024 at 02:05):

I think using a colon : instead of period . when initializing or destructuring custom types would resolve this issue.

view this post on Zulip Brian Teague (Nov 07 2024 at 02:09):

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

view this post on Zulip Richard Feldman (Nov 07 2024 at 02:15):

I dunno, the parens feel fine to me haha

view this post on Zulip Richard Feldman (Nov 07 2024 at 02:15):

like tuples have parens around them, so it feels pretty natural for something with the semantics of a "1-tuple" to have parens too

view this post on Zulip Richard Feldman (Nov 07 2024 at 02:15):

e.g. how they have .0 working

view this post on Zulip Richard Feldman (Nov 07 2024 at 02:15):

if .0 works on it, I kinda expect parens :big_smile:

view this post on Zulip Sam Mohr (Nov 07 2024 at 02:20):

I think either is fine, but dots are more consistent with the rest of our syntax, so I'd prefer that

view this post on Zulip Derin Eryilmaz (Nov 07 2024 at 02:30):

uh, what if you want to turn an object into a custom typed (opaque) object?

view this post on Zulip Isaac Van Doren (Nov 07 2024 at 02:31):

Yeah I would prefer to have the parens. It seems most expected

view this post on Zulip Derin Eryilmaz (Nov 07 2024 at 02:32):

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

view this post on Zulip Richard Feldman (Nov 07 2024 at 02:44):

by default, it would be something like Person.{ name: rawPerson.name, age: rawPerson.age }

view this post on Zulip Richard Feldman (Nov 07 2024 at 02:45):

if there's demand for doing that sort of thing, we could discuss having a way to generate a fromRaw implementation for you

view this post on Zulip Richard Feldman (Nov 07 2024 at 02:45):

but I'm not sure it would actually come up in practice much (if at all)

view this post on Zulip Richard Feldman (Nov 07 2024 at 02:45):

so I'd want to see if there was actually demand for it in practice before talking about something like that

view this post on Zulip Sam Mohr (Nov 07 2024 at 02:47):

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

view this post on Zulip Sam Mohr (Nov 07 2024 at 02:49):

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

view this post on Zulip Sam Mohr (Nov 07 2024 at 02:49):

And if patterns look like that, then we should support creation with

newPerson = Person.inner

view this post on Zulip Sam Mohr (Nov 07 2024 at 02:50):

But of course, this is now syntactically equivalent to Person.janeDoe

view this post on Zulip Sam Mohr (Nov 07 2024 at 02:50):

All this to say that custom type instantiation and destructuring should use the same syntax

view this post on Zulip Sam Mohr (Nov 07 2024 at 02:51):

And since instantiation and Module-qualified constant usage are the exact same if we don't require parens

view this post on Zulip Sam Mohr (Nov 07 2024 at 02:51):

Then we should require parens

view this post on Zulip Sam Mohr (Nov 07 2024 at 02:51):

For both custom type instantiation and destructuring

view this post on Zulip Richard Feldman (Nov 07 2024 at 02:52):

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?

view this post on Zulip Richard Feldman (Nov 07 2024 at 02:54):

oh wait, oops

view this post on Zulip Richard Feldman (Nov 07 2024 at 02:55):

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

view this post on Zulip Richard Feldman (Nov 07 2024 at 02:55):

so there would always be punctuation in a destructuring pattern

view this post on Zulip Richard Feldman (Nov 07 2024 at 02:55):

and Person.rawPerson would unambiguously continue to mean what it does today

view this post on Zulip Sam Mohr (Nov 07 2024 at 02:56):

Under the assumption that you can't do Person := Alias, but instead need to do Person := (Alias,)

view this post on Zulip Sam Mohr (Nov 07 2024 at 02:56):

Which I'm fine with, because there's no runtime cost for size or speed

view this post on Zulip Derin Eryilmaz (Nov 07 2024 at 02:56):

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)

view this post on Zulip Richard Feldman (Nov 07 2024 at 02:56):

I think Person := Alias would mean "this is a 1-tuple"

view this post on Zulip Derin Eryilmaz (Nov 07 2024 at 02:56):

so that's a fix I guess

view this post on Zulip Sam Mohr (Nov 07 2024 at 02:57):

Yep, you'd have to do realPerson = Person.(rawPerson) in my eyes

view this post on Zulip Richard Feldman (Nov 07 2024 at 02:57):

basically the rule I'm thinking of is based on syntax, specifically whether you use square brackets, curly braces, parens, or none of those:

view this post on Zulip Derin Eryilmaz (Nov 07 2024 at 03:00):

still, 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?

view this post on Zulip Richard Feldman (Nov 07 2024 at 03:12):

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

view this post on Zulip Richard Feldman (Nov 07 2024 at 03:14):

for what it's worth, I actually think removing @ as a symbol is a selling point of this proposal

view this post on Zulip Richard Feldman (Nov 07 2024 at 03:14):

it's the only place it appears in the language, and aesthetically I'd rather not have it

view this post on Zulip Richard Feldman (Nov 07 2024 at 03:15):

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:

view this post on Zulip Sam Mohr (Nov 07 2024 at 05:45):

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.

view this post on Zulip Sam Mohr (Nov 07 2024 at 05:49):

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.

view this post on Zulip Jasper Woudenberg (Nov 07 2024 at 07:09):

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.

view this post on Zulip Jasper Woudenberg (Nov 07 2024 at 07:20):

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:

view this post on Zulip Derin Eryilmaz (Nov 07 2024 at 13:52):

The syntax for non-exhaustive types (if we go through with this custom types proposal) should probably be

Type := [
  Variant,
  OtherVariant,
  _
]

or

Type := [
  Variant,
  OtherVariant,
  ..
]

view this post on Zulip Derin Eryilmaz (Nov 07 2024 at 14:17):

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

view this post on Zulip Eli Dowling (Nov 08 2024 at 03:38):

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:

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

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

view this post on Zulip Eli Dowling (Nov 08 2024 at 03:43):

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

view this post on Zulip Richard Feldman (Nov 08 2024 at 04:41):

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!

view this post on Zulip Richard Feldman (Nov 08 2024 at 04:41):

seems like a reasonable thing to consider as a separate idea that builds on this one

view this post on Zulip Brian Teague (Nov 09 2024 at 00:49):

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

view this post on Zulip Richard Feldman (Nov 09 2024 at 00:55):

it's not a type alias though, it's a nominal type

view this post on Zulip Richard Feldman (Nov 09 2024 at 00:55):

if it were a type alias then yeah, that already works!

view this post on Zulip Brian Teague (Nov 09 2024 at 02:31):

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

view this post on Zulip Derin Eryilmaz (Nov 09 2024 at 02:43):

What's the issue with the examples?

view this post on Zulip Brian Teague (Nov 09 2024 at 02:50):

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.

view this post on Zulip Brian Teague (Nov 09 2024 at 02:52):

You could say this example is type casting a Record type to a User type:

user1 = User.{ firstName: "a", lastName: "b" }

view this post on Zulip Brian Teague (Nov 09 2024 at 03:09):

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?

view this post on Zulip Derin Eryilmaz (Nov 09 2024 at 03:14):

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

view this post on Zulip Derin Eryilmaz (Nov 09 2024 at 03:16):

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

view this post on Zulip Jakob Steinberg (Nov 14 2024 at 09:30):

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

view this post on Zulip Richard Feldman (Nov 14 2024 at 12:29):

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:

view this post on Zulip Andy Ferris (Nov 18 2024 at 12:10):

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:

view this post on Zulip Andy Ferris (Nov 18 2024 at 12:15):

Derin Eryilmaz said:

That said, doing Email := Str would 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.

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

Andy Ferris said:

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.

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

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

view this post on Zulip Brendan Hansknecht (Nov 18 2024 at 16:40):

(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 := 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).

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.

view this post on Zulip Richard Feldman (Nov 18 2024 at 16:46):

yeah I don't like the idea of (foo,) being valid Roc syntax

view this post on Zulip Richard Feldman (Nov 18 2024 at 16:46):

that obviously looks like a mistake, plus it suggests that (foo, bar,) should be valid as well for a 2-tuple

view this post on Zulip Brendan Hansknecht (Nov 18 2024 at 16:47):

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

view this post on Zulip Richard Feldman (Nov 18 2024 at 16:48):

we could have a method for that

view this post on Zulip Richard Feldman (Nov 18 2024 at 16:48):

for both, really

view this post on Zulip Richard Feldman (Nov 18 2024 at 16:48):

like an automatically generated one

view this post on Zulip Brendan Hansknecht (Nov 18 2024 at 16:49):

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.

view this post on Zulip Derin Eryilmaz (Nov 18 2024 at 17:56):

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.

view this post on Zulip Derin Eryilmaz (Nov 18 2024 at 17:56):

the current implementation treats all wrapped types the same, whether they're tuples, structs, single types, etc and i like that a lot

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

This does that as well, assuming you ban single types.

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

The only inconsistency currently

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

I do think it is really nice to generally not need to unwrap

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

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

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

Can you record update a nominal type?

User.{ kind: Admin, ..user }

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

If not, I think there will be a ton of unwrapping and inconvenience like current opaque types

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

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

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

yeah arguably the 1-tuple idea is silly, and if you want to do that you should just use a 1-field record

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

maybe remove tuples from the language? :smiley:

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

just kidding of course

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

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 }

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

certainly it's trivial to learn haha

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

compared to "one-tuples"

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

to be fair, this is often how it's done in Rust

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

what would the type of to_str be there if there was no type annotation?

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

are we going to treat emails as records? (fair enough, I guess)

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

yeah I think they have to unify with structural records

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

my thinking there is that open (structural) records can unify with other structural records, or with nominal records

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

but only within the file they're defined in, ig

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

but then once you unify with a nominal record, that's it - you're now that nominal type

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

right, with access controls (based on whether or not you exposed it)

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

makes sense

view this post on Zulip Richard Feldman (Nov 18 2024 at 19:00):

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

view this post on Zulip Derin Eryilmaz (Nov 18 2024 at 19:00):

so will we get an operator to get the entire wrapped value (record, tuple, whatever)?

view this post on Zulip Derin Eryilmaz (Nov 18 2024 at 19:00):

I don't think it could be dot syntax because that would conflict

view this post on Zulip Derin Eryilmaz (Nov 18 2024 at 19:02):

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

view this post on Zulip Derin Eryilmaz (Nov 18 2024 at 19:06):

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

view this post on Zulip Derin Eryilmaz (Nov 18 2024 at 19:08):

so then i guess a.b would be the same as a^.b

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

what's wrong with User.{ kind: Admin, ..user } ?

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

why would it need separate syntax?

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

you're right, i just realized that

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

is there any case where we'd actually need to get the whole inner record

view this post on Zulip Brendan Hansknecht (Nov 18 2024 at 19:27):

I guess we could use .. as an easy unwrap technically out = { ..user }

view this post on Zulip Brendan Hansknecht (Nov 18 2024 at 19:28):

Same for easy construction:
user = User.{ ..in }

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

oh yeah that's nice

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

it would work for lists too right?

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

but probably not tuples...?

view this post on Zulip Brendan Hansknecht (Nov 18 2024 at 20:00):

Should work for tuples

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

I'm not sure if it would work for lists though

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

I guess it could

view this post on Zulip Brendan Hansknecht (Nov 18 2024 at 20:07):

Cause wrapping list alone would be the same as the one-tuple case

view this post on Zulip Brendan Hansknecht (Nov 18 2024 at 20:08):

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)

view this post on Zulip Isaac Van Doren (Nov 18 2024 at 20:40):

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

view this post on Zulip Brendan Hansknecht (Nov 18 2024 at 20:43):

I wonder if allowing that would be too bug prone

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

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

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

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

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

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

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

and then it could of course open the door to potential mistakes not being reported as errors because the types happen to line up

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

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:

view this post on Zulip Isaac Van Doren (Nov 18 2024 at 22:49):

Yeah starting with it opt in seems reasonable

view this post on Zulip Isaac Van Doren (Nov 18 2024 at 23:00):

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

view this post on Zulip Notification Bot (Nov 20 2024 at 04:11):

8 messages were moved from this topic to #ideas > 1-tuples by Richard Feldman.

view this post on Zulip Sam Mohr (Jan 12 2025 at 05:32):

Would you be able to define method constraints on a custom type? Something like

InspectableData x := [Data x] where x.to_str() -> Str

view this post on Zulip Sam Mohr (Jan 12 2025 at 05:33):

I'm not seeing any examples of it in the doc and didn't see any in Zulip

view this post on Zulip Sam Mohr (Jan 12 2025 at 05:33):

But I'm not sure why we wouldn't want to allow this

view this post on Zulip Ayaz Hafiz (Jan 12 2025 at 05:34):

why would you want this on the actual type? it only matters on functions right

view this post on Zulip Sam Mohr (Jan 12 2025 at 05:35):

I'm not sure. It's a thing you can do in Rust

view this post on Zulip Sam Mohr (Jan 12 2025 at 05:36):

Let me look for an example that I think would be worth implementing this feature

view this post on Zulip Sam Mohr (Jan 12 2025 at 05:38):

I think in general, it's useful for making sure that users of a struct use it for a specific set of purposes

view this post on Zulip Sam Mohr (Jan 12 2025 at 05:40):

https://stackoverflow.com/questions/49229332/should-trait-bounds-be-duplicated-in-struct-and-impl/66369912#66369912

view this post on Zulip Sam Mohr (Jan 12 2025 at 05:40):

Seems like they're probably more trouble than they're worth

view this post on Zulip Sam Mohr (Jan 12 2025 at 05:41):

Just wanted to check before I avoided implementing them for custom types

view this post on Zulip Sam Mohr (Jan 12 2025 at 05:41):

We can always add them after, anyway

view this post on Zulip Ayaz Hafiz (Jan 12 2025 at 06:13):

this is easier to add later than remove later

view this post on Zulip Ayaz Hafiz (Jan 12 2025 at 06:14):

i don't see the immediate benefit, if you need this you can add the bound on the relevant function

view this post on Zulip Sam Mohr (Jan 12 2025 at 06:15):

The rust example is

struct IteratorThing<I>
where
    I: Iterator,
{
    a: I,
    b: Option<I::Item>,
}

view this post on Zulip Sam Mohr (Jan 12 2025 at 06:15):

It lets you make an iterator efficiently

view this post on Zulip Sam Mohr (Jan 12 2025 at 06:15):

But yes, you could always just add that restraint to all methods

view this post on Zulip Sam Mohr (Jan 12 2025 at 06:15):

I don't think we need anything like this any time soon

view this post on Zulip Brendan Hansknecht (Jan 12 2025 at 16:57):

This is already used in current roc. For example the key in dict

view this post on Zulip Brendan Hansknecht (Jan 12 2025 at 16:59):

Dict specifies that its key must have eq and hash

view this post on Zulip Brendan Hansknecht (Jan 12 2025 at 16:59):

This avoids needing to specify it on essentially every single function definition in the Dict.roc file

view this post on Zulip Brendan Hansknecht (Jan 12 2025 at 17:01):

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

view this post on Zulip Richard Feldman (Jan 12 2025 at 17:03):

yeah I'd like to remove that Inspect restriction

view this post on Zulip Richard Feldman (Jan 12 2025 at 17:04):

Brendan Hansknecht said:

This avoids needing to specify it on essentially every single function definition in the Dict.roc file

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:

view this post on Zulip Richard Feldman (Jan 12 2025 at 17:05):

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!

view this post on Zulip Richard Feldman (Jan 12 2025 at 17:06):

also I just realized that static dispatch cleanly solves the "List has Eq only if its element type has Eq" problem

view this post on Zulip Richard Feldman (Jan 12 2025 at 17:06):

you just put the constraint on the equals function, bam, done

view this post on Zulip Richard Feldman (Jan 12 2025 at 17:06):

no need for any separate syntax or rules

view this post on Zulip Brendan Hansknecht (Jan 12 2025 at 17:13):

Richard Feldman said:

also I just realized that static dispatch cleanly solves the "List has Eq only if its element type has Eq" problem

This already just works with abilites

view this post on Zulip Brendan Hansknecht (Jan 12 2025 at 17:15):

Richard Feldman said:

Brendan Hansknecht said:

This avoids needing to specify it on essentially every single function definition in the Dict.roc file

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:

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.

view this post on Zulip Brendan Hansknecht (Jan 12 2025 at 17:17):

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.

view this post on Zulip Richard Feldman (Jan 12 2025 at 17:19):

Elm used to do that, but then removed the constraint

view this post on Zulip Richard Feldman (Jan 12 2025 at 17:19):

as an aside, I definitely prefer Dict.empty() over today's Dict.empty {} :big_smile:

view this post on Zulip Richard Feldman (Jan 12 2025 at 17:23):

in practice it didn't really help anything, just made more work for the type checker haha

view this post on Zulip Richard Feldman (Jan 12 2025 at 17:24):

bc you never just make an empty dictionary and leave it alone, you always insert stuff info it almost immediately

view this post on Zulip Richard Feldman (Jan 12 2025 at 17:24):

and insert has the constraint

view this post on Zulip Brendan Hansknecht (Jan 12 2025 at 17:25):

What makes comparable have a constraint in those elm docs? Just looks like a long type variable name.

view this post on Zulip Brendan Hansknecht (Jan 12 2025 at 17:26):

Asking cause it would nice if we could have something so short and succinct as that.

view this post on Zulip Richard Feldman (Jan 12 2025 at 17:42):

Elm just has a few hardcoded special type variables like comparable, they aren't extensibile in userspace

view this post on Zulip Richard Feldman (Jan 12 2025 at 17:42):

they just silently are constrained based on their names being language keywords

view this post on Zulip Brendan Hansknecht (Jan 12 2025 at 18:21):

Ah

view this post on Zulip Brendan Hansknecht (Jan 12 2025 at 18:21):

Does make it read nice

view this post on Zulip Brendan Hansknecht (Jan 12 2025 at 18:22):

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

view this post on Zulip Brendan Hansknecht (Jan 12 2025 at 18:23):

And even more so for more complex types

view this post on Zulip Richard Feldman (Jan 12 2025 at 18:38):

yeah I think we should allow type aliases for them

view this post on Zulip Richard Feldman (Jan 12 2025 at 18:38):

I think those were in the doc, but I could be misremembering

view this post on Zulip Brendan Hansknecht (Jan 12 2025 at 18:47):

Yeah, I think it is practically required for complex types like hashers

view this post on Zulip Richard Feldman (Jan 12 2025 at 18:49):

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

view this post on Zulip Brendan Hansknecht (Jan 12 2025 at 18:51):

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.

view this post on Zulip Brendan Hansknecht (Jan 12 2025 at 18:52):

I guess that means we kinda have implicit and explicit interfaces.

view this post on Zulip Brendan Hansknecht (Jan 12 2025 at 18:52):

Implicit is static dispatch. Explicitly is returning a struct of closures.

view this post on Zulip Sam Mohr (Jan 12 2025 at 21:31):

Yep, Richard has already suggested support for a.func(Str) -> U64 where a.Hasher as a postfix application of Hasher a

view this post on Zulip Sam Mohr (Jan 12 2025 at 21:33):

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

view this post on Zulip Anthony Bullard (Jun 23 2025 at 21:46):

While I'm bringing over the Num builtin module, I had some questions about the current "spec' for Custom Types.

view this post on Zulip Anthony Bullard (Jun 23 2025 at 21:55):

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