Using SRTP-Active Patterns in F#

A few days ago, we embarked on the process of tidying up a particularly complex piece of code. It involved moving typical frontend code to the backend, as it was becoming too cumbersome and we felt more secure writing it in F# in the backend. (more on that in another blog post)

After much of it was rewritten, we ran into a pattern matching construct that threatened to contain a lot of code duplication. To increase readability, we wanted to use an Active Pattern. The problem was that the match input was two values that came from different, but very similar types. The solution: Active Patterns utilizing SRTP.

For those who are mostly lost right now:

What are Active Patterns and how do they work?

Let’s assume we want to create a very simple text parser that should determine if a single text element is a word or a number. We would like to model the type for this as a discriminated union: 

type Element = 
    | Word of string
    | Number of int

One could write the parsing like this:

let elements =
    text
    |> String.split " "
    |> Array.map (fun e ->
        match e |> Int32.TryParse with
        | Some i -> Number i // a bit of pseudo code here...
        | None -> Word e)

That works, but let’s say we also want to parse spelled out numbers (e.g. one to ten) as numbers. Additionally, we want to maintain the readability of the match (not make it more complex). That’s where an Active Pattern can help:

let (|IsInt|_|) str = // that's the active pattern
    match Int32.TryParse(str:string), str with
    | (true,int), _ -> Some(int)
    | _, "One" -> Some 1
    | _,"Two" -> Some 2
    // and so on
    | _, _ -> None

let elements' =
    text
    |> String.split " "
    |> Array.map (fun e ->
        match e with
        | IsInt i -> Number i
        | str -> Word str)

Now that we’ve cleared up what Active Patterns are and how to use them, let’s move on to SRTP:

What is SRTP?

SRTP stands for Statically Resolved Type Parameters and helps us wherever we try to describe what we expect as input types without specifying the type itself: we describe our requirements for the type instead of naming the type. Many will now argue “but there are interfaces for that!” Yes and no: on the one hand, it is true that interfaces are exactly intended for this use case, BUT interfaces are much less flexible to use. The most obvious weakness of interfaces is that they must be specified during type declaration. What if you are not the owner of that type? (wrappers are cumbersome)! Also: What if the reason for the desired type description is very local? Do I actually want to declare and implement an interface in that case? In my opinion, clearly no. 

How do you use SRTP now? Firstly, you would naturally study the Microsoft Learn resource and imitate the code described there. Unfortunately, the description is relatively obsolete today: there is a table showing the difference between the generic 'a syntax and the supposed SRTP ^a syntax. The ^a syntax described there still works, but the 'a syntax is far from exclusive to run-time detection as Microsoft describes it in the table. Additionally, the Microsoft example only applies to static members. This is relatively confusing, as the “S” in SRTP also stands for “statically”, and could thus be interpreted that SRTP is solely about the description of these static members. This is not true. 
For this reason, I take the liberty of adding this example code for a SRTP to complement the previously linked resource. A typical case is simply to check if the passed type is a record that has a field with the desired type:

type Person = { PersonalId: int }

// this function uses SRTP:
let getIdAsString (a: 'a when 'a: (member PersonalId: int)) =
    $"{a.PersonalId}"

let person = { PersonalId = 20 }
let personalIdString = getIdAsString person

This could now be extended endlessly with additional ands and ors, but the idea should be clear. Any value that contains a PersonalId of type int can now be passed to this function, no matter where this type comes from. Super convenient, as we can now access this PersonalId in the body of the function.

So, this leads us finally to our specific use case:

Using SRTP in an Active Pattern

Since we wanted to write an active pattern that would work for types that are similar (i.e. types with certain, identical fields) and active patterns are just a special form of functions, we can now combine the two concepts mentioned above and have our (subjectively perceived) easy-to-read solution:

let inline (|SameAs|OtherThan|None|)
    (duty: 'd option
        when 'd: (member ServiceId: CommonServiceId)
        and 'd: (member ServicePlanId: Guid<ServicePlanId>))
    =
    match duty with
    | Some d when d.ServiceId = selectedServiceId && d.ServicePlanId = servicePlanId -> SameAs d
    | Some d -> OtherThan d
    | None -> None
    
let newTempDuties =
    match dutyInCell, tempDutyInCell with // notice: dutyInCell and tempDutyInCell don't share the exact same type!
    | None, None -> [ addNewTempDuty () ]
    | SameAs plannedDuty, _ -> [ remove plannedDuty ]
    | OtherThan plannedDuty, _ -> [ addNewTempDuty (); remove plannedDuty ]
    | _, SameAs _temp -> [] // _temp gets removed when it's not in the list
    | _, OtherThan _temp -> [ addNewTempDuty () ]

It’s not necessary to completely understand the previous code. The important thing is that dutyInCell and tempDutyInCell do not share the same type, but only contain the fields described in the SRTP of the Active Pattern. Additionally, the two values can each take on one of the states the Active Pattern has to offer. This results in a combination of the two states in the match, which then leads to the consequence described on the right side (behind the arrow). I really like the resulting code.

That’s it for today. I hope I was able to demonstrate that F# code is anything but intimidating and can often significantly simplify the readability of code.

About the author

Domenic Helfenstein

3 comments

  • Isn’t there a mistake in your text? “between the generic ‘a syntax and the supposed SRTP ^a syntax” must due syntax not be exactly the other way around?

  • I’m not 100% certain if I understand your comment correctly:
    The MS site says the 'a syntax stands for the standard generic feature, whereas ^a is reserved for the statically resolved feature.
    I disagree with this and show that 'a can also be used for SRTP. (I don’t know when they changed it, but they didn’t bother to update the docs).

Recent Posts