I'll have to learn more about free monads. In general I love F# for doing domain modeling and logic, but I still find OO-style DI better for organizing "services". I've followed ploeh and scott wlaschin for some time and all my attempts to use their DI concepts in my own real-world code have led to code that's less intelligible than IoC with no tangible benefit. It's not for lack of trying, and I think not for lack of intelligence. It just never worked for me.
If free monads could provide something better than standard DI, and (and this is a big caveat) still retain decent editor integration (autocomplete, go-to-declaration/implementation), then I'd check it out. But my gut feeling says that it'll end up being a leaky abstraction that will need undue patching up just to maintain it.
F# uses .NET classes and objects for a module system, so your use of Objects for "services" is not surprising. An OCaml programmer is much less likely to miss Objects and DI frameworks, as OCaml has a powerful module system (i.e. module functors).
Free Monads can reify an effectful computation, giving flexibility on how it is interpreted. But they are not really a substitute for a good module system.
Okay I think I get free monads now. They seem pretty awesome, essentially letting you plug in an interpreter for the function you're going to run. I can see high-level how this would appear to be a great generic DI option--you set up an "interpreter" to handle the statements in your function however you want: for reals, for test, for reals with logging, etc. And automagically everything gets executed exactly how you want with no additional cruft.
What makes me cautious about the concept though is that e.g. `do_x_and_y()` would be interpreted differently than `do_x(); do_y()`, even if they were fundamentally the same. While "so what?" is a perfectly valid response, that little tidbit just makes me feel like, while FM's are a very cool abstraction for something, it's not really ideal for DI. It's just something meant for a different level. The article "The Wrong Abstraction" comes to mind.
> ... you set up an "interpreter" to handle the statements in your function however you want: for reals, for test, for reals with logging, etc.
That is the tip of the iceberg of free monads. Their full power lies in being able to combine different type sof effects into more powerful, composed effects. E.g. you want to do IO while also processing probability distributions using a probability monad. But they can get pretty hairy. See https://youtu.be/qaAKRxO21fU for the gory details.
The monad laws guarantee that there's no difference between do_x(); do_y(); and do_x_and_y(); where the latter is defined as { do_x(); do_y() }. In fact, monads would be pretty useless if that were not the case.
Out of curiosity, OCaml also has traditional classes/interfaces/objects, right? If so, then how do you decide when to use those versus module functors?
Modules and functors are able to contain type definitions, while classes/objects are not. This makes modules practically much more useful for abstraction.
AFAIK no one uses Objects in OCaml as the module system is sufficiently powerful. The Mirage project is a good example of using OCaml module functors to specialise components.
It's not any lack on your part. F# doesn't really support the really helpful abstraction techniques like parameterised modules or typeclasses. You can roll your own typeclasses using just simple records containing functions. It's easy, idiomatic, and it works statically. E.g., imagine you have a users 'service', with operations 'get by ID', 'add', and 'rename':
(** Type-safe IDs using a phantom type. *)
module Id =
type 'a t = private T of uint64
let of_uint64 u = T u
let to_uint64 (T u) = u
(** A domain type. *)
module User =
type t = private { id : t Id.t; name : string; age : int }
let make uid name age : t = ...
...
(** Users service typeclass. *)
module User_service =
type t =
{ get_by_id : User.t Id.t -> User.t Async
add : User.t -> unit Async
rename : string -> User.t Id.t -> unit Async }
let db : t =
{ get_by_id = fun uid -> ...
add = fun u -> ...
rename = fun name uid -> ... }
let test : t =
Now, injecting a user service dependency into any function is equivalent to passing in a parameter of type `User_service.t`.
As for free monads, I don't think F# will make them easy. If you notice, one commenter in that GitHub discussion mentioned they were doing a lot of copy-pasting to implement FMs. Imho that's a bad sign.
If free monads could provide something better than standard DI, and (and this is a big caveat) still retain decent editor integration (autocomplete, go-to-declaration/implementation), then I'd check it out. But my gut feeling says that it'll end up being a leaky abstraction that will need undue patching up just to maintain it.