4 min read

Practical Guide to F# Active Patterns: Simplify Pattern Matching in Your Code

F# contains a pattern-matching feature called "active patterns" that allows the pattern to be parsed or detected dynamically. From the caller's perspective, the matching and output are integrated into a single step.
Practical Guide to F# Active Patterns: Simplify Pattern Matching in Your Code
Photo by Marcel Strauß / Unsplash

Introduction

The first time I saw an active pattern inside an F# project, I was scratching my head (I can't figure it out). However, the longer I stared at it, I realized it was a function, but a special kind of function.

Let's try to discuss it here.

It is also recommended to know what pattern matching is in F#. I have a previous article about pattern matching; you can click here.

Okay, let's get started.

What is an Active Pattern?

In F#, an active pattern is a language feature that allows developers to define custom pattern-matching operations.

Active patterns enable developers to extract data from complex or custom types, making our code expressive and maintainable.

Syntax of Active Pattern

let (|PatternName|) (param: paramType) = 
    // Pattern matching logic to extract data from paramType
    // Optionally, return Some result or None

Let's try to see the breakdown of the syntax.

  • The |PatternName|is the name of the active pattern you have defined. As you notice, it should start and end with a vertical bar |.
  •  The param is the type used for pattern matching.
  • The paramType is the type that the active pattern takes as input.

Active Pattern Example

Let's try to see a simple example.

let (|Morning|Afternoon|Evening|Night|) (hour:int) =
    match hour with 
    | h when h >= 5 && h < 12 -> Morning
    | h when h >= 12 && h < 17 -> Afternoon
    | h when h >= 17 && h < 21 -> Evening
    | h -> Night

The example code above shows an active pattern that checks whether the hour of the day is morning, afternoon, evening, or night.

Let's create another function that will use the |Morning|Afternoon|Evening|Night| function.

let showTimeCategory (hour: int) : string =
    match hour with 
    | Morning -> sprintf "%d:00 is in the morning." hour
    | Afternoon -> sprintf "%d:00 is in the afternoon." hour
    | Evening -> sprintf "%d:00 is in the evening." hour
    | Night -> sprintf "%d:00 is at night." hour

Again, the code example above shows that it will return a string indicating the day's hour and shows if it is morning, afternoon, evening, or night.

Let's see it in full action with the unit test.

namespace FsharpActivePatternTest

open System
open Xunit

module SampleActivePattern  = 
    let (|Morning|Afternoon|Evening|Night|) (hour:int) =
        match hour with 
            | h when h >= 5 && h < 12 -> Morning
            | h when h >= 12 && h < 17 -> Afternoon
            | h when h >= 17 && h < 21 -> Evening
            | h -> Night
    
    let showTimeCategory (hour: int) : string =
        match hour with 
        | Morning -> sprintf "%d:00 is in the morning." hour
        | Afternoon -> sprintf "%d:00 is in the afternoon." hour
        | Evening -> sprintf "%d:00 is in the evening." hour
        | Night -> sprintf "%d:00 is at night." hour

module TestActivePattern = 
    [<Fact>]
    let ``Test showTimeCategory Morning``() = 
        let time = new TimeOnly(6,0)
        let result = SampleActivePattern.showTimeCategory(time.Hour)
        Assert.True("morning" |> result.Contains)

    [<Fact>]
    let ``Test showTimeCategory Afternoon`` () = 
        let time = new TimeOnly(13,0)
        let result = SampleActivePattern.showTimeCategory(time.Hour)
        Assert.True("afternoon" |> result.Contains)

    [<Fact>]
    let ``Test showTimeCategory Evening`` () = 
        let time = new TimeOnly(18,0)
        let result = SampleActivePattern.showTimeCategory(time.Hour)
        Assert.True("evening" |> result.Contains)

    [<Fact>]
    let ``Test showTimeCategory Night`` () =
        let time = new TimeOnly(22,0)
        let result = SampleActivePattern.showTimeCategory(time.Hour)
        Assert.True("night" |> result.Contains)
        

Types of Active Pattern

There are two types of active patterns: the active pattern and the partial active pattern.

Let's try to discuss them one by one.

Active Pattern

  • It is used mainly on direct match expression to deconstruct data and perform pattern matching.
  • Complete functions that provide complete pattern matching and use the standard syntax let (|PatterName|) (param:paramType) = ... format.

Partial Active Pattern

  • As the name suggests, these partial functions provide custom pattern matching for a specific data type.
  • It uses a wildcard character (_ ) to define a partial active pattern and use this syntax let (|PatternName|_|) (param:paramType) = ... format.

Partial Active Pattern Example

Let's try to see another example.

let (|FirstRegexMatchGroup|_|) (input:string , pattern:string):Option<string>  =
    let matched = Regex.Match(input,pattern)
    if(matched.Success) then  Some matched.Groups[0].Value else None

The code sample above will let you set the input string and the pattern for your regular expression. Once there's a match, it will get the first group of that regular expression.

Now, let's create another function to check if the input is a valid email.

let IsEmailValid (str:string) = 
	match str , @"^[\w\-\.]+@([\w-]+\.)+[\w-]{2,}$" with 
		| FirstRegexMatchGroup  host -> 
				sprintf "Your is email is valid. You may now proceed using this email: %s"  host
		| _ -> sprintf "Not a valid email"

Easy to understand, right?

Let's create a unit test for this function, see the whole code, and see how it works under the hood.

module SamplePartialActivePattern = 
    let (|FirstRegexMatchGroup|_|) (input:string , pattern:string):Option<string>  =
            let matched = Regex.Match(input,pattern)
            if(matched.Success) then  Some matched.Groups[0].Value else None
                

    let IsEmailValid (str:string) = 
        match str , @"^[\w\-\.]+@([\w-]+\.)+[\w-]{2,}$" with 
            | FirstRegexMatchGroup  host -> 
                    sprintf "Your is email is valid. You may now proceed using this email: %s"  host
            | _ -> sprintf "Not a valid email"
            
module TestPartialActivePattern  = 
    [<Fact>]
    let ``Test Input Valid Email`` () = 
        let url = "jin.necesario@uap.asia"
        let result = url |> SamplePartialActivePattern.IsEmailValid 
        Assert.True("Your is email is valid" |> result.Contains)

    [<Fact>]
    let ``Test Input InValid Email`` () = 
        let url = "hello world"
        let result = url |> SamplePartialActivePattern.IsEmailValid 
        Assert.True("Not a valid email" |> result.Contains)

How's that? Simple right?

Hopefully, you have enjoyed the examples.

Summary

In this post, we have started discussing F#'s active pattern and show a simple example of how to use it for your projects.

Moreover, we have shown another type of active pattern, the partial active pattern, and of course, we have shown examples. Lastly, we have also seen their differences in discussing the types of active patterns.

Stay tuned for more. Until next time, happy programming and happy cloud computing!

Please don't forget to subscribe, bookmark, like, and comment. Cheers! and Thank you!

References