Is it possible to unwrap a single-case DU without creating a function?

I have this simple code which works:

type Name = Name of string

let myName = Name "Garry"

let isAllLowerCase (text : string) = if text = text.ToLower() then true else false

let (Name name) = myName

let result = name |> isAllLowerCase

Is it possible to unwrap the name from the DU without using a function, such as:

type Name = Name of string

let myName = Name "Garry"

let isAllLowerCase (text : string) = if text = text.ToLower() then true else false

let result = (Name myName) |> isAllLowerCase

ā€¦which I know doesnā€™t compile, but hopefully you get the picture.

I know I can do this:

type Name = Name of string

let myName = Name "Garry"

let isAllLowerCase (text : string) = if text = text.ToLower() then true else false

let result = myName |> (fun (Name name) -> name) |> isAllLowerCase

ā€¦but that seems worse then having the named function.

And I know I can do this:

type Name = Name of string with
    member this.Value =
        let (Name name) = this
        name

let myName = Name "Garry"

let isAllLowerCase (text : string) = if text = text.ToLower() then true else false

let result = myName.Value |> isAllLowerCase

ā€¦but that seems a little ā€˜clunkyā€™.

And I know I can change the Name definition to:

type Name = Name of string with
    member this.Value =
        let (Name name) = this in name

ā€¦but thatā€™s not much better.

Is there a better way?

Iā€™m not aware of a much better way. What Iā€™ve done in the past is indeed to use members, but with an interface:

type Wrapped<'x> = abstract member Value : 'x

let unwrap<'x> (wrapped:#Wrapped<'x>) = wrapped.Value

then your code would be

type Name = Name of string with 
  interface Wrapped<string> with
    member this.Value = let (Name name) = this in name

...
let result = unwrap name |> isAllLowerCase
// alternatively, if you want more of an annotation
let result = unwrap<Name> name |> isAllLowerCase

still a little clunky because of working with interfaces & members, but what I like is that the unwrap function can still work with function composition, pipelines, partial application and all that without making a lambda, so all the clunkiness is in having to take those 2 lines in the type definition of your Name type to add the interface.

1 Like

It depends a bit on your situation, but there are a few options. If you ā€œownā€ the consuming function, you can simply do the unwrapping in the function declaration:

let isAllLowerCase (Name text) = 
  text |> String.forall Char.IsLower

However, for a more general solution, I would recommend defining an explicit conversion operator:

type Name = Name of string
  with static member op_Explicit(Name name) = name

// ... elsewhere ...

myName |> string |> isAllLowerCase

Further, if you donā€™t plan to attach any extra business functionality to the ā€œwrappedā€ string, you may want to consider using something other than a single-case discriminated union. I usually use something like UMX (GitHub - fsprojects/FSharp.UMX: F# units of measure for primitive non-numeric types) when I donā€™t want a custom type but do want to distinguish between strings.

3 Likes

Thanks for the information ntwilson.

Iā€™ve not had the chance to get into using interfaces in F# yet but that works very nicely.

P.S. Thereā€™s a superfluous ā€˜#ā€™ in your code.

Thanks for the information pblasucci.

Explicit conversion operators are something that I will probably need to look up later but they look interesting.

Iā€™ve also bookmarked Fsharp.UMX for further reading at some point.

Thereā€™s so much stuff to learn that itā€™s difficult to know what to learn next.