Open and Closed Systems with Clojure (Draft)
Please Note: I accidentally made this post public after writing only a rough outline on June 18, 2021. I will leave this content here for now since it's already been noticed, but I will post a link on this page to the finished version of the article once I have completed it. Please excuse the mess and my oversight.
For the purposes of this article, I propose the following definitions:
- Members of a software system are data values and functions.
- An open system requires only extension of itself to support new members. Consumers can leverage new members at any point in time, including never.
- A closed system either doesn't support extension, or requires simultaneous extension of itself and its consumers when adding new members.
These definitions break down quickly if we dwell on the facts that our programs run on computers that are also running other programs, that they interact with hardware that is susceptible to physical forces of nature, that they run in time and yet there is no guarantee that the next instruction of our program will ever be executed.
But if we limit ourselves to the tamer, optimistic view of the universe that we employ when writing our programs, these concepts of open and closed systems prove useful tools for designing software systems.
Clojure—not only because it is a dynamically typed language, but also by virtue of its abstractions—encourages open systems. In this article I explore Clojure constructs that support openness and hope to highlight the benefits software engineers derive from this default.
For the Clojure examples that follow, I consider a construct open when it returns a value rather than halting or disrupting the execution of the program by throwing an exception.
This comes up in many of the sections in this article. Need to make sure it's underscored, that these open systems are powerful because they can also be inspected by the userland program.
Closed true/false. Open truthy/falsey.
Functions with Infinite Arity
Arithmetic: +, -, *, /
map n collections accepting function of n args.
Core Clojure functions return
nil or provide an arity for user-specified default values, rather than throwing exceptions when sought-after items are not found:
nth as an oddball exception; make note of its arity that doesn't throw an exception.
Keys of arbitrary types. Go complex enough, and maps with maps as keys become a type of pre-indexed database.
Private functions still available via var.
Ability to mutate a namespace from multiple files.
Ability to list all known namespaces (e.g., see implementation of
Orthogonal to primary execution of your program, but choice of that is up to you.
Pattern of specialized members being adorned with metadata and then collected from arbitrary location by inspection of namespaces and metadata.
Open by default.
:default for categorical defaults, not fall-backs.
methods for enforcing comprehensive coverage over a particular domain of values.
No methods have to be implemented, just has to be "marked" as extending the protocol.
Completely agnostic to the collections (if any) they will work on. Composable at any level.
Closed Systems in Clojure
Lesson learned, and
defrecord does more open thing of demoting itself to a hash-map when essential keys are removed.
 record definitions so they never lose their type.
Case throws an exception; cond returns nil.
One area that perhaps a sanctioned closed-system approach would benefit. Perhaps propose one here...
Redefined but within a specified scope.
Closed by design. Syntax of base language is rich but must be circumscribed to keep programs source readable across a wide audience. Tagged literals as an acceptable compromise.
TBD: Possible Additions
- Datalog systems inspired by Datomic?