24

I want to get the equivalent of Enum.GetName for an F# discriminated union member. Calling ToString() gives me TypeName+MemberName, which isn't exactly what I want. I could substring it, of course, but is it safe? Or perhaps there's a better way?

Brian
  • 117,631
  • 17
  • 236
  • 300
Dmitri Nesteruk
  • 23,067
  • 22
  • 97
  • 166
  • 1
    Beware! The very same expression, x.ToString(), will at different program runs sometimes give me AssemblyName+TypeName and sometimes AssemblyName+TypeName+MemberName. Another identical expression on the same type elsewhere will always give me AssemblyName+TypeName+MemberName. Same problem with x.GetType().Name. The accepted answer is good. – Bent Tranberg May 23 '16 at 08:28

4 Answers4

33

You need to use the classes in the Microsoft.FSharp.Reflection namespace so:

open Microsoft.FSharp.Reflection

///Returns the case name of the object with union type 'ty.
let GetUnionCaseName (x:'a) = 
    match FSharpValue.GetUnionFields(x, typeof<'a>) with
    | case, _ -> case.Name  

///Returns the case names of union type 'ty.
let GetUnionCaseNames <'ty> () = 
    FSharpType.GetUnionCases(typeof<'ty>) |> Array.map (fun info -> info.Name)

// Example
type Beverage =
    | Coffee
    | Tea

let t = Tea
> val t : Beverage = Tea

GetUnionCaseName(t)
> val it : string = "Tea"

GetUnionCaseNames<Beverage>()
> val it : string array = [|"Coffee"; "Tea"|]
Daniel Asher
  • 1,493
  • 1
  • 16
  • 21
  • 1
    I came here for the ```GetUnionFields``` syntax - thanks for that. While I'm here I thought I'd point out that ```GetUnionCaseName``` could be written slightly more succinctly as: ```let GetUnionCaseName (e:'a) = ( FSharpValue.GetUnionFields(e, typeof<'a>) |> fst ).Name``` – philsquared Jan 26 '15 at 17:35
  • 1
    Notice that this is a very slow approach, You'd definitely want to cache the results in order to get some performance milage out of this – Carlo V. Dango Jan 28 '16 at 15:23
2

@DanielAsher's answer works, but to make it more elegant (and fast? because of the lack of reflection for one of the methods), I would do it this way:

type Beverage =
    | Coffee
    | Tea
    static member ToStrings() =
        Microsoft.FSharp.Reflection.FSharpType.GetUnionCases(typeof<Beverage>)
            |> Array.map (fun info -> info.Name)
    override self.ToString() =
        sprintf "%A" self

(Inspired by this and this.)

Community
  • 1
  • 1
ympostor
  • 909
  • 7
  • 16
2

This answer supplies additional information and solutions to the top answer.

I just now had a case where the top answer did not work. The problem was that the value was behind an interface, and then I would sometimes get the case name (Coffee or Tea), but mostly only the type name (Beverage). I don't understand why. I'm on .NET 5.0.

I changed the function to this, and then it worked as expected on my interfaced DU, always giving me the case name.

open FSharp.Reflection

let GetUnionCaseName (x: obj) =
    match FSharpValue.GetUnionFields(x, x.GetType()) with
    | case, _ -> case.Name

I am aware that this is similar to other answers here, but this is not a member function, and so I guess should work on any DU, whether behind interfaces or not. I haven't tested what happens if used on a non-DU type.

type IMessage = interface end

type Beverage = Coffee | Tea

type Car =
    | Tesla of model:string
    | Ford
    interface IMessage
    
type MySingleCase = MySingleCase of string
type SingleCase2 = SingleCase2 of string interface IMessage

let m1: Beverage = Coffee
let m2: IMessage = (Tesla "Model 3") :> IMessage
let m3 = MySingleCase "x"
let m4 = SingleCase2 "x" :> IMessage

printfn "%s" (GetUnionCaseName m1) // Coffee
printfn "%s" (GetUnionCaseName m2) // Tesla
printfn "%s" (GetUnionCaseName m3) // MySingleCase
printfn "%s" (GetUnionCaseName m4) // SingleCase2
Bent Tranberg
  • 3,445
  • 26
  • 35
1

I would like to propose something even more concise:

open Microsoft.FSharp.Reflection

type Coffee = { Country: string; Intensity: int }

type Beverage =
    | Tea
    | Coffee of Coffee

    member x.GetName() = 
        match FSharpValue.GetUnionFields(x, x.GetType()) with
        | (case, _) -> case.Name  

When union case is simple, GetName() may bring the same as ToString():

> let tea = Tea
val tea : Beverage = Tea

> tea.GetName()
val it : string = "Tea"

> tea.ToString()
val it : string = "Tea"

However, if union case is fancier, there will be a difference:.

> let coffee = Coffee ({ Country = "Kenya"; Intensity = 42 })
val coffee : Beverage = Coffee {Country = "Kenya"; Intensity = 42;}

> coffee.GetName()
val it : string = "Coffee"

> coffee.ToString()
val it : string = "Coffee {Country = "Kenya";        Intensity = 42;}"
psfinaki
  • 1,814
  • 15
  • 29