Essay - Published: 2023.12.27 | create | enums | fsharp | tech |
DISCLOSURE: If you buy through affiliate links, I may earn a small commission. (disclosures)
Recently I've been playing around with server-side rendering in F#. In one experiment I tried out HTMX which utilizes element IDs to uniquely target the HTML element to swap / act on. This means if you get the element ID wrong your page interaction breaks (that's bad!).
Thinking about how to prevent this, I figured a type-safe collection of the finite potential elements I had on my page was a pretty good way to model it. This made me think Discriminated Unions and since the id was a string, why not a string-backed Enum which is essentially a DU with a single value.
type MyStrEnum =
| A = "a-id"
| B = "b-id"
| C = "c-id"
This serves several purposes:
Alas F# does not currently support string-backed Enums natively. Functional / performance purists are probably saying "duh, if you are checking the value of an Enum directly you are doing it wrong". But I want to do it anyway (for science, humanity, and simplicity!).
So in this post we'll explore how to define a string-backed Enum in F#:
Q: How to simply define a string-backed Enum in F#?
I experimented with several ways to try and achieve a string-backed enum-like experience that offers a simple definition and usage pattern:
After several iterations I've landed on a pretty good solution that offers a decent balance using string Literals.
In this post we'll explore a couple options and my journey to landing on my current best solution.
My first solution to this was to use a different type entirely which mapped my int-backed enum to strings and surfaced useful helper methods.
type MainPageTargets =
| FullPage = 0
| SentinelTable = 1
let mainPageTargetsStrEnum = StrEnum<MainPageTargets>(
fun e ->
match e with
| MainPageTargets.FullPage -> "full-page"
| MainPageTargets.SentinelTable -> "sentinel-table"
)
This works fine and is pretty simple to use. But there is a bit of complexity under the hood including lots of Reflection, the requirement for it to be an Enum, and internal processing.
See: Full Source Code
The usage is simple but the logic isn't colocated and it seems like a lot of stuff to just get a simple string-backed Enum.
So my previous solution worked but it didn't feel good. So it felt like there was a better way but I couldn't think of any.
So I reached out to r/fsharp for help. After lots of discussion and several different solutions, I think I landed on a relatively simple solution that I wouldn't feel too bad using.
Thanks everyone for your inputs!
In this version we are using string Literals which allow us to use these values directly in match cases.
You can play around with this option on Replit.
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
This is the best solution I've found yet for string-backed Enums in F#. That said I'm always on the hunt for even simpler solutions (ideally we'd just get these natively in F# (see: F# language suggestion for string enums)). So let me know if you have thoughts for how to do this better so I can further simplify in my future projects!
If you liked this post you might also like:
The best way to support my work is to like / comment / share for the algorithm and subscribe for future updates.