5 ms·
Functional Core, Imperative Shell (2012)
- throwaway81523 4y agoVideo doesn't work for me in Firefox, but if it is the one I think it is, I have seen it and it is very good. Recommended.
- maleldil 4y agoWorks fine for me on Firefox 110, macOS Monterey 12.6.3, M1 Pro. Perhaps it's an extension?
- nfrmatk 4y agoI think Gary Bernhardt's corresponding Boundaries conference talk [1] is quite good, particularly because of how pragmatic he is in introducing the topic. I have been thinking about and trying to work with this paradigm for a few years now. To be honest, it's been a struggle. Partly because it requires unlearning some habits and partly because there's limited opportunity to practice in my $DAYJOB where I'm working in codebases heavily invested in other paradigms. I've seen and read many, many videos, blog posts, etc. on this topic. Everyone loves to introduce the concept with a contrived example and then stop there. What I would love to see more of are complete examples for everything from simple to complex applications. I think there's potentially a lot of value in thinking this way (or at least taking as much from it as you can), but it's hard to learn and then to teach others without more examples to draw on. [1]: https://www.destroyallsoftware.com/talks/boundaries https://www.destroyallsoftware.com/talks/boundaries
- ecdavis 4y agoI've used this for many real applications that I would characterise as small (biggest being ~10KLOC excluding tests, types, etc.). I've applied the pattern across entire codebases and to specific components within much larger codebases which follow other patterns. I tend to combine it with some of the ideas in Mitchell Hashimoto's talk Advanced Testing with Go[0] - particularly small interfaces and defining test stubs alongside real implementations. In practice my imperative shell tends to have two layers. The inner layer is responsible for executing the imperative logic, while the outer layer is responsible for initialising configuration and dependencies, invoking the inner layer, and adhering to any sort of external interface that it may need to satisfy. Everything from the inner layer down through the functional core can be comprehensively tested using stub objects only -- no need to patch anything. Unfortunately everything I've applied this pattern to is proprietary, so I can't share any code examples. [0] https://www.youtube.com/watch?v=yszygk1cpEc https://www.youtube.com/watch?v=yszygk1cpEc
- whstl 4y agoI also did it for a Go app recently exactly like that. It was mostly about putting the "imperative" part closer to main.go and putting business rules and things like that in the other files: Routing, serialization, validation, business rules, data transformation, command line argument parsing, configuration parsing, they all go in the "functional" part. Instantiating the HTTP server, reading argv, files and reading/writing to the database goes in the "imperative" part. The major hurdle for me is frameworks and libraries that often want to be used in an imperative way. Some routers, for, instance could be purely functional, but they often want to instantiate the HTTP server themselves. Not a big issue in practice in Golang, though. Also: the database. To keep purity in a simple way, you gotta wrap the imperative parts and use callback-ish structures. This part is often where I cheat. But with a proper abstraction it's great. I hate to say the forbidden word here, but a "monadic" abstraction you can have your cake and eat it. I never did it in Golang and I have no idea whether it would work, but, I did it in C# and it was a breeze. I agree with your assessment about testing. In the end it was incredibly easy for me to get 100% coverage on the functional part.
- gkc3 4y agoWould love to see an example of your monadic database abstraction
- whstl 4y agoIf you're familiar with Node.js promises, you already know how it works. I just pass a hollow "DB" object to my controllers that can perform queries and commands, but instead of returning data imperatively, I use JS-like "promises" to chain the next steps. On the "success" callback you have the result of queries. At the end I will just return this "chain of statements" that wasn't executed yet. I only really run everything at the "imperative" layer, within a database transaction. return DB.Query(...).ThenCommand(res => ...).ThenHttpSuccess(res => ...); That's also similar to how Haskell IO works. If you desugar the "do" syntax, you get something like this. Of course I said "monadic" between quotes above because it doesn't follow the functor/monad laws, it's just a Fluent interface promise-thing tailor-made for that very small app. A big issue is that this was in very small project. Promises are not exactly pretty, and the code is not the easiest to maintain if you don't know how they work, which is why I "cheated" on the Golang app. I think someone else smarter might be able to figure this problem out too, though :)
- npstr 4y ago> This design has many nice side effects. Hahaha...great choice of words for an article on functional programming =)
- charlieflowers 4y agoThis is a classic and one of the most insightful talks/paradigms I've encountered. One of the few that stands the test of many years.
- LAC-Tech 4y agoAlmost the opposite of react, which is imperative core functional shell.
- Leftium 4y agoCould you elaborate? Do you mean the implementation internals of React, or how dev's end up using the React library? I tend to agree with https://svitla.com/blog/functional-programming-in-typescript#:~:text=Enter%20the%20functional,render/hydrate%20function https://svitla.com/blog/functional-programming-in-typescript....: > Enter the functional core, imperative shell approach where you can do as much work as possible in a functional way, and isolate impure interactions on a thin, outer layer. > The most prominent example of this approach is ReactJS where: > - All components can be seen as pure functions that take props, state, and context as an input and return a React element as an output. > - All “outside world interactions” take place on the ReactDOM render/hydrate function.
- LAC-Tech 4y agoIf you try and put all your state at the top and make the rendering a function of it, you end up with excessive re-renders. So you end up co-locating state at your leaf components - hidden state everywhere.
- geokon 4y agoI'm not super up to date with the web-dev world - but I think the folks in the react-and-company space have taken this the natural step forward With subscription/contexts you get to really pare down the whole imperative shell part. They allow you to keep using reproducible pure functions, but memoization allows you drastically minimize state management and to cache intermediary values, so you're not recomputing the whole world each time. When the state changes the whole system elegantly only recomputes the pieces that are strictly necessary. You don't need to do anything manually - it all just happens automatically For some reason I don't see this being picked up in generally outside of GUI/Web space. But the input side of state is semi-solved as I see it The output side still needs "management" or a "shell" of sorts (for context, I've only really played with this through Clojure's cljfx GUI library)
- michaelteter 4y agoI like to build this way when possible. Unfortunately, I find many people - especially tech leads - resistant to this approach. The first and most common reason is, "Why do I need this? What we are doing is fine." Or when the attitude is positive about it, the common issue is, "I think this would be too difficult for our devs to adopt." I find both of these scenarios disappointing and frustrating, because technology (and software) is usually about improving processes and systems so better results can be made more easily. But at this point it feels like a lost cause, or rather a hill not worth dying on. For my own projects, I go this path; for others, I work dumber and keep the corporate apple cart upright. Rant aside, there is a video which I really like that demonstrates this functional core/imperative shell approach: Solving Problems the Clojure Way - Rafal Dittwald. It is a Clojure talk, but it gives an imperative to functional refactor demo in JavaScript in the middle of the talk. Even though it is a toy example, it is big enough that the result shows a meaningful difference in approach. https://youtu.be/vK1DazRK_a0 https://youtu.be/vK1DazRK_a0
- epolanski 4y agoI think that most people, including leads, they do see the benefits, but they think the cons heavily outweighting the pros. Without having a certain number of key developers pushing the transition it's hard to make this jump. I would also say that this kind of transition can be had progressively, I myself know a person who pushed a company into a fully-typed static pure functional programming a team that started their whole business in php. Gradually moved them into typed functional php and then into Haskell. He was lucky enough that the team/company was small enough for that at a bigger scale this would've been much more difficult. In other places I've seen the transition happen (into the functional typescript ecosystem which is a sweet spot imho) it has been mostly the CTO pushing for it.
- danuker 4y agoHow do you gradually move the software of a small company to Haskell? Do you have to build two artifacts, one in PHP and one in Haskell? Two deployment mechanisms?
- dgb23 4y agoIf it helps anyone: I find it typically easier to ask “can I extract data and functions out of here” rather than “can I extract the side-effects out of here”. And I found that asking this question too early can be detrimental if you’re still trying to figure stuff out. I don’t follow the paradigm strictly and I often write code in order to understand something. The first draft is typically just procedural with minimal abstractions. It’s more efficient that way. Only after it’s working I start to pull out the pure bits and write tests for them.
- Leftium 4y agoThat's actually exactly how the author (Gary Bernhardt) describes implementing Functional Core, Imperative Shell: > This is the cadence of the entire project so far. I tend to bloat up this outer file to something like 250 lines in some cases. Then I extract behavior out, moving it into unit-tested classes, figuring out how to make it more functional. Skip to about 11:40 in the video. (There is some additional explanation right before that.)
- tlarkworthy 4y agoPeople get confused by the wizardly and can't recognize that in many places they are already doing it. A resource orientated REST API is a great example of this paradigm. The API model is declarative... but you interface with it as sequential http calls. So just do that in code signatures too.
- sirwhinesalot 4y agoI think this is a wonderful idea and one of the best ways to structure software, but I want to point out that the functional core is only one way to achieve the benefits and if you are in an environment where that is not possible due to performance or whatever, you can still derive most of the benefits by focusing on the state transformation part. The real key aspect of this approach is that your state is not spread around the code and changing all over the place, but instead a big State -> State function built out of many smaller referentially transparent functions. (i.e. the complete opposite of OO programming, which IMO is a generally terrible idea that only works well in Smalltalk or in distributed systems). You can achieve a similar outcome in the procedural setting with a "double buffered" state. Your procedural function writes out a new state based on the previous state, and the "imperative shell" only ever touches the "completed" states, and then flips them so the current state becomes the previous state and the previous state becomes the buffer to have the new state written into. Less convenient but if you need to have tight control over memory allocations or performance this can be beneficial.
- dgb23 4y agoConceptually this reminds me a bit of a ring buffer, which also deals with the issue of having to manage a fixed size memory allocation. Or a draft/publish workflow where you’re constantly re-use memory instead of remembering published documents. I like it! It’s clever and seems to be generally applicable. Is there a name for it?
- sirwhinesalot 4y agoThere's probably a name, but I don't know it, I saw the idea somewhere before but can't remember where.
- geokon 4y agoThe issue is that while this greatly simplifies the program logic, when you start to scale the application you do end up with a lot of redundant computation. So in the end you need to either: . take a performance hit . somehow track/update costly intermediaries in your global state, and this can balloon out of control really fast . pair it with some memoization mechanism/framework that automatically manages "derived state" variables
- Leftium 4y agoPrevious discussion: - https://hw.leftium.com/#/item/18043058 https://hw.leftium.com/#/item/18043058 (127 comments) - https://hw.leftium.com/#/item/29613107 https://hw.leftium.com/#/item/29613107 --- A FAQ is "Are there example of non trivial apps written in this manner?" - CodeMirror/ProseMirror [1] - React [2] [1]: https://codemirror.net/docs/guide/#:~:text=Functional%20Core%2C%20Imperative,just%20break%20things https://codemirror.net/docs/guide/#:~:text=Functional%20Core...). [2]: https://svitla.com/blog/functional-programming-in-typescript#:~:text=Enter%20the%20functional,render/hydrate%20function https://svitla.com/blog/functional-programming-in-typescript....
- rapnie 4y agoOr, to keep things on HN: - https://news.ycombinator.com/item?id=18043058 https://news.ycombinator.com/item?id=18043058 - https://news.ycombinator.com/item?id=29613107 https://news.ycombinator.com/item?id=29613107
- danwee 4y agoIsn't this what we all are doing since the "birth" of the so-called web frameworks? The funtional core is mainly your "use cases"/"services": they the core of your software because they deal with your domain model. They are functional in that they do not keep state (state belongs to the DB). The imperative shell is all about the "web controllers" and "db concrete implementations", and whatever i/o is needed. So, in your controller you read your POST request and retrieve the params you wanna work with; you also handle the rendering of templates if needed. Your db concrete implementation is all about SQL (or ORMs).
- F-W-M 4y agoAm kind of using this at work. All IO is isolated in an imperative shell layer, that communicates with an core via event. The core is kind of a state transformer, but actually does in place mutations. All state (of the business logic) is kept at one place, which has some nice benefits. Functional programming is not too important in the core, a lot of benefits arise without it, e.g.: I can replay a prod session from the events generated and get the exact same behaviour. Makes debugging a breeze. One advantage of having the state in one big place is that I can serialize it, send it to another thread that serves a UI from it.
- fredcy 4y agoThis is much like the Elm Architecture [1], a pattern that I enjoy using. That structure gives me problems though when I want to speed things up by caching frequent calculations and lookups in the View functions. In Elm, at least, this is only possible by keeping the cache explicitly in the Model and maintaining it in the Update function. So it feels like considerations specific to generating a particular view end up explicitly in the more general update of the application-global Model. And for the Update to avoid doing lots of such view-value computations not needed by the current view, the Update function has to know a lot about view-only state. [1]: https://guide.elm-lang.org/architecture/ https://guide.elm-lang.org/architecture/