Active Patterns

Bob
Bob in F#
module Bob

let (|Silence|_|) (phrase: string): unit option =
    if System.String.IsNullOrWhiteSpace(phrase) then Some () else None

let (|Yell|_|) (phrase: string): unit option =
    if phrase <> phrase.ToLower() && phrase = phrase.ToUpper() then Some () else None

let (|Question|_|) (phrase: string): unit option =
    if phrase.TrimEnd().EndsWith("?") then Some () else None

let response (phrase: string): string =
    match phrase with
    | Silence         -> "Fine. Be that way!"
    | Yell & Question -> "Calm down, I know what I'm doing!"
    | Yell            -> "Whoa, chill out!"
    | Question        -> "Sure."
    | _               -> "Whatever."

What are Active Patterns?

Active patterns are used in pattern matching and can be used to categorize input and/or extract data from input.

There are two types of active patterns:

  • Regular active patterns: these patterns will match any input
  • Partial active patterns: these pattern will match some inputs, but not all

If we apply this to the exercise, we see that we have three patterns:

  • The phrase is a question
  • The phrase is a yell
  • The phrase is silence

All three of these patterns must be partial, because they only match on some inputs, not all.

Handling the different responses

We start out with a function named response that takes a string as its sole parameter and returns a string:

let response (phrase: string): string

Response for silence

If the input is an empty string (signifying silence), the response should be "Fine. Be that way!". Let's define a Silence partial active pattern that takes the phrase as its parameter:

let (|Silence|_|) (phrase: string): unit option =

We are returning an unit option type, which signifies two things:

  • The option part indicates if the input matched the pattern (Some means it did, None means it didn't)
  • Te unit part indicates that the pattern does extract/return any information from the input; think of this pattern as being a boolean pattern (either is matches, or it doesn't)

We then need to determine if the input phrase is an empty string and return Some () if it is, and None if it isn't. We can check for an empty string via the built-in String.IsNullOrWhiteSpace() method:

if System.String.IsNullOrWhiteSpace(phrase) then Some () else None
Note

We opted for using System.String, but another option would be to open the System namespace and then we could omit the System. prefix for the String.IsNullOrWhiteSpace call:

open System

let isEmpty = String.IsNullOrWhiteSpace(phrase)

If you were to use multiple types from the System namespace, we'd recommend using the above approach where the namespace is explicitly opened.

Now that we can determine whether a phrase is empty, we can use this pattern in the response function:

match phrase with
| Silence -> "Fine. Be that way!"

Nice! Let's move on to the next type of phrase.

Response for yell

If the input's letters are all in uppercase, and there is at least one letter, then the phrase is considered to be a yell. We can check if calling ToUpper() on the phrase doesn't introduce any changes, which means that every letter was already in uppercase. However, it could be that there weren't any letters, so to handle that we can use ToLower(), and see if that is not equal to the phrase (which means that there were uppercase letters).

The active pattern looks like this:

let (|Yell|_|) (phrase: string): unit option =
    if phrase <> phrase.ToLower() && phrase = phrase.ToUpper() then Some () else None

The correct response can be returned via:

match phrase with
| Yell -> "Whoa, chill out!"

Response for question

If the input ends with a question mark, it is considered to be a question. Let's define a Question active pattern and use the EndsWith() method to check for ending with a question mark:

let (|Question|_|) (phrase: string): unit option =
    if phrase.EndsWith("?") then Some () else None

This doesn't pass all the tests though, as we need to ignore any trailing white space. The fix for that is easy: first remove the trailing white space using TrimEnd():

if phrase.TrimEnd().EndsWith("?") then Some () else None

The correct response can then be returned via:

match phrase with
| Question -> "Sure."

Response for yelled question

Due to the fact that we have defined patterns for a yell (Yell) and question (Question), we can use the & keyword to check for two patterns matching at the same time:

match phrase with
| Yell & Question -> "Calm down, I know what I'm doing!"

Note that we need to check this before checking of the yell or question responses, as otherwise this will never run.

Default response

Finally, we'll need to return the default response, which we can do via:

match phrase with
| _ -> "Whatever."

Putting it all together

Putting our active patterns together gives us the following code:

match phrase with
| Silence -> "Fine. Be that way!"
| Yell & Question -> "Calm down, I know what I'm doing!"
| Yell -> "Whoa, chill out!"
| Question-> "Sure."
| _  -> "Whatever."

Alignment

While definitely not needed, aligning the code vertically could help with readability:

match phrase with
| Silence         -> "Fine. Be that way!"
| Yell & Question -> "Calm down, I know what I'm doing!"
| Yell            -> "Whoa, chill out!"
| Question        -> "Sure."
| _               -> "Whatever."
Note

A downside of vertical alignment is that changes to the code require more work, as you'll need to ensure everything is still aligned. For this particular case, it isn't really an issue, as the spec is fixed and the code is thus unlikely to change.

Final code

And with that, we have an implementation of the response function that passes all the tests:

let response (phrase: string): string =
    match phrase with
    | Silence         -> "Fine. Be that way!"
    | Yell & Question -> "Calm down, I know what I'm doing!"
    | Yell            -> "Whoa, chill out!"
    | Question        -> "Sure."
    | _               -> "Whatever."
30th Oct 2024 · Found it useful?