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

[Proposal] Nestable union types? #2858

Closed
cannorin opened this issue Apr 17, 2022 · 4 comments · Fixed by #2863
Closed

[Proposal] Nestable union types? #2858

cannorin opened this issue Apr 17, 2022 · 4 comments · Fixed by #2863

Comments

@cannorin
Copy link
Contributor

Since Fable only supports up to 8-union, ts2fable has to emit obj when it encounters 9-union or more.

I was experimenting around and got this (live):

open Fable.Core
open Fable.Core.JsInterop

type [<Erase>] UMore<'a, 'b, 'c> =
    | Case1 of 'a
    | Case2 of 'b
    | CaseMore of 'c
    static member op_ErasedCast(x:'a) = Case1 x
    static member op_ErasedCast(x:'b) = Case2 x
    static member op_ErasedCast(x:'c) : UMore<_, _, 'c> = CaseMore x
    static member inline op_ErasedCast(x:'t) : UMore<_, _, ^U> = CaseMore (!^x)

// CaseMore works when unnested
let a1 : UMore<int, string, bool> = !^true

// CaseMore works when nested
let a2 : UMore<int, string, UMore<bool, float, unit>> = !^4.2

// CaseMore works when nested 2 times
let a3 : UMore<int, string, UMore<float, bool, UMore<unit, int64, float32>>> = !^4.2f

// unnested CaseMore inside nested CaseMore works
let a4 : UMore<int, string, UMore<bool, float, unit>> = !^()

// compiler does not complain if the same type (int) appears twice
let a5 : UMore<int, string, UMore<int, string, float>> = !^42

So I found that nestable union types are possible even with Fable's limited support to trait (SRTP) calls.

Should we do this? (Modifying U8 would break the existing codes, so I would add U9 with nest support)

@alfonsogarciacaro
Copy link
Member

Thanks @cannorin! This looks like a good solution so we don't need to set an arbitrary limit for TS unions. It's a bit more cumbersome but I guess that's ok if it's mainly intended for auto-generated code. What do you think @MangelMaxime @Booksbaum?

@Booksbaum
Copy link
Contributor

Accessing a nested value is then quite laborious:

let a8 : UMore<int, unit, UMore<int, unit, UMore<int, string, unit>>> = !^ "foo"
match a8 with
| CaseMore (CaseMore (Case2 str)) ->
    printfn "got %s" str
| _ -> ()

-> must track how deep nested the Case is (and what actual Case in final nesting) instead of just what index it has.
(-> better for creating union than consuming it)

But way better than obj.
-> I think it's an ok solution for now



One consideration is probably how fast RFC 1092 gets implemented -> solves problem without need for nested unions
But considering how complex that is and how slowly implementation goes it'll likely take a long time




One issue I found: deeply nested unions:

let a9 : UMore<int, unit, UMore<int, unit, UMore<int, unit, UMore<int, unit, UMore<int, unit, UMore<int, unit, UMore<int, unit, UMore<int, unit, UMore<int, unit, UMore<int, unit, UMore<int, unit, UMore<int, unit, UMore<int, unit, UMore<int, unit, UMore<int, unit, UMore<int, unit, UMore<int, unit, UMore<int, unit, UMore<int, unit, UMore<int, unit, UMore<int, unit, UMore<int, unit, UMore<int, unit, UMore<int, unit, UMore<int, unit, UMore<int, unit, UMore<int, unit, UMore<int, unit, UMore<int, unit, UMore<int, unit, UMore<int, unit, UMore<int, unit, UMore<int, unit, UMore<int, unit, UMore<int, unit, UMore<int, unit, string>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> = !^ "foo"

(repl)

-> Compilation takes forever (or cannot resolve at all?). I terminated comilation after a couple of minutes.
(same behaviour of course for tooling (like tooltips))

Unlikely to have such intense union (especially when you only nest after 8 prev values). But possible from a tool like ts2fable. That's going to be a funny debugging session of why the F# compiler doesn't come to an end....
(Though ts2fable wouldn't instantiate the type, but just declare it -> compilation would only break when user accesses the type (like passing something into a function that expects that type))

@MangelMaxime
Copy link
Member

Removing arbitrary limitation is always a good thing.

Only thing is that the code to access the value is not really easy to read/understand. But like @Booksbaum said, it is probably better than obj and if need I suppose the user can always box or unbox the value to "simplify" the code.

let value = unbox<string> myComplexNestedUnionValue

// to avoid stuff like:
let value =
    match a8 with
    | CaseMore (CaseMore (Case2 str)) ->
        str
    | _ -> failwith "unexpected"

@cannorin
Copy link
Contributor Author

cannorin commented Apr 19, 2022

Regarding the problem on creating U9+:

  • As @Booksbaum pointed out, !^ takes forever to resolve if the target type is deeply nested and it involves a lot of recursive SRTP calls
  • I think on ts2fable we can set the limit to how deeply nested U9 can become? (to add warning and/or emit obj instead)
  • I need to do some experiments to know the limit

Regarding the problem on consuming U9+:

  • Matching against Un types only works if they only contain basic types (typeof) and classes (instanceof), but there are only a handful of basic types and ts2fable (almost?) never emits classes, so my guess is match would be useless for most 9-unions or more
  • As @MangelMaxime said, people can always use unbox or !! (which they would have used for obj) so I think it's ok?

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 a pull request may close this issue.

4 participants