I 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."<p>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.<p>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.<p><a href="https://youtu.be/vK1DazRK_a0" rel="nofollow">https://youtu.be/vK1DazRK_a0</a>
I 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.<p>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).<p>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.<p>Less convenient but if you need to have tight control over memory allocations or performance this can be beneficial.
I think Gary Bernhardt's corresponding Boundaries conference talk [1] is quite good, particularly because of how pragmatic he is in introducing the topic.<p>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.<p>I've seen and read many, <i>many</i> 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.<p>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.<p>[1]: <a href="https://www.destroyallsoftware.com/talks/boundaries" rel="nofollow">https://www.destroyallsoftware.com/talks/boundaries</a>
I'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<p>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<p>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<p>The output side still needs "management" or a "shell" of sorts<p>(for context, I've only really played with this through Clojure's cljfx GUI library)
People 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.<p>So just do that in code signatures too.
Previous discussion:<p>- <a href="https://hw.leftium.com/#/item/18043058" rel="nofollow">https://hw.leftium.com/#/item/18043058</a> (127 comments)<p>- <a href="https://hw.leftium.com/#/item/29613107" rel="nofollow">https://hw.leftium.com/#/item/29613107</a><p>---<p>A FAQ is "Are there example of non trivial apps written in this manner?"<p>- CodeMirror/ProseMirror [1]<p>- React [2]<p>[1]: <a href="https://codemirror.net/docs/guide/#:~:text=Functional%20Core%2C%20Imperative,just%20break%20things" rel="nofollow">https://codemirror.net/docs/guide/#:~:text=Functional%20Core...</a>).<p>[2]: <a href="https://svitla.com/blog/functional-programming-in-typescript#:~:text=Enter%20the%20functional,render/hydrate%20function" rel="nofollow">https://svitla.com/blog/functional-programming-in-typescript...</a>.
This 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.<p>[1]: <a href="https://guide.elm-lang.org/architecture/" rel="nofollow">https://guide.elm-lang.org/architecture/</a>
Isn't this what we all are doing since the "birth" of the so-called web frameworks?<p>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).<p>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).
Am 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.<p>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.<p>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.
If it helps anyone:<p>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.<p>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.<p>Only after it’s working I start to pull out the pure bits and write tests for them.