16 ms·
Gary Bernhardt describes a similar architecture using a "Functional Core, Imperative Shell" in his Boundaries talk[1]. "Purely functional code makes some thing
by slashdotdash 10y ago
Gary Bernhardt describes a similar architecture using a "Functional Core, Imperative Shell" in his Boundaries talk[1].
"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 https://www.destroyallsoftware.com/talks/boundaries
[2] https://www.destroyallsoftware.com/screencasts/catalog/functional-core-imperative-shell https://www.destroyallsoftware.com/screencasts/catalog/funct...
- asciihacker 10y agoI'm enjoying watching the boundaries talk, especially when he converts the serial code the concurrent code using actors.
- VeejayRampay 10y agoI'd like to use the occasion that I'm really waiting for the video of his talk "Ideology" given at StrangeLoop 2015.
- RodericDay 10y agoSame, but for whatever he talked about in PyCon 2015
- _mhr_ 10y agoIf it helps at all, here's a transcript of that talk: https://github.com/strangeloop/StrangeLoop2015/blob/master/transcripts/Gary%20Bernhardt%20StrangeLoop%209-25-15.txt https://github.com/strangeloop/StrangeLoop2015/blob/master/t...
- junke 10y agoAt the lowest levels, the pattern can be reversed: you provide a nice functional shell around a little bit of imperative core (e.g. implementing "map" with a loop).
- harryjo 10y agoYou are referring to, for example: http://clojure.org/reference/transients http://clojure.org/reference/transients Combining the two ideas Transient imperative logic in the core (5%), Functional mantle (90%), Side-effecting imperative crust (5%).
- junke 10y agoYes, because some purely functional approaches cannot beat imperative ones when it comes to resource usage.
- aisofteng 10y agoCould you give a concrete example?
- gamegoblin 10y agoMost in-place algorithms. E.g. quicksort You can do merge sort in Haskell asymptotically as well as C, but not quicksort (because you can't mutate things in place). I am of course omitting things like ST which do give you this sort of ability in Haskell, but I doubt that's what the OP meant by "purely functional".
- whateveracct 10y agoST is exactly the same sort of thing junke was talking about, except it also uses the type system to ensure the imperative core doesn't leak into the outside world.
- m_mueller 10y ago
- KirinDave 10y agoI was quite disappointed the author chose not to cite Bernhardt's work, as this is pretty clearly derivative of that talk and other work in the community around this design. I'm certain I've heard Hickey talk about it a few years ago as well. Trying to remember where.
- dack 10y agoI don't think it's derivative. He mentions the onion architecture, which is an older concept. I really liked the way Gary presented the idea, but he wasn't the originator.
- hota_mazi 10y agoI understand the value of referential transparency and how it makes "certain" things easy, but saying that it automatically makes testing functional code easy is a myth. Sometimes it does, sometimes it doesn't. 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.
- DanWaterworth 10y agoWhen you do dependency injection well, you don't inject every object; that would be horrible/impossible. What you may have noticed is that there are two kinds of objects, ones that you inject and ones that you don't. The ones you don't are things like numbers, strings and maps/sets; things that you treat as values. The other objects do things, I tend to call them services. 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.
- hota_mazi 10y agoI agree there is a dichotomy between objects you inject and objects you don't but I think your characterization is incorrect: what decides if an object needs to be injected is not tied to its type but to its role. Sometimes, I inject integers or strings or other primitive types. Other times, I pass them explicitly. The decision is made based on whether that object is a runtime object (i.e. decided by the user or some other factor that cannot be known when the app starts) or a dependency that's decided early and won't change through the life of the app. Either way, this aspect is independent of the point I was making above and which is that functional code is not inherently easier to test than procedural code.
- qznc 10y agoI like the 'weakly pure' concept in D. A function like 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.
- thinkpad20 10y agoI'm not sure how D's purity system works, but that doesn't seem very pure to me. Mutating the database object is a globally visible effect, after all.
- buzzybee 10y agoThe guarantee it's making is that it's not going to manipulate program state that is outside the scope of "db". That is a pretty big deal for a systems developer since, as I discovered while working with D, many standard library functions that you wouldn't have given a second thought about happen to do unpure things like set a processor flag. We aren't even talking about your own program. From an abstracted viewpoint, it's not great, since a whole database covers potentially a lot of scope, and you may not want to care about the details of your floating point calculations in hardware, but in a concrete sense this is totally correct!
- mej10 10y agoYou can achieve this level of control using monad transformers or extensible effects -- this is a standard technique in Haskell. 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.
- pmarreck 10y agoThis is one of the best talks I've ever seen, highly recommend anything by Gary Bernhardt