Modularity is a desirable property of any reasonably complex software. Modularity allows the programmer to examine, understand and change parts — modules — of the software while temporarily ignoring the rest of it. When the software becomes too large for a single programmer to work on it sequentially, modularity allows a team of individuals to work on it in parallel. Ideally, modularity is recursive — modules should themselves consist of modules, and so on, until each module is small enough for an individual to grasp quickly, even with an arbitrarily large team.
From one perspective, modularity is less about breaking down software into smaller modules, and more about creating small modules that can be easily combined with other modules to create large systems. Combining modules is called ‘composition’, and composability is the holy grail of software design.
Functions are the ultimate tool in the toolbox of composition. In mathematics, a function has inputs and outputs, and its definition represents the mapping of inputs to outputs. In the computational world, these mappings may be viewed as transformations of inputs into outputs. Functions are inherently composable, as under the right conditions, the outputs of one function may be connected to the input of another function (even itself). Unfortunately, functions in mainstream programming languages are impure in the sense that they may do other things, such as write bytes to disk or send data over a network. These so-called ‘effects’ hinder our ability to compose functions using their mathematical representations, unless the effects themselves are modeled as first-class inputs or outputs.
Only a few languages like Haskell support pure functions — functions that are free of effects. In Haskell, effects are possible only if they are modeled as first-class inputs or outputs. In these cases, effects are encoded into the runtime instances of a special type called IO
, and it is the responsibility of language runtime to execute these effects on behalf of the programmer. For example, in the program below, the main
function returns an IO
instance, and the language runtime executes the effects encapsulated by the instance. In fact, without resorting to backdoor (aka unsafe) techniques, it is not possible to specify effects within a function that doesn’t return an IO
instance. With the IO
type, Haskell programs are smart enough to declare which functions are effectful, and these functions can be distinguished from ones that are not.
import System.IO (BufferMode (NoBuffering), hSetBuffering, stdout)
-- Main program.
-- This function returns an IO instance.
main :: IO ()
main = do
hSetBuffering stdout NoBuffering -- :: IO ()
putStr $ "Enter a number x: " -- :: IO ()
x <- getLine -- :: IO String
putStr $ "Enter a number y: " -- :: IO ()
y <- getLine -- :: IO String
putStrLn $ show $ mult (read x) (read y) -- :: IO ()
-- Multiply two numbers.
-- This function cannot write to disk or send data over the network.
mult :: Int -> Int -> Int
mult x y = x * y
Effects represented by IO
instances can themselves be combined, but only sequentially. In the example above, each line within the main
function returns some kind of IO
instance. These IO
instances are strung together to create a single combined expression…which is itself an IO
instance. Furthermore, the computational results of an IO
instance can never be extracted into a pure function: once you enter the real world, you can never come back.
Object Oriented Languages
In object-oriented languages like Java, composability still remains the holy grail of software development, and so lessons from the functional world are applicable. The essence of the ideas described above can be boiled down into a simple rule of thumb —
When you need to perform an action that deals with the external world, like writing to disk or sending data over the network, encapsulate the action within a ‘command’, and separate the decision of performing the action from actually performing the action.
This separation allows you as the programmer to inspect, re-arrange and re-compose your effectful code easily. Given adequately precise shapes for commands, the compiler will even aid you in making these changes safe. The command interface is analogous to the IO
type in Haskell. And just like IO
, you can string together commands to construct more sophisticated composite ones.
Once you are speaking the language of commands, you can perform computations on the commands themselves. For instance, suppose that instead of printing a message to the screen, you create a ‘log statement’ object that is capable of printing a message to the screen, and then invoke it. You can now enrich all log statements with timestamps, apply filtering based on various criteria and perform other actions before or after you print messages. As another example, suppose that instead of calling a remote web service, you create a ‘service invoker’ object that is given all of the information it needs to call the remote web service, and then invoke it. You can now apply throttling and caching mechanisms that control the flow of how these effects are performed.
The real world is messy, uncertain and error-prone. If we can limit the interactions of our systems with the external world to a few points at the edges, our software is that much more robust, and easier to develop, operate and maintain.
That’s all for today, folks! 🖖