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

[Suggestion] String-backed enums in F# #1343

Open
2 tasks
SIRHAMY opened this issue Dec 16, 2023 · 12 comments
Open
2 tasks

[Suggestion] String-backed enums in F# #1343

SIRHAMY opened this issue Dec 16, 2023 · 12 comments

Comments

@SIRHAMY
Copy link

SIRHAMY commented Dec 16, 2023

I propose we add string-backed enums / string enums

type MyStrEnum =
  | A = "a-id"
  | B = "b-id"
  | C = "c-id"

The existing way of approaching this problem in F# is ...

  • Create a DU
  • Either:
    • Build a type on top of that for Enum -> String, String -> Enum
    • Add helpers on the DU to simulate StrEnum
    • Q: maybe other ways?

Here is the best pattern I've been able to come up with so far. It works fine but is a bit of boilerplate to setup for a relatively common occurrence.

(lmk if you have a better soln - I am in the market for a simpler one!)

let [<Literal>] AId = "a-id"
let [<Literal>] BId = "b-id"
let [<Literal>] CId = "c-id"

type MyStrEnum =
  | A 
  | B 
  | C 
  member this.AsString() =
    match this with 
    | A -> AId
    | B -> BId
    | C -> CId
  static member AsEnum (s) : MyStrEnum option =
    match s with 
    | AId -> Some A 
    | BId -> Some B 
    | CId -> Some C
    | _ -> None

More details in: String-backed Enums in F#

Pros and Cons

The advantages of making this adjustment to F# are ...

  • F# becomes simpler for a common use case

The disadvantages of making this adjustment to F# are ...

  • Probably perf issues?
  • Probably breaks something?
  • Diverges from C# / dotnet implementation?

Extra information

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

Related suggestions: (put links to related suggestions here)

Affidavit (please submit!)

Please tick these items by placing a cross in the box:

  • [x ] This is not a question (e.g. like one you might ask on StackOverflow) and I have searched StackOverflow for discussions of this issue
  • [x ] This is a language change and not purely a tooling change (e.g. compiler bug, editor support, warning/error messages, new warning, non-breaking optimisation) belonging to the compiler and tooling repository
  • [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
  • [x ] I have searched both open and closed suggestions on this site and believe this is not a duplicate

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 👍 emoji on this issue. These counts are used to generally order the suggestions by engagement.

@kerams
Copy link

kerams commented Dec 16, 2023

The CLR does not support this, so these would not be true enums.

F# becomes simpler for a common use case

Perhaps I live in a bubble, but I don't think it's particularly common outside of JS (which is why Fable supports attribute-based string enums).

Why don't you use a regular DU and use ToString on a case to get an identifier you can use?

@cartermp
Copy link
Member

I would say that until such a time that C# and the CLR choose to adopt the feature, it shouldn't land in F#: dotnet/csharplang#2849

@SIRHAMY
Copy link
Author

SIRHAMY commented Dec 16, 2023

Why don't you use a regular DU and use ToString on a case to get an identifier you can use?

The problem with simple Enum.ToString is:

  • The variable name is now the exact string - okay if you want that but ime you often don't
  • No great way imo to go from Enum -> String - You can always get into Enum.Parse / Enum.TryParse but I'd prob write my own F# wrappers around these to get around exceptions, out vars / nulls

So def doable but I don't think it's as good as native StrEnums

cc @kerams

@bartelink
Copy link

bartelink commented Dec 17, 2023

If one is looking for a practical ways to handle enums with string values without language level changes/support in the interim, TypeShape's UnionEncoder lets you (if you use a DU) have a default mapping and override some cases via DataMember

FsCodec does some wrapping of that and also has stuff like https://github.com/jet/fscodec?tab=readme-ov-file#typesafeenum-fallback-converters-using-jsonisomorphism

@smoothdeveloper
Copy link
Contributor

Myriad may also be relevant: https://moiraesoftware.github.io/myriad/docs/plugins/du-extensions/

@Lanayx
Copy link

Lanayx commented Dec 19, 2023

Perhaps I live in a bubble, but I don't think it's particularly common outside of JS

It's a very common task in backend as well, first - whenever you need to implement any protocol with values of constant strings, second - when you want to keep enum values in database, it's much better to keep strings there and not to be afraid of refactoring (I'm referring to .ToString() solution)

I would say that until such a time that C# and the CLR choose to adopt the feature, it shouldn't land in F#: dotnet/csharplang#2849

Fully agree

@nodakai
Copy link

nodakai commented Dec 20, 2023

When you mention enums, are you suggesting they should inherit the same characteristic with System.Enum?

type E = | A = 0 | B = 1

printfn "%O" <| enum<E> 123  // compiles and displays 123

The initial proposal for C# dotnet/csharplang#2849 indeed wants that behavior:

OSPlatform platform = (OSPlatform)"Apple Toaster with Siri Support";

This implies that a match expression won't be exhaustive without including | other -> .... I wonder how this approach would resonate with the F# community. While anticipating an unknown case is crucial in ser/des contexts, it might be redundant for internal APIs.

@Lanayx
Copy link

Lanayx commented Dec 20, 2023

@nodakai I think the idea behind enums is that we don't want 2 implementations of the same thing (C# and F#) that differ with only characteristic - handling of non-defined cases. Once C# adds that, F# users will still face the need of handling such cases.

I would imagine that for F# we could add generic mechanism for any enum, like attribute [<Exhaustive>] that will allow F# users to skip additional wildcard match for any standard .NET enum regardless of it's base (int, string etc), while still generating that case behind the scene and throwing exception in runtime for unhandled case

@Swoorup
Copy link

Swoorup commented Feb 8, 2024

Wouldn't literal types and erased union types void the need for this?

@Lanayx
Copy link

Lanayx commented Feb 8, 2024

They would, however literal types suggestion is not even approved

@Swoorup
Copy link

Swoorup commented Feb 8, 2024

Sure, I think it would become a lot easier to add this feature atop, once the erased union types have landed. I can't see a good reason for this particular approach or at least the examples aren't that convincing enough as an alternative to literal types + erased union.

type MyStrEnum =
  | A = "a-id"
  | B = "b-id"
  | C = "c-id"

Here the strings are self descriptive and the specifying the type name in the case is moot?

@voronoipotato
Copy link

Probably something you could gen up with myriad tbh.

type Stuff =
    | A
    | B
    | C
    with override this.ToString() =
        match this with
        | A -> "a-id"
        | B -> "b-id"
        | C -> "c-id"
module Stuff = 
    let ofStr = function |  "a-id" -> Ok A | "b-id" -> Ok B | "c-id" -> Ok C | _ -> Error "unsupported string"

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

10 participants