Using boolean functions as pattern discriminators in F#
Asked Answered
K

2

9

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.

Kiss answered 1/9, 2016 at 3:2 Comment(3)
Active patterns is what you wantProm
The active pattern approach looks slightly nicer but requires lots of boilerplate above it. I prefer the answer from @FuleSnabel which just uses higher order functions and data. But actually, I think your code is just fine as it is and perfectly understandable. Adding some column alignment might make it less likely to be accidentally broken in future.Cycad
I am also of the opinion that there is nothing especially wrong with the code posted by OP.Vituline
E
11

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|_|)
Expectancy answered 1/9, 2016 at 5:0 Comment(3)
Thanks, I suspected as much. Somewhat tangent question, though: I tried rewriting my predicates as Active Patterns instead (to return Some and None) but then I couldn't figure out if it is possible to combine two Active Patterns together. Is such a thing possible without hacky code ?Kiss
In other words, is there another way of doing this : let (|IsStraightFlush|_|) hand = if ((|IsFlush|_|) hand |> Option.isSome) && ((|IsStraight|_|) hand |> Option.isSome) then Some () else None ?Kiss
@Kiss - I've edited my answer to answer your second question about combining two active patterns.Expectancy
V
8

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)
Vituline answered 1/9, 2016 at 6:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.