fsharp / fslang-suggestions

The place to make suggestions, discuss and vote on F# language and core library features
345 stars 21 forks source link

Allow the union pattern to be implemented explicitly #1152

Open dsyme opened 2 years ago

dsyme commented 2 years ago

In F#, discriminated unions are a pattern, e.g.

type U = A of value: int | B of value: string

generates code containing roughly this public API:

type U =
    static member NewA(value: int) = ...
    static member NewB(value: string) = ...
    member IsA: bool
    member IsB: bool
    member Tag: int
    static type A =
        inherit U
        member value: int
    static type B =
        inherit U
        member value: string
    static type Tags =
        | A = 0
        | B = 1

plus various attributes.

I propose it be possible to implement this pattern explicitly (unchecked) without committing to a representation of the backing data beyond the presence of subtypes A and B of the reference type U, and the presence of Tag/Tags.

This is quite a large feature:

The existing way of approaching this problem in F# is to accept the default representations for backing data.

Pros and Cons

The advantages of making this adjustment to F# are that programmers can control the ultimate representation of union types without subsequent consuming code changing and without breaking binary compatibility.

The disadvantages of making this adjustment to F# are that

On the whole I feel this is "too costly" to do for the benefits it brings but I am recording the idea here, partly because it clarifies what was meant by #726 and partly because it relates to #154 and #277

Extra information

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

Related suggestions: https://github.com/fsharp/fslang-suggestions/issues/164 deals with some similar issues for records.

Affidavit (please submit!)

Please tick this by placing a cross in the box:

Please tick all that apply:

For Readers

If you would like to see this issue implemented, please click the :+1: emoji on this issue. These counts are used to generally order the suggestions by engagement.

En3Tho commented 2 years ago

This possibly can enable better interop with "variant-style" unions e.g.

type JsonUnion = {
     Tag: CaseEnum
     Prop1: Case1 option (or nullable)
     Prop2: Case2 option (or nullable)
     Prop3: Case3 option (or nullable)
}

where it's guaranteed to have 1 of N cases at one time. It's closer to JS/C# folks usually do I guess. For example, telegram typically sends it's messages like that and I've used multiple ActivePatterns for better interop with C# Telegram library

module Update =
    [<return: Struct>]
    let inline (|Unknown|_|) (update: Update) =
        match update.Type with
        | UpdateType.Unknown -> ValueSome()
        | _ -> ValueNone

    [<return: Struct>]
    let inline (|Message|_|) (update: Update) =
        match update.Type with
        | UpdateType.Message -> ValueSome update.Message
        | _ -> ValueNone

    [<return: Struct>]
    let inline (|InlineQuery|_|) (update: Update) =
        match update.Type with
        | UpdateType.InlineQuery -> ValueSome update.InlineQuery
        | _ -> ValueNone

    [<return: Struct>]
    let inline (|ChosenInlineResult|_|) (update: Update) =
        match update.Type with
        | UpdateType.ChosenInlineResult -> ValueSome update.ChosenInlineResult
        | _ -> ValueNone

    // etc

We could instead generate our own DU-like types for this kind of interop. For example, C# could just see it as Tag + Property but F# could see it as proper Union type.

Also, this could in theory provide perf-oriented people to write custom struct DU's with overlapped fields of any type (at own risk)

roboz0r commented 1 year ago

provide perf-oriented people to write custom struct DU's

This would be good not just for perf reasons but low-level interop in general. I'm working with a C API using overlapped unions implemented something like this:

    [<StructLayout(LayoutKind.Explicit, Size = 24)>]
    type Values = 
        struct
            [<FieldOffset(0)>] val mutable a:float
            [<FieldOffset(0)>] val mutable b:nativeint
            [<FieldOffset(0)>] val mutable c:int
            [<FieldOffset(0)>] val mutable d:SomeEnum
        end

    [<StructLayout(LayoutKind.Sequential)>]
    type Union = 
        struct
            val mutable value:Values
            val mutable tag:UnionEnum
        end

I then use partial active patterns and static members to safely read and write these structs. It would be nice to simplify this but I can understand it not being a high priority.