Skip to content

Instantly share code, notes, and snippets.

@michaeloyer
Last active February 29, 2024 19:52
Show Gist options
  • Star 9 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save michaeloyer/615543e33713033dc1c4e01c98dd9f2a to your computer and use it in GitHub Desktop.
Save michaeloyer/615543e33713033dc1c4e01c98dd9f2a to your computer and use it in GitHub Desktop.
F# SRTP Example
// SRTP: Statically Resolved Type Parameters
// https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/generics/statically-resolved-type-parameters
// SRTP Allows for pulling members out of types that where the member is named and typed the same
// In this example SRTP will be used to pull out the 'First: string' and 'Last: string' members
// from different types
// One example of SRTP in the F# Base Class Library is the (+) operator.
// You'll see that it has this type signature:
(*
val inline ( + ):
x: ^T1 (requires static member ( + ) ) ->
y: ^T2 (requires static member ( + ) )
-> ^T3
*)
// Notice the " ^ " symbol to define the generic class instead of " ' "
// Members can come from either record or class definitions
type PersonRecord = {
First: string
Last: string
}
// Even if the other type has extra members such as the 'Middle" member in this case
type PersonClass(first: string, middle: string, last: string) =
member val First = first
member val Middle = middle
member val Last = last
let personRecord = { First = "Person"; Last = "Record"}
let personClass = PersonClass("Person", "Of", "Class")
// The person parameter has the type ^Person.
// The definition of printPersonRegularParameter looks like this:
(*
val inline printPersonRegularParameter:
person: ^Person (requires member First and member Last )
-> unit
*)
// Notice that in the (+) signature the 'static member' was required, but our ^Person type
// Just needs a regular 'member'
let inline printPersonRegularParameter person =
// Notice the value is being pulled out of person with pattern matching
let first = (^Person : (member First: string) person)
let last = (^Person : (member Last: string) person)
printfn $"Person is: {first} {last}"
printPersonRegularParameter personRecord
printPersonRegularParameter personClass
// Because our 'PersonRecord' and 'PersonClass' are both going to be used in printPersonRegularParameter
// We also need to apply the 'inline' keyword to the function
// This will create two versions of our function in the compiled code,
// one that takes the 'PersonRecord', and one that takes the 'PersonClass'.
// Without 'inline' this would create one function that takes in either the the
// 'PersonRecord' or 'PersonClass' type (which ever is used in the function first),
// and won't compile when we try to use the other type
(*
ex.
let printPersonRegularParameter person = ... (srtp definition from above, just without inline in the signature) ...
printPersonRegularParameter personRecord // Compiler warning:
// This construct causes code to be less generic
// than indicated by the type annotations. The type variable
// 'Person has been constrained to be type 'PersonRecord'.
printPersonRegularParameter personClass // Compiler error:
// This expression was expected to have type
// 'PersonRecord'
// but here has type
// 'PersonClass'
*)
// Because pulling values is done with pattern matching we can create
// an active pattern to tidy up the pattern matching
let inline (|FirstName|) o =
// ^FirstName could just as easily be ^T or ^a as long as it's using the '^' symbol
(^FirstName: (member First: string) o)
let inline (|LastName|) o =
(^LastName: (member Last: string) o)
(* Active Patterns can be composed together with the '&' symbol
This is still looking for a single parameter defined as
having the First:string and Last:string members. *)
// Same signature as printPersonRegularParameter
let inline printPersonActivePatterns (FirstName first & LastName last) =
printfn $"Person is: {first} {last}"
printPersonActivePatterns personRecord
printPersonActivePatterns personClass
// And we could define another active pattern that combines our previous active patterns
// We'll just put return first and last in a tuple
let inline (|Person|) (FirstName first & LastName last) = first, last
// Same signature as printPersonRegularParameter and printPersonActivePatterns
// We can pull the tuple apart with further pattern matching
let inline printPersonCombinedActivePattern (Person (first, last)) =
printfn $"Person is: {first} {last}"
printPersonCombinedActivePattern personRecord
printPersonCombinedActivePattern personClass
//F# 7 No longer requires ^ character in the definition, the compiler figures it out with the `inline` keyword and member constraints
let inline printPersonGeneric<'Person
when 'Person:(member First:string)
and 'Person:(member Last:string)
> (person:'Person) =
printfn $"Person is: {person.First} {person.Last}"
printPersonGeneric personRecord
printPersonGeneric personClass
// F# 7 Grouped Member Constraint
type PersonMembers<'T
when 'T:(member First:string)
and 'T:(member Last:string)> = 'T
// Here the PersonMembers acts as a wrapper so that we can more easily define generic 'Person type,
// with all of those members necessary for a "Person" type
let inline printPersonGenericGroup<'Person when PersonMembers<'Person>> (person:'Person) =
printfn $"Person is: {person.First} {person.Last}"
printPersonGenericGroup personRecord
printPersonGenericGroup personClass
Person is: Person Record
Person is: Person Class
Person is: Person Record
Person is: Person Class
Person is: Person Record
Person is: Person Class
Person is: Person Record
Person is: Person Class
Person is: Person Record
Person is: Person Class
type PersonRecord =
{
First: string
Last: string
}
type PersonClass =
new: first: string * middle: string * last: string -> PersonClass
member First: string
member Last: string
member Middle: string
val personRecord: PersonRecord = { First = "Person"
Last = "Record" }
val personClass: PersonClass
val inline printPersonRegularParameter:
person: ^Person -> unit
when ^Person: (member get_First: ^Person -> string) and
^Person: (member get_Last: ^Person -> string)
val inline (|FirstName|) :
o: ^FirstName -> string
when ^FirstName: (member get_First: ^FirstName -> string)
val inline (|LastName|) :
o: ^LastName -> string
when ^LastName: (member get_Last: ^LastName -> string)
val inline printPersonActivePatterns:
^a -> unit
when ^a: (member get_Last: ^a -> string) and
^a: (member get_First: ^a -> string)
val inline (|Person|) :
^a -> string * string
when ^a: (member get_Last: ^a -> string) and
^a: (member get_First: ^a -> string)
val inline printPersonCombinedActivePattern:
^a -> unit
when ^a: (member get_Last: ^a -> string) and
^a: (member get_First: ^a -> string)
val inline printPersonGeneric:
person: 'Person (requires member First and member Last )
-> unit
val inline printPersonGenericGroup:
person: 'Person (requires member First and member Last )
-> unit
@AvalonWot
Copy link

https://gist.github.com/michaeloyer/615543e33713033dc1c4e01c98dd9f2a#file-srtp-fsx-L46-L50

let inline printPersonRegularParameter person = 
    // Notice the value is being pulled out of person with pattern matching
    let first = (^Person : (member First: string) person)
    let last = (^Person : (member Last: string) person) 
    printfn $"Person is: {first} {last}"

hello, where could find the document about this "pattern matching"?
I google it but get anything about it

@michaeloyer
Copy link
Author

michaeloyer commented May 8, 2023

This is a combination of the documentation on SRTP and Pattern Matching. Neither one contains an example of that directly, it's kind of a combination of both concepts working together.

F# 7 has updated the Syntax for SRTP (that you'll see in the documentation above) Depending on your preference this definition may be more practical:

let inline printPersonRegularParameter<
        'Person when 
            'Person:(member First: string) and 
            'Person:(member Last: string)
    > 
    (person:'Person) =

    printfn $"Person is: {person.First} {person.Last}"

@AvalonWot
Copy link

Thank you for your answer! Which pattern does SRTP combine with? is it "Identifier pattern"?

@michaeloyer
Copy link
Author

The concepts of SRTP and Patrern Matching are combined in this example.

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