Search code examples
f#pattern-matchingdiscriminated-union

Match on discriminated union case not contents


Is it possible in F# to match a Discriminated Union based on its case rather than by case contents? For example, if I wanted to filter a list by elements that are of the case Flag, is it possible to filter as such? Currently, I am forced to have three separate functions to filter the way I desire. This is the approach I have so far:

type Option = 
 {Id : string
  Arg : string}

type Argument =
     | Flag of string
     | Option of Option
     | Unannotated of string

//This is what I'm going for, but it does not work as the "other" match case will never be matched
let LocateByCase (case:Argument) (args : Argument List) =
    args
    |> List.filter (fun x -> match x with
                             | case -> true
                             | _ -> false)

let LocateAllFlags args =
    args
    |> List.filter (fun x -> match x with
                             | Flag y -> true
                             | _ -> false)
let LocateAllOptions args =
    args
    |> List.filter (fun x -> match x with
                             | Option y -> true
                             | _ -> false)

let LocateAllUnannotated args =
    args
    |> List.filter (fun x -> match x with
                             | Unannotated y -> true
                             | _ -> false)

Am I missing some facet of the F# language that would make this much easier to deal with?


Solution

  • There is no built-in way to find out the case of a DU value. The usual approach, when faced with such requirement, is to provide appropriate functions for each case:

    type Argument =
         | Flag of string
         | Option of Option
         | Unannotated of string
        with
         static member isFlag = function Flag _ -> true | _ -> false
         static member isOption = function Option _ -> true | _ -> false
         static member isUnannotated = function Unannotated _ -> true | _ -> false
    
    let LocateByCase case args = List.filter case args
    
    let LocateAllFlags args = LocateByCase Argument.isFlag args
    

    (needless to say, the LocateByCase function is actually redundant, but I decided to keep it in to make the answer clearer)


    WARNING: DIRTY HACK BELOW

    Alternatively, you could provide the case as a quotation, and make yourself a function that will analyze that quotation, fish the case name out of it, and compare it to the given value:

    open FSharp.Quotations
    
    let isCase (case: Expr<'t -> Argument>) (value: Argument) = 
        match case with
        | Patterns.Lambda (_, Patterns.NewUnionCase(case, _)) -> case.Name = value.GetType().Name
        | _ -> false
    
    // Usage:
    isCase <@ Flag @> (Unannotated "")  // returns false
    isCase <@ Flag @> (Flag "")  // returns true
    

    Then use this function to filter:

    let LocateByCase case args = List.filter (isCase case) args
    
    let LocateAllFlags args = LocateByCase <@ Flag @> args
    

    HOWEVER, this is essentially a dirty hack. Its dirtiness and hackiness comes from the fact that, because you can't require a certain quotation shape at compile time, it will allow nonsensical programs. For example:

    isCase <@ fun() -> Flag "abc" @> (Flag "xyz")  // Returns true!
    isCase <@ fun() -> let x = "abc" in Flag x @> (Flag "xyz")  // Returns false. WTF?
    // And so on...
    

    Another gotcha may happen if a future version of the compiler decides to generate quotations slightly differently, and your code won't recognize them and report false negatives all the time.

    I would recommend avoiding messing with quotations if at all possible. It may look easy on the surface, but it's really a case of easy over simple.