Prefer Records of Functions to Interfaces

Matthew Doig
4 min readOct 7, 2016

--

The above advice directly contradicts the following suggestion from the Component Design Guidelines.

Consider using interface types to represent related groups of operations that may be implemented in multiple ways.

In F# there are a number of ways to represent a dictionary of operations, such as using tuples of functions or records of functions. In general, we recommend you use interface types for this purpose.

So why would we recommend doing the exact opposite of what the componenet design guildlines ask us to consider?

Prefer Simplicity to Interoperability

The component design guidelines focus on how we write f# code that plays nice with c# code, so in the context of interoperability, interfaces do make more sense, as they’re easier for object oriented programmers to use.

However, interoperability with an object oriented language when building a software system, is a rather niche concern. Do the authors of Suave.io really care how easy it is to use from c#? Should you care how easy your system is to use from c#? Probably not.

What you should be concerned about is simplicity and maintainability.

Records of Values

Anybody that programs in f# understands records of values. Here’s a simple record representing a todo item.

type TodoItem = 
{ Key : string
Name : string
IsComplete : bool }

And everybody understands creating an instance of this TodoItem.

let item1 = 
{ Key = "1"
Name = "My Item 1"
IsComplete = false }

And everybody understands passing our instance to a function.

let printTodo todo =
printfn "todo name:%A" todo.Name

printTodo item1

Records of Functions

As functions are first class values in f#, we can define records of functions in the same way we declare records of values.

type TodoRepository = 
{ Add : TodoItem -> Option<TodoItem>
GetAll : unit -> seq<TodoItem>
Find : string -> Option<TodoItem>
Remove : string -> Option<TodoItem>
Update : TodoItem -> Option<TodoItem> }

And create instances of in the exact same.

let todoRepositoryDb = 
{ Add = add
GetAll = getAll
Find = find
Remove = remove
Update = update }

And pass our instances in the exact same way.

let find db =
db.Find
find todoRepositoryDb "1"

Basically, if we understand records of values, we understand records of functions. Jumping conceptually from records of values to records of functions isn’t too tough.

Interfaces

So how easy it is to jump conceptually from records of value to interfaces?

Instead of a record of functions we’ll use an interface to represent our dictionary of operations.

type ITodoRepository = 
abstract Add : TodoItem -> Option<TodoItem>
abstract GetAll : unit -> seq<TodoItem>
abstract Find : string -> Option<TodoItem>
abstract Remove : string -> Option<TodoItem>
abstract Update : TodoItem -> Option<TodoItem>

Reasonably similar to our record of functions, except we’ve introduced a new keyword abstract that we didn’t need in the record of values.

And creating an instance of our interface is umm…well let’s just have a look.

type TodoRepositoryDbType() = 
interface ITodoRepository with
member x.Add(item) = add item
member x.GetAll() = getAll ()
member x.Find(key) = find key
member x.Remove(key) = remove key
member x.Update(item) = update item

First we’ve needed to create an additional type that implements our interface. And in defining this type we’ve introduced a few new keywords, and the syntax for declaring functions looks a little odd, and the getAll function requires those additional parenthesis.

At least now we can just create an instance of our though right? Not so fast…

let todoRepositoryDbTemp = new TodoRepositoryDbType()
let todoRepositoryDb = todoTemp :> ITodoRepository

First have to create an instance of the type that implements our interface then cast that type back to our interface type.

And when passing our interface type to a function, we have to make sure we annotate the function with a type signature for the interface instead of the type that implements it.

let find (db:ITodoRepository) key =
db.Find(key)
find todoRepositoryDb "1"

Hmm…

Just explaining what we’re doing is a mouthful, never mind the code to actually do it!

Prefer Functional Constructs to Object Oriented

F# is a multi paradigm language that fully supports object oriented programming, however our preference should be to keep things functional and simple until we need the added complexity of object orientation.

The mental load of all those keywords and type signatures and additional types adds up and we should be aware of the context required to understand our code. Object orientation requires a ton of context!

A dictionary of operations can be represented perfectly well with the functional construct of a record, so use it, and the programmer coming out of school to maintain your code will be happy you did.

--

--

Matthew Doig

A programmer looking for ways to see the forest instead of the trees.