Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

F# Use generic type as pattern discriminator

If there's another way to achieve what I'm trying to do below, please let me know. Suppose I have the following sample code

type FooBar = 
  | Foo
  | Bar

let foobars = [Bar;Foo;Bar]

let isFoo item  = 
  match item with
  | Foo _ -> true
  | _ -> false

foobars |> Seq.filter isFoo

I want to write a generic/higher-order version of isFoo that allows me to filter my list based on all other types of the discriminated union (Bar in this case).

Something like the following, where 'a can be either Foo or Bar

let is<'a> item  = 
  match item with
  | a _ -> true
  | _ -> false

However, this attempt yields the following error:

error FS0039: The pattern discriminator 'a' is not defined

like image 985
Johannes Rudolph Avatar asked Oct 25 '12 14:10

Johannes Rudolph


2 Answers

If you just want to filter a list, then the easiest option is to use function to write standard pattern matching:

[ Foo; Bar; Foo ]
|> List.filter (function Foo -> true | _ -> false)

If you wanted to write some more complicated generic function that checks for a case and then does something else, then the easiest option (that will work in general) is to take a predicate that returns true or false:

let is cond item  = 
  if cond item then
    true
  else
    false

// You can create a predicate using `function` syntax
is (function Foo -> true | _ -> false) <argument>

In your specific example, you have a discriminated union where none of the cases has any parameters. This is probably an unrealistic simplification, but if you only care about discriminated unions without parameters, then you can just use the cases as values and compare them:

let is case item = 
  if case = item then
    true
  else
    false

// You can just pass it 'Foo' as the first parameter to 
// `is` and use partial function application
[ Foo; Bar; Foo ]
|> List.filter (is Foo)

// In fact, you can use the built-in equality test operator
[ Foo; Bar; Foo ] |> List.filter ((=) Foo)

This last method will not work if you have more complicated discriminated union where some cases have parameters, so it is probably not very useful. For example, if you have a list of option values:

let opts = [ Some(42); None; Some(32) ]
opts |> List.filter (is Some) // ERROR - because here you give 'is' a constructor 
                              // 'Some' instead of a value that can be compared. 

You could do various tricks using Reflection (to check for cases with a specified name) and you could also use F# quotations to get a bit nicer and safer syntax, but I do not think that's worth it, because using pattern matching using function gives you quite clear code.

EDIT - Just out of curiosity, a solution that uses reflection (and is slow, not type safe and nobody should actually use it in practice unless you really know what you're doing) could look like this:

open Microsoft.FSharp.Reflection
open Microsoft.FSharp.Quotations

let is (q:Expr) value = 
  match q with
  | Patterns.Lambda(_, Patterns.NewUnionCase(case, _)) 
  | Patterns.NewUnionCase(case, _) ->
      let actualCase, _ = FSharpValue.GetUnionFields(value, value.GetType())
      actualCase = case
  | _ -> failwith "Wrong argument"

It uses quotations to identify the union case, so you can then write something like this:

type Case = Foo of int | Bar of string | Zoo

[ Foo 42; Zoo; Bar "hi"; Foo 32; Zoo ]
|> List.filter (is <@ Foo @>)
like image 71
Tomas Petricek Avatar answered Nov 23 '22 05:11

Tomas Petricek


As long as union cases accept the same set of parameters, you can pass a constructor as an argument and reconstruct DUs for comparison.

It looks more appealing when Foo and Bar have parameters:

type FooBar = Foo of int | Bar of int

let is constr item = 
    match item with
    | Foo x when item = constr x -> true
    | Bar x when item = constr x -> true
    | _ -> false

In your example, constructors have no argument. So you can write is in a simpler way:

type FooBar = Foo | Bar

let is constr item = item = constr

[Bar; Foo; Bar] |> Seq.filter (is Foo)
like image 23
pad Avatar answered Nov 23 '22 07:11

pad