Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support coercing string to elgible untagged variants #6443

Merged
merged 5 commits into from
Oct 19, 2023

Conversation

zth
Copy link
Collaborator

@zth zth commented Oct 18, 2023

Adds support for a very specific (but useful) coercion - from string to an unboxed (elgible) variant:

@unboxed
type x = One | Two | Other(string)

let x = "Hello"

let y = (x :> x)

let f = switch y {
| One => "hollo"
| _ => "bollo"
}

This works because the variant is unboxed, and because there's a "catch all" unboxed string constructor in the variant, that will catch any provided string that doesn't map to One or Two.

Criteria for this coercion to work:

  • Variant must be unboxed
  • All variant construtors must be coercable to string (criterion for this is covered elsewhere)
  • There must be a constructor case with a string payload. This acts as a "catch all" (because it's unboxed) and is what makes the variant cover all possible cases of string

@cristianoc
Copy link
Collaborator

This is fine as long as a catch-all case with string payload exists.
Though it seems super niche. It seems more common to want to coerce a specific constant string, such as "One".

@mununki
Copy link
Member

mununki commented Oct 19, 2023

Not sure I understand the context fully, but how about this case in the runtime?

@unboxed
type x = One | Two

let x = "Hello"

let y = (x :> x)

let f = switch y {
| One => "hollo"
| Two => "bollo"
}

// Console.log(f) result ???

@zth
Copy link
Collaborator Author

zth commented Oct 19, 2023

It's indeed super niche and I've opted to only support strings here, while we could theoretically also support floats. But, it's quite useful in everyday work, because it gives the developer a "free" (DX wise) "variant from string" style function, where the current alternative is either an identity cast, or manually enumerating all strings and mapping them to the correct variant constructors. So, here we piggy back on the pattern match compiler already doing all of that work for us anyway.

"Variant from string" functions have a ton of use cases of course, since you need them anytime you want to decode a string to variant. Which is often in the JS world, since so much is stored as strings. A few examples of concrete use cases:

  • Dealing with form elements like selects, radios, etc. They always return strings, and if you want to map them to your own variant (which you almost always want) you need to manually write and maintain a mapper from string to variant, which is a lot of work, and error prone
  • Local storage, session storage, etc. All strings.
  • Message communication by strings, like web sockets, workers, window.onMessage, and so on.
  • Essentially mapping any external string to a well-known variant format. If you have full control over the external type definition you can just put the unboxed variant instead of string directly. But, in many cases you don't.

I will say though that this is approaching "weird" territory and feels like an advanced feature. All in all though I think the benefits overweigh the costs.

@zth
Copy link
Collaborator Author

zth commented Oct 19, 2023

Not sure I understand the context fully, but how about this case in the runtime?

@unboxed
type x = One | Two

let x = "Hello"

let y = (x :> x)

let f = switch y {
| One => "hollo"
| Two => "bollo"
}

// Console.log(f) result ???

This is a type error because there's no "catch all" constructor with a string payload. So, can't coerce that from a string because the variant doesn't cover all possible cases for string.

@mununki
Copy link
Member

mununki commented Oct 19, 2023

@unboxed
type x = One | Two

let x = "Hello"

let y = (x :> x)

let f = switch y {
| One => "hollo"
| Two => "bollo"
| _ => "hello"
}

Would this code be compiled?

@mununki
Copy link
Member

mununki commented Oct 19, 2023

@unboxed
type x = One | Two

let x = "Hello"

let y = (x :> x)

let f = switch y {
| One => "hollo"
| Two => "bollo"
| _ => "hello"
}

Would this code be compiled?

I read the PR again, then it seems not compiled.

@cristianoc
Copy link
Collaborator

cristianoc commented Oct 19, 2023

It's indeed super niche and I've opted to only support strings here, while we could theoretically also support floats. But, it's quite useful in everyday work, because it gives the developer a "free" (DX wise) "variant from string" style function, where the current alternative is either an identity cast, or manually enumerating all strings and mapping them to the correct variant constructors. So, here we piggy back on the pattern match compiler already doing all of that work for us anyway.

"Variant from string" functions have a ton of use cases of course, since you need them anytime you want to decode a string to variant. Which is often in the JS world, since so much is stored as strings. A few examples of concrete use cases:

  • Dealing with form elements like selects, radios, etc. They always return strings, and if you want to map them to your own variant (which you almost always want) you need to manually write and maintain a mapper from string to variant, which is a lot of work, and error prone
  • Local storage, session storage, etc. All strings.
  • Message communication by strings, like web sockets, workers, window.onMessage, and so on.
  • Essentially mapping any external string to a well-known variant format. If you have full control over the external type definition you can just put the unboxed variant instead of string directly. But, in many cases you don't.

I will say though that this is approaching "weird" territory and feels like an advanced feature. All in all though I think the benefits overweigh the costs.

What I had in mind as super niche, is having a catch-all case Other(string). Once you have such a case, then surely the conversion is natural.
Whether or not you have that catch-all, you might still want to convert specific string constants to cases of the type. So I guess my only comment is that there are other kinds of casts in that generic direction, which might be more common (or who knows perhaps not), and are not covered currently.
That said, those conversions are easy to implement: "One" :> t is the same as One which is easy but a bit indirect.

@zth
Copy link
Collaborator Author

zth commented Oct 19, 2023

@cristianoc yeah that's a good point. If we ever were to implement the "subtype matching" (lack of a better name) for variants that polyvariants has, one could pretty conveniently achieve decoding into things without default cases too:

@unboxed
type myVariant = One | Two

@unboxed
type withUnknownCase = | ...myVariant | Unknown(string)

let myVariantFromString = (str: string): option<myVariant> => {
  switch (str :> withUnknownCase) {
  | ...myVariant as value => Some(value)
  | Unknown(_) => None
  }
}

@cristianoc
Copy link
Collaborator

A better example, for the case @as(20) Twenty is 20 :> t vs Twenty.

@cristianoc
Copy link
Collaborator

@cristianoc yeah that's a good point. If we ever were to implement the "subtype matching" (lack of a better name) for variants that polyvariants has, one could pretty conveniently achieve decoding into things without default cases too:

@unboxed
type myVariant = One | Two

@unboxed
type withUnknownCase = | ...myVariant | Unknown(string)

let myVariantFromString = (str: string): option<myVariant> => {
  switch (str :> withUnknownCase) {
  | ...myVariant as value => Some(value)
  | Unknown(_) => None
  }
}

No I'm not thinking about weird catch-all cases. I'm thinking about what people use every day.

Base automatically changed from coerce-untagged-variants-with-payload-to-primitive to master October 19, 2023 12:35
@tsnobip
Copy link
Contributor

tsnobip commented Oct 19, 2023

There is still a @deriving(jsConverter) for polymorphic variant to convert from and to string we could remove from the code base thanks to this feature.

@zth zth force-pushed the coerce-string-to-elgible-untagged-variants branch from af7279c to 3ee079d Compare October 19, 2023 17:30
@zth zth marked this pull request as ready for review October 19, 2023 17:31
@zth zth requested a review from cristianoc October 19, 2023 17:31
@cristianoc
Copy link
Collaborator

"All variant construtors must be coercable to string "

Why?

@zth
Copy link
Collaborator Author

zth commented Oct 19, 2023

Good point, thinking of it again that's not needed, it's only needed that it's unboxed and has a catch-all string case.

@zth
Copy link
Collaborator Author

zth commented Oct 19, 2023

@cristianoc removed the requirement for all variant constructors to be coercable to string.

CHANGELOG.md Outdated Show resolved Hide resolved
@zth zth merged commit 78b3172 into master Oct 19, 2023
2 checks passed
@zth zth deleted the coerce-string-to-elgible-untagged-variants branch October 19, 2023 20:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants