Skip to content

Instantly share code, notes, and snippets.

@isaacabraham
Last active December 22, 2023 18:11
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save isaacabraham/04fc03620a9d1e5160ea9c28edbc8bdd to your computer and use it in GitHub Desktop.
Save isaacabraham/04fc03620a9d1e5160ea9c28edbc8bdd to your computer and use it in GitHub Desktop.
Active Patterns
open System
let s = "Card 1: 41 48 83 86 17 | 83 86 6 31 17 9 48 53"
// challenge - split above into values we can reason about e.g.
type Card = {
Id : int // 1
Winning : int list // 41 48 83 86 17
Ticket : int list // 83 86 6 31 17 9 48 53
}
// this based on day four of Advent of Code 2023 https://adventofcode.com/2023/day/4
// Active pattern to split a string on a given character and return the resultant parts as a list.
let (|Split|) (on: char) (s: string) =
s.Split(on, StringSplitOptions.RemoveEmptyEntries ||| StringSplitOptions.TrimEntries)
|> Array.toList
// 1. s split on : into a list bound to the symbol 'elements'.
match s with
| Split ':' elements -> () // ["Card 1"; "41 48 83 86 17 | 83 86 6 31 17 9 48 53"]
// 2. Expand 'elements' => must be a list of EXACTLY two items, bound to symbols 'a' and 'b'.
match s with
| Split ':' [ a; b ] -> () // a = "Card 1", b = "41 48 83 86 17 | 83 86 6 31 17 9 48 53"
| _ -> failwith "bad input"
// 3. Expand 'a' => Split on ' ' into a list that must have exactly two items, first element must be absolute value "Card".
match s with
| Split ':' [ Split ' ' [ "Card"; n ]; b ] -> () // n = "1"
| _ -> failwith "bad input"
// Active pattern to safely convert string to int
let (|Int|_|) (s: string) =
match Int32.TryParse s with
| true, n -> Some(Int n)
| false, _ -> None
// 4. Expand 'n' => n must be an Integer bound to the symbol 'n'
match s with
| Split ':' [ Split ' ' [ "Card"; Int n ]; b ] -> () // (n = 1)
| _ -> failwith "bad input"
// 5. Apply same technique to 'b' => Split on '|' into a list of EXACTLY two items which are bound to symbols 'a' and 'b'.
match s with
| Split ':' [ Split ' ' [ "Card"; Int n ]; Split '|' [ a; b ] ] -> () // a = "41 48 83 86 17", b = "83 86 6 31 17 9 48 53"
| _ -> failwith "bad input"
// 6. Expand 'a' => Split on ' ' and bind the list into the symbol 'winning'
match s with
| Split ':' [ Split ' ' [ "Card"; Int n ]; Split '|' [ Split ' ' winning; b ] ] -> () // winning = ["41"; "48"; "83"; "86"; "17"]
| _ -> failwith "bad input"
// 7. Expand 'b' => do the same as step 6 again and bind the list into the symbol 'ticket'
match s with
| Split ':' [ Split ' ' [ "Card"; Int n ]; Split '|' [ Split ' ' winning; Split ' ' ticket ] ] -> () // ticket = ["83"; "86"; "6"; "31"; "17"; "9"; "48"; "53"]
| _ -> failwith "bad input"
// 8. Final result
match s with
| Split ':' [ Split ' ' [ "Card"; Int n ]; Split '|' [ Split ' ' winning; Split ' ' ticket ] ] ->
{ Id = n; Winning = winning |> List.map int; Ticket = ticket |> List.map int }
| _ -> failwith "bad input"
// Equivalent "imperative" code (no pattern matching):
let elements = s.Split ':'
let cardNumber = elements[0].Split ' '
if cardNumber[0] = "Card" && cardNumber.Length = 2 then
let parsed, number = Int32.TryParse cardNumber[1]
if parsed then
let tickets = elements[1].Split '|'
if tickets.Length = 2 then
let winningNumbers =
tickets[0].Split(' ', StringSplitOptions.RemoveEmptyEntries) |> Array.map int
let ticket =
tickets[1].Split(' ', StringSplitOptions.RemoveEmptyEntries) |> Array.map int
{
Id = number
Winning = winningNumbers |> List.ofArray
Ticket = ticket |> List.ofArray
}
else
failwith "bad input"
else
failwith "bad input"
else
failwith "bad input"
@mattgallagher92
Copy link

mattgallagher92 commented Dec 15, 2023

For easy comparison with the "imperative" way, here are the key bits of the active pattern approach:

let (|Split|) (on: char) (s: string) =
    s.Split(on, StringSplitOptions.RemoveEmptyEntries ||| StringSplitOptions.TrimEntries)
    |> Array.toList

let (|Int|_|) (s: string) =
    match Int32.TryParse s with
    | true, n -> Some(Int n)
    | false, _ -> None

match s with
| Split ':' [ Split ' ' [ "Card"; Int n ]; Split '|' [ Split ' ' winning; Split ' ' ticket ] ] ->
    { Id = n; Winning = winning |> List.map int; Ticket = ticket |> List.map int }
| _ -> failwith "bad input"

Very cool, thanks Isaac!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment