4 ms·
> The downside of course, is that there are now five extra parameters for the function, which looks painful. (Of course, the equivalent method in the OO version
by default-kramer 8y ago
> The downside of course, is that there are now five extra parameters for the function, which looks painful. (Of course, the equivalent method in the OO version also had these five dependencies, but they were implicit). In my opinion though, this pain is actually helpful!
I'm definitely in favor of making dependencies explicit, but I wish there was a typed language that allowed me to avoid mentioning "less important" dependencies when I want to. Logging is a perfect example:
function log(msg: string) =
ImplicitDependencies<ILogInfo>().Write(msg)
Let's imagine that "ImplicitDependencies" is some special keyword. The compiler would give the "log" function a type of (string -> (implicit ILogInfo) -> unit). But my code is allowed to say that its type is (string -> unit) if it wants. Now if I use this "log" function in UpdateCustomerProfile, then UpdateCustomerProfile gets an (implicit ILogInfo) parameter too.
The compiler would also give me some way to designate a function as "root level" meaning that all implicit dependencies must be resolved. I'd imagine something like Racket's parameterize for this. (https://docs.racket-lang.org/guide/parameterize.html https://docs.racket-lang.org/guide/parameterize.html)
The idea is, most of the time, I don't want to keep mentioning logging in my type signatures. I just want to silently kick it up the call stack.
- gliese1337 8y agoScala almost has this. You can declare a function with "implicit" arguments, and the compiler will look for something that matches the requirements in each calling scope and insert it automatically. It does not, however, automatically infer the implicit parameters based on usage of implicit arguments in the function body, though.
- adrice727 8y agoDotty is moving in this direction with Implicit Function Types. https://dotty.epfl.ch/docs/reference/implicit-function-types.html https://dotty.epfl.ch/docs/reference/implicit-function-types... https://www.youtube.com/watch?v=YXDm3WHZT5g&feature=youtu.be https://www.youtube.com/watch?v=YXDm3WHZT5g&feature=youtu.be
- dragonwriter 8y agoOther than the fact that the implicit dependencies are declared as parameters (with an “implicit” modifier)) in function definitions and implicit only at the call site (which, IMO, is less awkward than the proposed syntax), Scala has something like this (the method of resolving the implicits may be different than you imagine, but it's not entirely clear to me what your resolution mechanism is.) https://docs.scala-lang.org/tour/implicit-parameters.html https://docs.scala-lang.org/tour/implicit-parameters.html
- default-kramer 8y agoThe idea is that I want to add an implicit parameter (say a logger) 10 layers deep in some call stack. I don't want to update all the signatures in the middle 8 layers, but at the root level I should get an error if I haven't satisfied that logger parameter. The idea is that the type system knows everything, but allows you ignore these implicit parameters if you want.
- dragonwriter 8y agoThat's essentially what Scala implicits do, though the resolution path is very much not straight up the call stack.
- gitgud 8y agoThe logging example reminds me of Aspect Oriented Programming, where you inject less-important things like logging into the programming using decorators. There's a few libraries that do that for c# not sure about the f# ecosystem though.
- bjz_ 8y agoAlgebraic effect systems can also do this. See Koka, Eff, Multicore OCaml, Frank, Purescript-run, freer-effects, freer-simple etc. These generally allow you to handle effects in different ways. Not sure if F#'s type system is powerful enough to build this as a library though.
- default-kramer 8y agoI took a look at Koka and it doesn't look like what I want because I have to correctly specify the effects whenever I write a function signature. What I am looking for is a type system that would track these "implicit dependencies" but not force me to pollute all my type signatures with them. In other words, if the compiler knows the type is function square3( x : int ) : div int My code should be allowed say function square3( x : int ) : int Basically, the problem is that in most typed languages, if you add a logger dependency to a function, then you have to rewrite the signatures of all the functions that depend on that function. That sucks.
- noblethrasher 8y agoLogging with computation expressions: https://fsharpforfunandprofit.com/posts/computation-expressions-intro/ https://fsharpforfunandprofit.com/posts/computation-expressi...
- efnx 8y agoYou can do this easily in Haskell using lens’s field classes and mtl style constraints.
- bunderbunder 8y agoThis is where I start to like Scala's traits. Being able to declare a class "with logging" and automatically get a pre-configured loger handle with no extra ceremony needed, is really convenient. It's also unprincipled as heck, from both an object-oriented and a functional perspective. I think my alignment in the logging department might be chaotic good.
- default-kramer 8y agoNice, I don't do much Scala so I never thought about using traits for that! Is that a common pattern? It seems similar to traditional OO dependency injection, except without all the ceremony of adding a constructor parameter and corresponding class member to hold it.
- bunderbunder 8y agoI'm not entirely sure how common it is. It's a case of multiple implementation inheritance, so I could see a lot of people balking at it.
- wtetzner 8y agoConceptually, Racket's parameterize is basically just dynamic vars (though practically, it's more complex than that, because continuations etc.). If you have a language that supports thread-locals and lambdas, you can get pretty far with something like: public static S parameterize<T,S>(ThreadLocal<T> param, T value, Callable<S> body) { T old = param.get(); param.set(value); try { S result = body.call(); } finally { param.set(old); } return result; } You can then do something like: public static final ThreadLocal<Logger> LOG = ThreadLocal.withInitial(() -> getRootLogger()); And when you need a different logger, you can do: String result = parameterize(LOG, myFancyLogger, () -> { return methodThatLogsThings(); });
- default-kramer 8y agoYep, this is pretty much it, except I want the type system to let me know if "methodThatLogsThings" makes it all the way to the top of the call stack without satisfying the LOG parameter. C# doesn't do that, but I think a theoretical version of C# could - just stick the implicit dependencies into an attribute on each method. For example, the compiler inserts an attribute like: [ImplicitDependency(LOG)] void methodThatLogsThings() Which would cascade up the call hierarchy until it gets satisfied.
- amorphid 8y agoHere's a blog article by Carbon Five on dependency injection in Elixir that I quite liked... https://blog.carbonfive.com/2018/03/19/lightweight-dependency-injection-in-elixir-without-the-tears/ https://blog.carbonfive.com/2018/03/19/lightweight-dependenc... For context, Elixir (which includes Erlang) is a compiled language, but it doesn't/can't catch a lot of compile time type errors that you'd only uncover at runtime. For example, one process can send a message of any type to another process. There just isn't a great way (for the Elixir compiler) to catch that at compile time. It gives you more flexibility, but you can't have confidence that it'll work just because it compiled. Dependency injection with tests is a really helpful tool in Elixir.