Recently, I needed to implement a new service within a client’s developer-facing software development kit (SDK). This prompted me to explore the idea of implementing a mock with generic methods in F#.
The service provided some basic methods for creating RSA public and private key info. It also provided methods for encrypting and decrypting arbitrary F# types into hex strings and back using these keys. The signature of the service looks like this:
type ICryptoService =
abstract member GenerateKeys : unit -> string * string
abstract member Encrypt : string -> 'a -> string
abstract member Decrypt : string -> string -> 'a
GenerateKeys returns an RSA key pair, while Encrypt takes a private key and an arbitrary type and returns a hex string. Decrypt takes a public key and a hex string and returns the client requested type.
Testing code that consumed this service led me to some tricky F# details. Specifically, the problem was how to mock ICryptoService in a way that the user of the mock can specify a concrete return value for Decrypt, while still maintaining its generic signature. My first attempt looked like this:
let getCryptoServiceMock keys enc dec =
{ new ICryptoService with
member this.GenerateKeys () = keys
member this.Decrypt a b = dec a b
member this.Encrypt a b = enc a b }
The Problem
I initially thought that the caller of getCryptoServiceMock
could specify the behavior of the interface, and that type inference would sort out any type issues since the functions providing the actual implementation were passed through. However, the F# compiler complains:
error FS0670: This code is not sufficiently generic.
The type variable 'a could not be generalized because it would escape its scope.
Huh? Encrypt is generic, decrypt is generic, why can’t the compiler figure out to make the parameters to the mock function correspondingly generic and move on?
The problem is that caller determines the concrete type of a generic function, not the function being called. This means that getCryptoServiceMock
would be locked into a particular generic type based on the types of the lambdas passed in. This means that the service it returns would also be locked into that particular type.
That’s a problem in F# because the service isn’t generic. Two of the methods it exposes are. This means Encrypt
and Decrypt
need to be able to support any type that their callers ask for, not just the type that the caller of getCryptoServiceMock
locks the service into.
A Solution
So what does a successful implementation of getCryptoServiceMock
look like?
let getCryptoServiceMock
keys
(enc : string -> obj -> string)
(dec : string -> string -> obj)
=
{ new ICryptoService with
member this.GenerateKeys () = keys
member this.Decrypt a b = downcast (dec a b)
member this.Encrypt a b = enc a (box b)
}
This gets the job done with minimal fuss, while also keeping the F# compiler happy. We simply make the types of lambdas that implement the mock’s behavior explicit. By using obj
as the type, we can call F#’s downcast and box functions on the values of type obj
provided by the lambdas. This gives the compiler the wiggle room to get all of the generics lined up correctly.
Drop a comment if you have a more idiomatic approach to this problem!