11

I have several verbal expressions that I've packaged into one function:

open FsVerbalExpressions
open FsVerbalExpressions.VerbalExpression
open System.Text.RegularExpressions
open System

let createOrVerbExFromList (verbExList: VerbEx list) = 
    let orVerbEx = 
        verbExList
        |> List.reduce (fun acc thing -> verbExOrVerbEx RegexOptions.IgnoreCase acc thing) //simpleVerbEx

    orVerbEx 

let k12VerbEx =     
    let kTo12 =  ["SCHOOL"; "DIST"; "SD"; "HS"; "BD OF ED"]
    kTo12
    |> List.map (fun word -> VerbEx(word))
    |> createOrVerbExFromList

let twoYearCollegeVerbEx = 
    VerbEx("2 Year College")

let universityVerbEx = 
    VerbEx("UNIV")

let privateSchoolVerbEx = 
    VerbEx("ACAD")

//Here there be dragons:
let newInst (x: string) =
    match (isMatch x k12VerbEx) with 
    | true -> "K - 12"
    | _ -> match (isMatch x twoYearCollegeVerbEx) with
            | true -> "2 Year College"
            | _ -> match (isMatch x universityVerbEx) with
                    | true -> "University" 
                    | _ -> match (isMatch x privateSchoolVerbEx) with
                            | true -> "Private / Charter School"
                            | _ -> "Other"

I'd like to rewrite the newInst function so that it's no longer the "pyramid of doom. My question is how can I get rid of the pyramid of doom? Can I get rid of it? I have the suspicion that it will be some sort of async workflow or other computational expression, but those are all very new to me.

Steven
  • 3,238
  • 21
  • 50

2 Answers2

20

If you are only matching against booleans, then if ... elif is sufficient:

let newInst (x: string) =
    if isMatch x k12VerbEx then
        "K - 12"
    elif isMatch x twoYearCollegeVerbEx then
        "2 Year College"
    elif isMatch x universityVerbEx then
        "University"
    elif isMatch x privateSchoolVerbEx then
        "Private / Charter School"
    else
        "Other"

A more flexible possibility would be to create an active pattern:

let (|IsMatch|_|) f x =
    if isMatch x f then Some () else None

let newInst (x: string) =
    match x with
    | IsMatch k12VerbEx -> "K - 12"
    | IsMatch twoYearCollegeVerbEx -> "2 Year College"
    | IsMatch universityVerbEx -> "University"
    | IsMatch privateSchoolVerbEx -> "Private / Charter School"
    | _ -> "Other"
Tarmil
  • 11,177
  • 30
  • 35
  • This was a great introduction to active patterns. Can you tell me the significance of the additional `|_` in the active pattern? – Steven Oct 04 '16 at 19:38
  • 2
    The `|_` indicates a partial active pattern, meaning that it may not match all values (which is why it returns an option). [See the documentation](http://stackoverflow.com/documentation/f%23/962/active-patterns/23523/complete-and-partial-active-patterns#t=201610041942173049847) about the difference between total and partial APs. In your case you might actually want to write a total AP, but its implementation itself will be subject to the same pyramid of doom :) – Tarmil Oct 04 '16 at 19:45
3

When there is sequential repetition of exactly the same form of code, I prefer to use a data-driven approach instead:

let verbExStrings =
    [
        (k12VerbEx, "K - 12")
        (twoYearCollegeVerbEx, "2 Year College")
        (universityVerbEx, "University")
        (privateSchoolVerbEx, "Private / Charter School")
    ]

let newInst x =
    verbExStrings
    |> List.tryPick (fun (verbEx, string) -> if isMatch x verbEx then Some string else None)
    |> function Some x -> x | _ -> "Other"

An advantage of this approach is that the raw data (verbExStrings) can come in handy in other places and is not tied to your code implementation.

TheQuickBrownFox
  • 10,544
  • 1
  • 22
  • 35