Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using boolean functions as pattern discriminators in F#

I tried googling this but I couldn't find a collection of words that guided me to what I am trying to do.

I am trying to solve Project Euler Problem 54, and I have this rather ridiculous function:

let evaluate hand =
    if isRoyalFlush hand then 9
    elif isStraightFlush hand then 8
    elif isFour hand then 7
    elif isFullHouse hand then 6
    elif isFlush hand then 5
    elif isStraight hand then 4
    elif isThree hand then 3
    elif isTwoPair hand then 2
    elif isPair hand then 1
    else 0

All the isSomething keywords are functions that take a string array and return a Boolean. Is there a more elegant way of doing this using pattern matching ?

This doesn't work:

match true with
| isRoyalFlush hand -> 9
| isStraightFlush hand -> 8
// .. etc

I am looking for something like this:

match hand with 
| isRoyalFlush -> 9
| isStraightFlush -> 8
// .. etc

I remember seeing something like that at one point but I can't remember where or how to find it.

like image 209
asibahi Avatar asked Sep 01 '16 03:09

asibahi


2 Answers

You want active patterns:

let (|IsRoyalFlush|_|) hand =
    if isRoyalFlush hand then Some () else None

let (|IsStraightFlush|_|) hand =
    if isStraightFlush hand then Some() else None

// etc.

match hand with
| IsRoyalFlush -> 9
| IsStraightFlush -> 8
// etc.

Or better, pull out all that common code into a simple active-pattern builder:

let toActivePattern pred x =
    if pred x then Some () else None

let (|IsRoyalFlush|_|) = isRoyalFlush |> toActivePattern
let (|IsStraightFlush|_|) = isStraightFlush |> toActivePattern
// etc.

match hand with
| IsRoyalFlush -> 9
| IsStraightFlush -> 8
// etc.

If you don't understand how that second example works, leave a comment and I'll go into more depth.

Composing active patterns together

Since active patterns are just functions, you can use standard function composition to join them together. Well, almost standard function composition. Normal function composition with the >> operator means "take the result of function 1, and use it as the input to function 2". But here, function 1 and function 2 both return Some (), but take an int; you can't pass the output of 1 into the input of 2, since they aren't compatible types. But what we want is actually to pass the same input to both functions, and combine their output.

So instead of using normal function composition, we'll define our own function that takes two active patterns, and returns Some () if both patterns match the input:

let matchesBoth pattern1 pattern2 x =
  match pattern1 x with
  | None -> None
  | Some _ -> pattern2 x

And while we're at it, let's define a custom operator so you can see how that works. This matchesBoth function works very much like the && operator, in that it will return Some () only if both patterns would return Some () for any given input x. We shouldn't overload the && operator to take a different type, so let's create a custom operator that looks like &&, but reminds us that it's combining two active patterns. If our operator looks like |&&|, that should be perfect. So let's create it:

let (|&&|) = matchesBoth

That's it! Now we can do something like:

let (|Div3|_|) n =
  if n % 3 = 0 then Some () else None

let (|Div5|_|) n =
  if n % 5 = 0 then Some () else None

let (|Div15|_|) = (|Div3|_|) |&&| (|Div5|_|)

let fizzbuzz n =
    match n with
    | Div15 -> "FizzBuzz"
    | Div5 -> "Buzz"
    | Div3 -> "Fizz"
    | _ -> string n

fizzbuzz 30  // Result: "FizzBuzz"
fizzbuzz 31  // Result: "31"

Or, for your example:

let (|IsStraightFlush|_|) = (|IsStraight|_|) |&&| (|IsFlush|_|)
like image 176
rmunn Avatar answered Nov 11 '22 06:11

rmunn


Active patterns is certainly an alternative but I sometimes use a table of functions and values:

let handScores = 
  [|
  //Which hand?       Score
    isRoyalFlush    , 9
    isStraightFlush , 8
    isFour          , 7
    isFullHouse     , 6
    isFlush         , 5
    isStraight      , 4
    isThree         , 3
    isTwoPair       , 2
    isPair          , 1
    (fun _ -> true) , 0 // Matches all hands
  |]
let handScore hand = 
  handScores 
  |> Array.pick (fun (t, s) -> if t hand then Some s else None)
like image 8
Just another metaprogrammer Avatar answered Nov 11 '22 06:11

Just another metaprogrammer