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 Sub-set to Super-set casts for Tagged Union #439

Open
Booksbaum opened this issue Apr 17, 2022 · 3 comments
Open

Support Sub-set to Super-set casts for Tagged Union #439

Booksbaum opened this issue Apr 17, 2022 · 3 comments

Comments

@Booksbaum
Copy link
Contributor

interface Circle {
  kind: "circle";
  radius: number;
}
 
interface Square {
  kind: "square";
  sideLength: number;
}
 
type CircleOrSquare = Circle | Square;

interface Triangle {
  kind: "triangle";
  sideLength: number;
}

type Shape = CircleOrSquare | Triangle;

=>

// [...]
type [<TypeScriptTaggedUnion("kind")>] [<RequireQualifiedAccess>] CircleOrSquare =
    | Circle of Circle
    | Square of Square
    static member inline op_ErasedCast(x: Circle) = Circle x
    static member inline op_ErasedCast(x: Square) = Square x

// [...]

type [<TypeScriptTaggedUnion("kind")>] [<RequireQualifiedAccess>] Shape =
    | Circle of Circle
    | Square of Square
    | Triangle of Triangle
    static member inline op_ErasedCast(x: Circle) = Circle x
    static member inline op_ErasedCast(x: Square) = Square x
    static member inline op_ErasedCast(x: Triangle) = Triangle x

=> 'Shape is superset of CircleOrSquare' is lost

-> Add additional op_ErasedCast to convert from CircleOrSquare to Shape:

    static member inline op_ErasedCast(x: CircleOrSquare) =
        match x with
        | CircleOrSquare.Circle circle -> Shape.Circle circle  
        | CircleOrSquare.Square square -> Shape.Square square

(see #438 (comment))



Maybe an additional conversion from Shape to CircleOrSquare?

Typescript can detect matching subset:

function onCircleOrSquare(cos: CircleOrSquare) {
    console.log("circle or square")
}

function onShape(s: Shape) {
    if(s.kind == "square" || s.kind == "circle") {
        onCircleOrSquare(s)
    }
    else {
        console.log("just shape")
    }
}

In F# maybe some (explicit) cast function?

let asCircleOrSquare (s: Shape): CircleOrSquare option =
    match s with
    | Shape.Circle c -> Some (!^ c) 
    | Shape.Square s -> Some (!^ s)
    | _ -> None

let onCircleOrSquare (cos: CircleOrSquare) =
    printfn "circleOrSquare"
let onShape (s: Shape) =
    printfn "shape"
    match s |> asCircleOrSquare with
    | Some cos -> onCircleOrSquare (cos)
    | None -> printfn "No circle or square"

onShape (!^ circle)
onShape (!^ triangle)

(fable repl)

F# function in Shape module? member on Shape? member on CircleOrShape (probably not -> CircleOrShape sub-set in Shape, not part of Shape in CircleOrShape decl)? Probably needs try in name (tryAs...)

@cannorin
Copy link
Contributor

cannorin commented Apr 19, 2022

TypeScriptTaggedUnion keeps the original JS value and instead converts the match statements directly, so op_ErasedCast(x: CircleOrSquare) : Shape can be safely implemented with !! or [<Emit("$0")>] (no need to match):

[<Emit("$0")>] static member op_ErasedCast(x: CircleOrSquare) : Shape = jsNative

For asCircleOrSquare it isn't easy as the above, but I think we can add the following:

[<Emit("$0.kind")>] member x.kind : string = jsNative

then do:

let asCircleOrSquare (s: Shape) : CircleOrSquare option =
    if (s.kind == "square" || s.kind == "circle") then
        Some !!s
    else
        None

I guess this will result in a more performant JS output. Also I think it needs try as a prefix and tryAsCircleOrSquare should live in Shape too (function or member).

@Booksbaum
Copy link
Contributor Author

can be safely implemented with !! or [<Emit("$0")>] (no need to match)

I like explicit matching -- even when completely erased at compile time.
But it's nice when reading the code: shows exactly what cases form the subtype.
For direct cases (like Circle) it emits the case ctor while !! would be valid too.

But of course: one case ctor is easier to do than a match with lots of cases -> longer source and potential more complex to read.

Directly just passing the js object along without any matching is of course easier to implement -> I think we should do that

I prefer !! over EmitAttribute: Easier to read (Attribute is split in two: Attribute & jsNative), doesn't need JS in generated source and additional hides it's implementation from user (while Attribute can be seen)


For asCircleOrSquare it isn't easy as the above, but I think we can add the following: [...]

Doesn't the match boil down to exactly that?

// match
export function asCircleOrSquare(s) {
    switch (s.kind) {
        case "circle": {
            const c = s;
            return some(c);
        }
        case "square": {
            const s_1 = s;
            return some(s_1);
        }
        default: {
            return void 0;
        }
    }
}

// kind
export function asCircleOrSquare2(s) {
    if (((s.kind) === "square") ? true : ((s.kind) === "circle")) {
        return some(s);
    }
    else {
        return void 0;
    }
}

(Don't know which exactly is faster (probably get reduced to same? JS interpreters are quite clever nowadays). Though if there's a speed difference I think it's negligible).

I which case does directly using kind work while a match doesn't?
I don't want to add another member to the Tagged Union -- especially one that's already handled by Fable.

@cannorin
Copy link
Contributor

I prefer !! over EmitAttribute

👍

Doesn't the match boil down to exactly that?

Ah, I can see match is more performant since it uses switch under the hood. Disregard that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants