Disclaimer: I havenāt watched the video.
So this is an opinionated issue, so donāt take anything I say as gospel. From the code snippets that you posted, I would much prefer the 2nd, as it seems much simpler. Thereās a few reasons Iāll reach for an interface, and I canāt speak to whether or not the author of the video intended any of these reasons:
- Interoping with other .net stuff. The .net ecosystem is fond of its interfaces, and a lot of times you need to have your own implementations of an interface just to use some of the existing machinery. This is where Object Expressions can be really handy, because you donāt need to clutter your type definitions and itās very low boilerplate
- Defining different records that share some fields, like
type Person =
abstract member Name : string
abstract member Email : string
type Employee = { Name:string; Email:string; ID:int; Salary:float } with
interface Person with
member this.Name = this.Name
member this.Email = this.Email
type Customer = { Name:string; Email:string; RelationManager:Employee } with
interface Person with
member this.Name = this.Name
member this.Email = this.Email
and then you can have functions that just take in a Person
directly and that can cut back on boilerplate.
- Passing around generic functions. This one is a little more advanced. Warning: highly contrived example. Say you want to build a list processing function
extract
that supports filtering the data in different ways:
let employees : Employee list = [ ... ]
let customers : Customer list = [ ... ]
let extract (filterFn:'a list -> 'a list) = (filterFn employees, filterFn customers)
let top50 xs = List.truncate 50 xs
let top50Percent xs = List.truncate (int (float (List.length xs) / 2.0)) xs
let last50 xs = top50 <| List.rev xs
let bottom50Percent xs = top50Percent <| List.rev xs
extract top50
extract bottom50Percent
Youāll get a compile error because filterFn
wonāt āstay generic.ā As soon as you call extract
, it has to figure out what 'a
is, and it has to be something concrete like Employee
or Customer
, but it canāt remain as a generic function that could accept either. You can fix this by altering it to use an interface:
type FilterFn =
abstract member filter : 'a list -> 'a list
let extract (filterFn:FilterFn) = (filterFn.filter employees, filterFn.filter customers)
let top50 = { new FilterFn with member _.filter xs = List.truncate 50 xs }
let top50Percent = { new FilterFn with member _.filter xs = List.truncate (int (float (List.length xs) / 2.0)) xs }
let last50 = { new FilterFn with member _.filter xs = top50 <| List.rev xs }
let bottom50Percent = { new FilterFn with member _.filter xs = top50Percent <| List.rev xs }
f top50
f bottom50Percent
Thereās probably other good reasons to reach for interfaces beyond the three that Iāve given here, but I would tend to agree that unless you have some need that an interface is specifically solving, itās much more common to stick to plain functions.
I suspect a lot of folks come to F# from C# (I know I did). I personally think itās best to try to tear down a lot of what you know and rebuild it with FP principles in mind, but I suspect thereās a lot of folks out there who appreciate some of the FP goodness that F# brings but are still stuck in sort of an OO frame of mind, and F# will happily accommodate those folks.