Simplify Your Code: Functional Core, Imperative Shell

11 points by hwayne


zmitchell

Similar topic from Gary Bernhardt:

typesanitizer

The choice of example involving a database is an interesting one for sure. If I were looking at that code, I would be asking things like:

Now, of course I understand that it's meant to be a small toy example. But I find that trying to apply the idea of functional core/imperative shell for this kind of DB-related code basically ends up forgoing proper of database query planning optimizations, transactions etc. in favor of trying to handle everything in the application code.

xavdid

I find that I end up writing a lot of CLIs this way. There's a bunch of pure functions that are easily testable and then the top layer of CLI commands that call into those functions, but don't have much test coverage directly.

In practice, this works pretty well. the CLI commands don't change all that much and compiler / typechecker verifies that the CLI is supplying arguments for everything in the function, so drift is easy to catch. It works nicely!

ph14nix

I've seen people do a functional core even when the core logic is inherently interactive. The core logic gets implemented as a state machine f :: State -> InEvent -> (OutEvent, State). Your OutEvent could be an outside call you make which will deliver its result in the next InEvent.

It's surprisingly not bad when you're ok with some performance hit, and when your language supports concise ways of expressing such f. For example, in Haskell it's trivial to hand-make a monad (4 lines of code, roughly) where you can write the f as a regular sequential algorithm to be automatically translated to a state machine. Or if your language supports coroutines, e.g. Python generators, then you can abuse those to write state machines in sequential style.

def f():
  name = input('name: ')
  print(f'Hello {name}')

So something like the above is broken down into this.

def f_core():
  name = yield { 'func': 'input', 'arg': 'name:' }
  yield { 'func': 'print', 'arg': f'Hello {name}' } 

def f_shell():
  coroutine = f_core()
  req = coroutine.send(None)
  while True:
    try:
      if req['func'] == 'print':
        res = print(req['arg'])
      elif req['func'] == 'input':
        res = input(req['arg'])
      else:
        raise "Unknown function"
      req = coroutine.send(res)
    except StopIteration:
      break;

Maybe not best Python style, and clumsy without types, but it's very natural if you're used to purely functional approach. And it's is much cleaner if you're writing it in a functional language.

It gets terrible very quickly though when you're writing something like old C++ with no coroutines, having to unfold sequential code with loops and states all over into an explicit state machine is a burden hard to justify. It's unclear whether any benefit of the functional core is worth it. I guess, there's a limit to how much you can achieve with a good functional idea in a poor language.