"Purely functional code makes some things easier to understand: because values don't change, you can call functions and know that only their return value matters—they don't change anything outside themselves. But this makes many real-world applications difficult: how do you write to a database, or to the screen?"
"This design has many nice side effects. For example, testing the functional pieces is very easy, and it often naturally allows isolated testing with no test doubles. It also leads to an imperative shell with few conditionals, making reasoning about the program's state over time much easier."
[1] https://www.destroyallsoftware.com/talks/boundaries
[2] https://www.destroyallsoftware.com/screencasts/catalog/funct...
Combining the two ideas
Transient imperative logic in the core (5%), Functional mantle (90%), Side-effecting imperative crust (5%).
If you want to stick to referential transparency, you can't use dependency injection: you have to pass all the parameters the function needs. None of these can be implicit or belong to a field on the class since that would mean side effects. The `Reader` monad is not dependency injection, it's dependency passing and it comes with a lot of unpleasant effects on your code.
And because of that, functional code is often very tedious to test. Actually, in my experience, there is a clear tension between code that's referentially transparent and code that's easily testable. In practice, you have to pick one, you can't have both.
In all my years of software development, I've never encountered a referentially-transparent function that was even remotely hard to test, let alone harder than one with environmental baggage. In fact, being referentially transparent opens you up to new kinds of powerful testing strategies that are nearly impossible if the function isn't, like QuickCheck. (I can't highly recommend quick check enough, it's worth the little learning curve 100x over)
Lets be honest, most dependency injection frameworks and techniques are about hiding junk under the rug. But they fix the symptoms, not the disease. You see, if you find yourself having components with too many dependencies, feeling pain on initialization, the problem is that you have too many dependencies, which actually means you have too much tight coupling and not that it is hard to initialize them. At this point you should embrace that pain and treat the actual disease.
Also, functional programming naturally leads to building descriptions of what you want, in a declarative way. So instead of depending directly on services that trigger side-effects directly, like a DB component that does inserts or something doing HTTP requests or whatever, instead you build your application to trigger events that will eventually be linked to those side-effects triggering services.
There are multiple ways of doing this. For example you could have a channel / queue of messages, with listeners waiting for events on that queue. And TFA actually speaks about the Free monad. Well the Free monad is about separating the business logic from the needed side-effects, the idea being to describe your business logic in a pure way and then build an interpreter that will go over the resulting signals and trigger whatever effects you want. There's no dependency injection needed anymore, because you achieve decoupling.
> And because of that, functional code is often very tedious to test.
That hasn't been my experience at all, quite the contrary, we've had really good results and we're doing such a good job of pushing the side-effects at the edge that we no longer care to unit-test side-effecting code. And yes, I believe you've had that experience, but I think it happens often with people new to FP that try and shoehorn their experience into the new paradigm.
E.g. do you need a component that needs to take input from a database? No, it doesn't have to depend on your database component at all. Do you need a component that has to insert stuff into the database? No, it doesn't have to depend on your database component at all. Etc.
In order to do a straight-forward conversion to functional programming, I suggest leaving the values as they are and each service becomes a free monad transformer. So, instead of having a logger, you have a logging monad transformer that has a log instruction. Instead of having a database, you have a database monad transformer that has a query instruction, etc.
You are then free (no pun intended) to replace the interpreters of these free monads during testing with whatever mock implementation you please and the result is a more principled dependency injection inspired style.
Actually, I would constrain the monad type via type classes, rather than using free monads, but the approaches are equivalent.
> And because of that, functional code is often very tedious to test.
Your argument rests on a fundamentally wrong assumption. Expressions in functional programs do not have to be (and indeed are almost never) referentially transparent. Just consider global or module-level immutable variables. Those function names? Also not referentially transparent. This goes all the way back to free variables in the lambda calculus: https://en.wikipedia.org/wiki/Lambda_calculus#Free_variables
Further, dependency injection is a completely idiotic and broken pattern and IMO the worst thing to come out of object oriented programming. Once you have dynamic scoping (surprise! also not referentially transparent) everything that DI does (and much more) becomes trivial.
If you want dependency injection as you've defined it, you can use (if we're talking about Haskell) typeclasses or, by extension, implicit parameters, to do dependency injection in the way you like.
It's still much safer and easier to reason about than Java-style dynamic dependency injection.
pure int frignate(database db, const config cfg);
cannot change anything except the database object (and anything reachable from it). Can not mutate the environment. Can not mutate the config object parameter. This is finer control than pure functional programming and safer than imperative/object-oriented programming. frignate :: (MonadDB m) => Config -> m Int
frignate cfg = do
db <- getDB
...
return 1
And it is composable, so if a function calls a function that uses one of the managed resources then the requirement propagates upward.And you can swap in non-IO based instances for testing, or whatever else you want.
I'm certain I've heard Hickey talk about it a few years ago as well. Trying to remember where.
That is not true and this overselling of the Free monad is hurting the concept.
The Free monad is nothing more than the flatMap/bind operation, specified as a data-structure, much like how a binary-search tree describes binary search. And this means an imposed ordering of operations and loss of information due to computations being suspended by means of functions.
You see, if the statement I'm disagreeing with would be true, then you'd be able to build something like .NET LINQ on top of Free. But you can't.
https://www.youtube.com/watch?v=H28QqxO7Ihc
The point is that free monads allow introspection up to the information-theoretic limit (obviously you can't inspect a program whose structure depend on a runtime value), while transformers do not allow any introspection at all.
https://people.cs.kuleuven.be/~tom.schrijvers/Research/paper...
> then you'd be able to build something like .NET LINQ on top of Free. But you can't.
You sort of can. You could build an interpreter of Linq commands and then have an executor interpret that. Which I suppose could be argued is making Linq. :)
From a DSL perspective, it's like you can only inspect the program so far as to know the next statement in the "do" block. To see what the next statement will be, you need to actually evaluate the current one.
Instead of free monads, if you want very analyzable structures, look at free applicative functors.
"Applicative functors are a generalisation of monads. Both allow the expression of effectful computations into an otherwise pure language, like Haskell. Applicative functors are to be preferred to monads when the structure of a computation is fixed a priori. That makes it possible to perform certain kinds of static analysis on applicative values. We define a notion of free applicative functor, prove that it satisfies the appropriate laws, and that the construction is left adjoint to a suitable forgetful functor. We show how free applicative functors can be used to implement embedded DSLs which can be statically analysed."
These are the same things we were going to happen when using Java and C++ years ago.
You can even see similar graphics here: https://docs.oracle.com/javase/tutorial/java/concepts/object...
I remember there was another in this tutorial that shared more with the image in this post. Although this is the same idea. You're just hiding Objects in Objects.
What follows is not necessarily of high value, I'm simply a working programmer since 20 odd years that's bit weary and sad that the craft appears to be stuck in a rut by getting stuck between an unnecessarily theory-less reality and a nirvana of unrealistic purity. It's - maybe - a backdrop to explain why I felt a need to thank you for your choice of words with so many words.
If we consider every problem has a shape (loose analogue for the set of constraints thet uniquely identifies a problem) , then for every shape, or class of shapes a problem embodies, there exists an in some sense - ideal - language to solve that problem. Few are however the times when you only need to solve one discrete class of problem in the same system, but in case of mismatc between language and shape, it's quite common the only solution brought forth is to change languages. Unfortunately that solution is rarely feasible for a multitude of reasons. In the fallout after having to keep working with the same non-ideal language, the entire idea that there are multiple ways - styles - to express a solution is sadly often lost. This would not necessarily happen if the idea that style matters enough that when we can't change language, we could, and would still change how we express the solution within our constraintd. Be it in any language or paradigm under the sun, we need the words from them all to be able to talk about our problems, as they are either unique or already solved.
It came out in a different guise under Model Driven Architecture and executable specifications a few years back.
It'll pop up again in the future under some other name. The principle is as old as computing itself, though.
Say pg/psql.
> Beginning at the center, each layer is translated into one or more languages with lower-level semantics.
ORM mapping into Java?
> At the outermost layer of the application, the final language is that of the application’s environment — for example, the programming language’s standard library or foreign function interface, or possibly even machine instructions.
Or maybe even html+javascript.
Congratulations, you have (re-)invented the layered architecture.
> The onion architecture can be implemented in object-oriented programming or in functional programming.
Hierarchical layering, careful interfaces, dynamic programming, functional composition, refinement, event-based simulation... all showing up before Simula was published in 1967. So, it anywhere from depended on to came after many things with properties key to OOP's effectiveness. It certainly got a powerful technique for structuring programs started but didn't happen in isolation or even necessarily ahead in many ways. It surprised me that needs of Monte Carlo apps is what led to OOP but not that ALGOL60 was involved.
http://phobos.ramapo.edu/~ldant/oop/simula_history.pdf
https://www.rand.org/content/dam/rand/pubs/research_memorand...
In the OO graphic, it's representing a single cell organism, hence the circle. This is not the complete application. It's encapsulating the state of a single process, and only through externally interacting with the organism can the state be inspected & changed, all based on time.
The circles in the FP represent domain logic, the inner most circle represents your high level business logic. This is the complete application. Then translating your logic to the next lower level domain, until the physical hardware layer is reached and your program becomes something concrete and runnable. This layering resembles an onion, which is also a circle.
http://www.haskellforall.com/2012/06/you-could-have-invented...
http://www.haskellforall.com/2012/07/purify-code-using-free-...
http://jeffreypalermo.com/blog/the-onion-architecture-part-1... http://jeffreypalermo.com/blog/the-onion-architecture-part-2... http://jeffreypalermo.com/blog/the-onion-architecture-part-3... http://jeffreypalermo.com/blog/onion-architecture-part-4-aft...
It makes me think of an article where the author tries to abstract the implementation of IO from the the domain logic. [1]
[1] https://blog.skcript.com/asynchronous-io-in-rust-36b623e7b96...