Search code examples
f#namedtuplediscriminated-union

Unwrap F# single-case discriminated union tuple type


We can unwrap type like type Address = Address of string using unwrapping function like

let unwrapAddress (Address a) = a
let addr = Address "sdf"
let str = unwrapAddress addr

so str will be of type string, but if there is type like this approach willn't work:

type Composite = Composite of integer:int * someStr:string
let unwrap (Composite c) = c

will produce error

let unwrap (Composite c) = c;;
------------^^^^^^^^^^^
error FS0019: This constructor is applied to 1 argument(s) but expects 2

Can I somehow unwrap composite types to a simple tuple?


Solution

  • These all work for me. It's your matching syntax, that most often you'll find used with match statements, but it's on the l.h.s. of an assignment. Possibly, this makes the most sense, initially, for tuples, but you can use this with any structure.

    let (a,b) = (1,2)
    
    let (x,_) = (4,5)
    

    Two other interesting things to try:

    let (head::tail) = [1;2;3;4]
    

    FSI responds warning FS0025: Incomplete pattern matches on this expression. For example, the value '[]' may indicate a case not covered by the pattern(s).

    "That's true," you reason aloud. "I should express it as a match and include an empty list as a possibility". It's better to bubble these kinds of warnings into fully bonafide errors (see: warn as error e.g. --warnaserror+:25). Don't ignore them. Resolve them through habit or the compiler enforced method. There's zero ambiguity for the single case, so code-on.

    More useful + interesting is the match syntax on the l.h.s. of a function assignment. This is pretty cool. For pithy functions, you can unpack the stuff inside, and then do an operation on the internals in one step.

    let f (Composite(x,y)) = sprintf "Composite(%i,%s)" x y
    
    f (Composite(1,"one"))
    
    > val it : string = "Composite(1,one)"
    

    About your code:

    type Address = Address of string //using unwrapping function like
    
    let unwrapAddress (Address a) = a
    let addr = Address "sdf"
    let str = unwrapAddress addr
    
    type Composite = Composite of integer:int * someStr:string
    let unwrap (Composite(c,_)) = c
    let cval = Composite(1,"blah")
    unwrap cval
    

    Workaround:

    let xy = Composite(1,"abc") |> function (Composite(x,y))->(x,y)
    

    ... but the nicer way, assuming you want to keep the named elements of your single case DU would be...

    let (|Composite|) = function | Composite(x,y)->(x,y)
    
    let unwrap (Composite(x)) = x
    
    let unwrap2 (Composite(x,y)) = (x,y)
    

    ... not strictly decomposing through a single case DU, but decomposing through a single-case Active Pattern

    lastly, you could attach a method to the Composite structure...

    module Composite = 
      let unwrap = function | Composite(x,y)->(x,y)
    

    One of the best discussions about using this technique is over here

    Also, check out the signature that unwrap gives us: a function that takes a Composite (in italics), and returns an int (in bold)

    Signature -- val unwrap : Composite -> int