fslang-suggestions
fslang-suggestions copied to clipboard
[Suggestion] String-backed enums in F#
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)
- Literals as types - https://github.com/fsharp/fslang-suggestions/issues/656
- Predefined value cases in discriminated unions - https://github.com/fsharp/fslang-suggestions/issues/1051
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 :+1: emoji on this issue. These counts are used to generally order the suggestions by engagement.
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?
I would say that until such a time that C# and the CLR choose to adopt the feature, it shouldn't land in F#: https://github.com/dotnet/csharplang/issues/2849
Why don't you use a regular DU and use
ToStringon 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
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
Myriad may also be relevant: https://moiraesoftware.github.io/myriad/docs/plugins/du-extensions/
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#: https://github.com/dotnet/csharplang/issues/2849
Fully agree
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# https://github.com/dotnet/csharplang/issues/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.
@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
Wouldn't literal types and erased union types void the need for this?
They would, however literal types suggestion is not even approved
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?
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"