Closed Krzysztof-Cieslak closed 5 years ago
How will Saturn treat things such as database connections or other services which could be made available through D.I, but also work without it? Or phrased another way, will Saturn recommend D.I be used for everything, or only those ASP.NET Core components which require it?
@lmortimer
Or phrased another way, will Saturn recommend D.I be used for everything, or only those ASP.NET Core components which require it
I think (and it may change with time) that we will suggest using it just for framework services. And for composing your normal code users should use normal F# idiomatic ways of handling dependencies. But this is just suggestion - if someone wants to inject IRepository
into the action with this mechanism nothing would stop they from doing it. I guess correct usage patterns will emerge in real world usage ;)
I am not sure if dependency injection has a part in functional programming. IMHO dependency injection solves OOPs composition problem. FP doesn't have this problem. We use partial application instead.
i wonder if this will be needed if we follow advices from https://www.infoq.com/presentations/mock-fsharp-tdd
Unfortunately the only work by Mark Seemann that ASP.NET team heard about is his DI book. Saturn and Giraffe are both living in the world created for us by ASP.NET Core - we can't change fundamental designs of the framework.For example - you can't get an instance of the ILogger
without some kind of service locator / DI - that's exactly what ctx.GetILogger
function, that Giraffe provides, do.
As mentioned above - this is not really designed, or suggested to be a general DI mechanism (but it can be if someone really tries) but rather a way to get framework services in more declarative, and up-front manner.
In practice, Saturn controller actions (just like in any other MVC framework used correctly) become the place for some kind of glue code - it's a place where you get dependencies, get model and then pass those services, model and all other required stuff into your pure functional, idiomatic F# code. This solution won't change that, but makes it easier and more declarative which is totally in a spirit of all other F# abstractions. Instead of having set of ctx.getService<'T>
calls, you declaratively define set of services that you need from the framework.
Honestly as someone going through the FP struggles of this aspect now, DI isn't all too bad for modularizing codebases.
The only other real 'viable' option for F# is using Reader monad style dependency handling (which is still used a lot in Haskell production codebases due to its 'simplicity' compared to Eff or other more esoteric things). It's not as ergonomic in F# as DI and it's also pretty bad for perf in CLR land due to the endless allocs for closures, Reader binds and the many types of Reader<'T\'T2\'T3> generic instantiations you're going to have to create for the different pieces of your codebase, but it could work if you really want to.
Partial application isn't a true contestant for bigger projects at all as it wrecks your ability to refactor. When, not if, a function needs extra services you are now going to have to find all the places you called or partially applied it and add those extra service arguments, possible having to carry that service all the way up to the place this caller got its services from etc etc. Comparing that to the ease of constructor based DI or even a Reader monad with a record you need to extend and the difference is pretty stark.
For anything better like say PureScript can give with polymorphic records it's all dependent on the language features being there, and they're not there in F#. Has nothing to do with OO or FP, modularizing codebases follows a simple truth and there are only so many ways you can go about it.
EDIT: So I'm all for merging this!
I’m fairly convinced that I like this design. But there is one small thing that worries me a bit - forcing people to add this additional parameter even if they don’t use any dependencies. I wish we already had the overloads for the custom operations :-(
Closed in favour of #164
Dependency injection is part of .Net Core, and it’s the part that’s hard to ignore. While not idiomatic F# code, you just can’t get access to the framework services (such as
ILogger
, orIHostingEnv
) without DI. Giraffe “solves” this problem by addingGetServixe<‘T>
function toHttpContext
. However this is rather imperative way of doing things- you need to manually call function for each service you need. And fairly often this is the main activity that’s happening in the controller actions - user just gets those services and passes to the business logic code.This PRs changes the signature of all controller actions adding additional parameter to it, that represent actions dependencies. Dependencies are modeled as F# type - ripple or record. Framework automatically creates instances of this type calling
GetService
for ever record field or tuple element, and then pass this instance to the action handler. This enables user to declaratively describe what dependencies action has.What’s important is that ever action can have its own type for dependencies, meaning it can have its own dependency set.
Opinions and review welcomed.