7

I'm learning about "Functional Core, Imperative Shell" as espoused by Gary Bernhardt in his talk about "Boundaries". In reality, it seems like these ideas have been known for a long time, and put into practice with languages like Elm or Haskell. Gary has just articulated them very well and to an audience that is unfamiliar with the ideas.

What I want to know is how to handle lots of I/O from within the core. A modern application, especially on the web, can't help but have a ton of it. Getting users from the database, checking if access tokens are expired through introspection, renewing them, writing to the database, calling an external API to parse natural language, writing logs virtually everywhere, random values for encryption virtually everywhere. It doesn't seem linear to me at all about the so called "imperative shell".

So how can one reconcile these two principles?

One idea might be to write some kind of interpreter, that parses the response of a function and returns the next function for the interpreter to run. Then you have exactly zero decisions to make in the shell and just one big switch statement of all your core functions.

But... is this ultimate conclusion really better? Does you think it improves the architecture? Are there better ways?

To reiterate. The simple way:

Shell

But now image that core, is constantly needing the shell to do work and effectively make a lot of decisions.

Maletor
  • 179
  • 3
  • 1
    Avoid reading too much into the literary meaning of "core" : in this particular example, "core" **does not** have the same meaning as "of utmost importance" ; "taking control / in charge of", etc. Try think of "core" as "utility" ; "a suite of reusable components that are highly dependable and well tested". – rwong Sep 21 '20 at 00:56
  • 1
    Recommended watch [Deconstructing the framework, by Gary Bernhardt](https://www.dailymotion.com/video/x2guoue). – Theraot Sep 21 '20 at 05:10

3 Answers3

6

It is really-really difficult to explain this in a short answer, but you do that by essentially deferring any I/O (any non-pure action) to the "shell", what in Haskell would just be main.

So instead of, for example, querying the database and returning a count of users, you return a database operation that has an integer "value". You can continue to work with this unknown value, but the operation is not really executed. It is only executed once it gets "out" into the outer shell.

Your "interpreter" comment was not that far off, except you don't switch-case in the outer shell, you directly receive an "object" (call it whatever you like) that can be "executed" to do the dirty stuff. Sidenote: there are literally interpreter-based solutions that also exist.

I suggest you start with learning what monads are, if you have not done so yet, and go from there.

Robert Bräutigam
  • 11,473
  • 1
  • 17
  • 36
4

Calling it a "shell" and a "core" makes it sound more cohesive than it actually is. The pattern is more that your user management has an IO layer and a pure layer, and your session management has an IO layer and a pure layer, and your API calls have an IO layer and a pure layer, then you kind of have some glue that sticks all the IO layers together. Sometimes people treat pervasive things like logging or random numbers as okay to be impure because they aren't "in the main path," but there are concepts like the writer and state monads to handle those sorts of concerns as well.

This pattern is more and more common in imperative programming too, due to wanting to mock out the IO for testing. If you've ever made a minimal interface with one production implementation that does real IO and a test implementation that returns dummy data, and you push as much logic as possible to code that depends on this interface, then you've done the "impure core" architectural pattern, at least in a small part. Functional programmers just take it a step or two further.

Karl Bielefeldt
  • 146,727
  • 38
  • 279
  • 479
0

My understanding of "Functional Core, Imperative Shell" is basically that you do all the Input (I) in I/O first in the imperative shell, then you call your pure functions (in the functional core) to generate some result based on the input, and then in the imperative shell you take the result and do the Output (O) in I/O.

This has also been called Impureim sandwich by Mark Seemann.

To simplify things, you have three functions:

  • An impure function to read inputs: () => Input
  • A pure function: Input => Result
  • An impure function to write outputs: Result => ()

What you have pointed out is very true: things are not always linear in this way.

  1. You can have a long running process (function) that needs to be fed new inputs while it is running. This can be because of performance reasons, e.g. you cannot read all orders from the database at once. Or it can be for logical reasons, e.g. you don't know which orders you need to get from the database before doing some calculations.
  2. You can have a long running process (function) that needs to output some data while it is running. Again, this can be for performance reasons, e.g. you cannot return a 4GB object at the end of the function call, so you need to insert data into the database in pieces while you are running. Or it can be for monitoring reasons, e.g. you want to display status to the user while you are running.
  3. Even a short running function might need to get data from different sources based on some logic, e.g. you do some calculations then then decide you want to grab data from source 1 or source 2 based on the results of the calculations, and then you need to do calculations based on the data returned from source 1 or source 2. Based on the result of those calculations you need to read data from source 3 or source 4, and this can go on and on...

I think in those cases (which are very common), you really cannot have a true "Functional Core, Imperative Shell".

I think the next best thing is an Honest Potentially-Functional Core, Imperative Shell.

That is, you create a potentially-functional core. That is a collection of potentially-pure functions.

Basically, your functions are for example:

  • An impure function to read inputs: () => Input
  • A potentially-pure function: (Input, () => MoreInputs1, () => MoreInputs2, SomeResults1 => (), SomeResults2 => (), SomeResults3 => SomeInputs3) => Result
  • An impure function to write outputs: Result => ()

When the imperative shell calls the potentially-pure function, it passes impure functions for the following arguments (which are of type function):

  • () => MoreInputs1
  • () => MoreInputs2
  • SomeResults1 => ()
  • SomeResults2 => ()
  • SomeResults3 => SomeInputs3

The "pure" thing about your potentially-pure function is that it declares all dependencies that would make it impure. This makes it an honest function.

See this article for more details about composing potentially-pure functions: https://divex.dev/knowledge-base/main/dependency-injection-can-be-functional/

I think there are other ways to create a potentially-functional core. I think the Haskell IO monad is one such way, but I am not sure.

Yacoub Massad
  • 456
  • 3
  • 9