Given a ASP.NET MVC application with the following layers:
The reason for no separate data access layer, is that I'm using SQL type provider.
(The following code may not be working, as it's only a raw draft).
Now imagine a service named UserService
defined like:
module UserService =
let getAll memoize f =
memoize(fun _ -> f)
let tryGetByID id f memoize =
memoize(fun _ -> f id)
let add evict f name keyToEvict =
let result = f name
evict keyToEvict
result
And then in my Controllers layer, I'll have another module named UserImpl
or it could just as well be named UserMemCache
:
module UserImpl =
let keyFor = MemCache.keyFor
let inline memoize args =
MemCache.keyForCurrent args
|> CacheHelpers.memoize0 MemCache.tryGet MemCache.store
let getAll = memoize [] |> UserService.getAll
let tryGetByID id = memoize [id] |> UserService.tryGetByID id
let add =
keyFor <@ getAll @> [id]
|> UserService.add MemCache.evict
The usage of this would be like:
type UserController() =
inherit Controller()
let ctx = dbSchema.GetDataContext()
member x.GetAll() = UserImpl.getAll ctx.Users
member x.UserNumberOne = UserImpl.tryGetByID ctx.Users 1
member x.UserNumberTwo = UserImpl.tryGetByID ctx.Users 2
member x.Add(name) = UserImpl.add ctx.Users name
Using interfaces, we would have the following implementation:
type UserService(ICacheProvider cacheProvider, ITable<User> db) =
member x.GetAll() =
cacheProvider.memoize(fun _ -> db |> List.ofSeq)
member x.TryGetByID id =
cacheProvider.memoize(fun _ -> db |> Query.tryFirst <@ fun z -> z.ID = ID @>)
member x.Add name =
let result = db.Add name
cacheProvider.evict <@ x.GetAll() @> []
result
And the usage would be something like:
type UserController(ICacheProvider cacheProvider) =
inherit Controller()
let ctx = dbSchema.GetDataContext()
let userService = new UserService(cacheProvider, ctx.Users)
member x.GetAll() = userService.GetAll()
member x.UserNumberOne = userService.TryGetByID 1
member x.UserNumberTwo = userService.TryGetByID 2
Obviously the interface implementation has much less code, but it doesn't really feel like functional code anymore. If I start using interfaces throughout my web app, when do I know when to use higher order functions instead? - else I'll just end up with a plain old OOP solution.
So in short: When should interfaces be used, and when to use higher order functions? - some line has to be drawn, or it will all be types and interfaces, whereof the beauty of FP disappears.
On interfaces. First of all, I think you can see interfaces as just named pairs of functions. If you have:
type ICacheProvider =
abstract Get : string -> option<obj>
abstract Set : string * obj -> unit
then this is pretty much equivalent to having a pair (or a record) of functions:
type CacheProvider = (string -> option<obj>) * (string * obj -> unit)
The benefit of using interfaces is that you give the type a name (you would get that with records too) and you are more clearly expressing your intention (other components can implement the interface).
I think using an interface is a good idea if you have more than 2 functions that are often passed to some other function together - this way, you avoid having too many parameters.
Module or class. The real difference in your code is whether to use module with higher-order functions or a class that takes the interface as constructor argument. F# is a multi-paradigm language that combines functional and OO style, so I think using classes in this way is perfectly fine. (You can still benefit from the functional style when defining data types to represent the domain etc.)
One thing to keep in mind is that functional programming is all about composition. This might not be as useful in this case, but I always prefer writing code that I can compose to add more functionality rather than code that requires me to provide something when I want to use it.
Perhaps you could write it so that your database access code does not do caching directly (this would include all the database queries and pre-processing logic):
module UserService =
let getAll () = (...)
let tryGetByID id = (...)
let add name = (...)
...and then define a type that wraps this and adds caching (and this would then be used by the main type of the web application - it is quite similar to the type you defined in your example, but now we are separating the database access and memorization using a cache provider):
type UserService(cacheProvider:ICacheProvider) =
member x.GetAll() = cacheProvider.memoize UserSerivce.getAll ()
member x.TryGetByID id = cacheProvider.memoize UserService.tryGetByID id
member x.Add name = cacheProvider.memoize UserService.add name
Summary. But - I think your approach using a class that takes ICacheProvider
is perfectly fine - F# is pretty good in mixing functional and object oriented style. The example I posted is really just a possible extension that might be useful in bigger projects (if you wanted to use functional aspects and clearly separate different aspects of the functionality)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With