A Highly Effective Approach for Designing Functional Programs
Nowadays, I design my programs with an approach that I call ‘Double Cone Design’. Inspired from a rendering of an elegant mathematical construct called a ‘double cone’ that is intersected by a plane segment, it colorfully illustrates my overall approach to software design –
The bottom cone, data abstraction, represents the bottom-up ‘architectural’ view of my programs. Except for primitive types like Vector2’s, nearly every type in my program is implemented as a data abstraction as described in SICP here. I also gave a detailed presentation on how to successfully architect F# programs by recursively applying data abstraction to your domain’s types.
The top cone, symbolics, represents the top-down, ‘representational’ view of my programs. Nearly every type in my program can be represented as a generic ‘Symbol’ type, manipulated symbolically, serialized to disk, and so on. Here’s the encoding of the Symbol type in F#.
Most importantly, it gives us the ability to implement a top-level scripting language for our program, as well as the ability to add component-specific domain-specific languages. An example of the latter being the special effect system of my purely functional game engine, Nu –
The plane section, purity / efficiency, represents the spectrum from purity to efficiency somewherein I choose to linger depending on what I’m creating. Think of moving along this plane by sliding the double cone either left of right.
How do we decide where to move on this spectrum? When programmability is paramount, such as when encoding game logic, I scoot closer to the purity side of the spectrum, building almost everything out of immutable data structures. When performance is paramount, such as when writing a renderer or physics engine, I build most things out of mutable data structures. When connecting the two worlds, I use very special techniques like message queuing and mutation-caching (EG – Prime.KeyedCache and Prime.MutantCache) to encapsulate highly-efficient mutable abstractions below the highly-programmable immutable APIs.
Not represented by the above image, but oft-enough utilized by myself to be of note in this discussion, is sub-typing as an extensibility technique. .NET-style interfaces on F# records and DUs allow users of our APIs to write self-contained components that can be plugged in to customize behavior where needed. Of course, component-style plug-ins as such are less composable than combinator-based APIs, but are often more suitable than the latter in certain circumstances.
Since I’m a functional programmer, I sometimes get push-back against the use of sub-typing via interfaces from my peers. But what I emphasize is that sub-typing is not OOP, but is in fact an orthogonal concept — and one that is unfortunately too conflated with OOP in the mind of most programmers due to its historical association. You can think of sub-typing as simply adding a level of abstraction over an existing set of related data abstractions to form an abstraction over data abstractions. We don’t always need this level of indirection, but it is essential for building things like program plug-ins in F#.
Interestingly, I’ve implemented both F# and C++ libraries (open source with MIT licenses on both) that provide the basic types needed for the ‘double cone design’ approach. If you’re going for purity and programmability (the left-hand side of our plane), I suggest you try out the F# library, Prime, here. If you’re going for raw machine efficiency (the right-hand side), try out the C++ library, ax, here. If you don’t like either of those languages, the relevant portions of these libraries are portable to most other languages.