fsharp / fsharp/fslang-suggestions

Erased type-tagged anonymous union types

Open
#538 89 comments 88 reactions 0 assignees View on GitHub
approved-in-principle area: fable-inspired area: type-system
Dominant language
No language data
Stars
373
Forks
21
PR merge metrics
No merged PRs in 30d

Description

## Modified Proposal (by @dsyme)

This is a suggestion to add adhoc structural type-tagged union types where
* type syntax `(A|B)` or `Typed(A|B)` or `Typed`
* each type of (A|B|C|...) is distinct and non-overlapping w.r.t. runtime type tests
* such types are erased to `object`
* introducing such a union value would need to be either explicit (e.g. using some new operator like `Typed`) or type-directed or both

See https://github.com/fsharp/fslang-suggestions/issues/538 for original proposal. e.g. this would allow some or all of these:

let generateValue1 () : Typed(int | string)) = if Monday then 2 else "4"

let generateValue2 () = if Monday then Typed 2 else Typed "4"

let eliminateValue (x : Typed(int | string)) = ...
match x with
| :? int as i -> ...
| :? string as s -> ...

let eliminateValue2 x = ...
match x with
| Typed(i : int) -> ...
| Typed(s; string) -> ...

type Allowed = Typed(int | string)

There are plenty of questions about such a design (e.g. can you eliminate "some but not all" of the cases in a match? Is column-polymorphism supported?). However putting those aside, such a construct already has utility in the context of Fable, since it corresponds pretty closely to Typescript unions and how JS treats values. It also has lots of applications in F# programming, especially if the use of `Typed` can be inferred in many cases.

Now, this construct automatically gives a structural union message type , e.g.

type MsgA = MsgA of int * int
let update1 () = Typed (MsgA (3,4))

type MsgB = MsgB of int * int
let update2 () = Typed (MsgB (3,4))

val update1 : unit -> Typed MsgA // actually : unit -> Typed (MsgA | ..), i.e. column-generic on use
val update2 : unit -> Typed MsgB // actually : unit -> Typed (MsgB | ..), i.e. column-generic on use

and a combination of update1 and update2 would give

let update = combine update1 update2

val update : unit -> Typed (MsgA | MsgB)

As noted in the comments, some notion of column-generics would likely be needed, at least introduced implicitly at use-sites.

## Original Proposal (@alfonsogarciacaro)

I propose we add erased union types as an F# first citizen. The erased union types already exist in Fable to emulate Typescript (non-labeled) union types:

http://fable.io/docs/interacting.html#Erase-attribute

Note that Fable allows you to define your custom erased union types, but this is because it's painful to type a generic one like `U2.Case1`. If the compiler omits the need to prefix the argument, this wouldn't be necessary and using a generic type can be the easiest solution.

The F# compiler could convert the following code:

```fsharp
// The name ErasedUnion is tentative
// The compiler should check the generic args are different
let foo(arg: ErasedUnion) =
match arg with
| ErasedUnion.Case1 s -> s.Length
| ErasedUnion.Case2 i -> i

// No need to instantiate ErasedUnion, but the compiler checks the type
foo "hola"
foo 5
// This doesn't compile
foo 5.
```

Into something like:

```fsharp
let foo(arg: obj) =
match arg with
| :? string as s -> s.Length
| :? int as i -> i
| _ -> invalidArg "arg" "Unexpected type"
```

- **Pros**: It will make the Fable bindings generated from Typescript declaration files much more pleasant to work with.

- **Cons**: It's a feature that seems to be exclusively dedicated to interact with a dynamic language like JS.

- **Estimated cost** (XS, S, M, L, XL, XXL): S

## Alternatives

For Fable it's been suggested to generate overloads in the type bindings instead of using erased union types:

```typescript
interface IFoo {
foo(arg: string | number): void;
}
```

```fsharp
type IFoo =
abstract foo: arg: string -> unit
abstract foo: arg: number -> unit
```

However these has some problems:

- It can quickly explode when you have several erased union arguments
- Due to type inference the F# compiler many times doesn't know which overload to use
- Cannot be used in properties
- Doesn't let you use erased unions yourself.

## Affadavit

Please tick this by placing a cross in the box:
* [x] This is not a question (e.g. like one you might ask on [stackoverflow](http://stackoverflow.com)) and I have searched stackoverflow for discussions of this issue
* [x] I have [searched both open and closed suggestions on this site](http://github.com/fsharp/fslang-suggestions/issues) and believe this is not a duplicate
* [x] This is not something which has obviously "already been decided" in previous versions of F#. If you're questioning a fundamental design decision that has obviously already been taken (e.g. "Make F# untyped") then please don't submit it.

Please tick all that apply:
* [x] This is not a breaking change to the F# language design
* [x] I would be willing to help implement and/or test this
* [ ] I or my company would be willing to help crowdfund F# Software Foundation members to work on this

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.