fslang-suggestions icon indicating copy to clipboard operation
fslang-suggestions copied to clipboard

Allow the union pattern to be implemented explicitly

Open dsyme opened this issue 2 years ago • 1 comments

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:

  • there are some variations to consider for single case unions
  • there are variations to consider for struct unions
  • there will be responses that the pattern is too complex to write by hand. For example people may request that it is possible to omit the Tag and Tags even if pattern matching over a large number of union cases is slower for these cases (preferring type tests on the input type)
  • there will be responses that authoring is not the same as proposed C# unions, which may reduce the utility of the feature
  • there will be questions about whether it is possible to use FSharp.Reflection on these types, given this is driven by various additional attributes
  • there will be questions about what happens if people get the pattern slightly wrong
  • there will be questions about whether this allows https://github.com/fsharp/fslang-suggestions/issues/154
  • there will be questions about whether the Tags need to form a contiguous 0..n-1 list
  • there will be questions about whether the attributes need to be present
  • it relies on #277

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

  • it is complex to use (more complex than people will like)
  • its use cases are rare
  • it may inhibit later development of features for unions

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:

  • [ ] This is not a question (e.g. like one you might ask on stackoverflow) and I have searched stackoverflow for discussions of this issue
  • [ ] I have searched both open and closed suggestions on this site and believe this is not a duplicate
  • [ ] 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:

  • [ ] This is not a breaking change to the F# language design
  • [ ] I or my company would be willing to help implement and/or test this

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.

dsyme avatar Jun 16 '22 13:06 dsyme

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)

En3Tho avatar Jul 15 '22 08:07 En3Tho

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.

roboz0r avatar Oct 31 '22 00:10 roboz0r