Paradigms

Rather than trying to pin down what programming paradigms like ‘object-oriented’ or ‘functional’ are, I will write about what I think is important in programming and then discuss how paradigms help or hinder writing programs properly.

Loosely coupling the environment

To work for us, programs must interact with their environment. The program could be running inside a virtual machine inside a laptop. The laptop may be part of a network that contains other devices and the network may be connected to the internet. All of that is the environment the program can interact with. If it doesn’t, it better not run.

Interactions with the environment make programming more difficult. Code reuse requires a similar environment to work properly. In particular, testing interactive programs requires a realistic test environment. The code of an interactive program doesn’t show what is happening in the environment. Therefore maintainers must use their imagination or rely on documentation to figure out what a program does. Similarly, static analysis tools like type checkers miss a lot of information that might help in finding bugs.

The best way to deal with the environment in a program is, therefore, to split off the parts that interact with the environment into separate modules. The result is a pure core that is loosely coupled to the environment. The smaller the interactive modules are, the larger the part of the program that is easy to work with.

The cost of loose couplings

Loosely coupling the environment has costs that are unavoidable no matter what language or paradigm you use. The most obvious is that the loose couplings must be traversed for the program to have its interactions. A more technical problem is that performing computations requires interactions with the memory and processors of the machine the program is running on, whether those computations are pure or not. Let’s go into more detail here.

Suppose the code says that the program reads a file, processes its data and writes the results to another file. To become a program, the code has to pass a translator, like a compiler that translates it into a machine language, or an interpreter that translates it into direct action. The translator has to generate both the interaction needed for reading and writing files, but also the interactions needed to store the data and the results in memory and to perform calculations in the processors. A smart compiler could rewrite the source code to undo the loose couplings and to add the extra interactions to produce efficient machine code. A smart compiler is a slow compiler, however. A faster but less smart translator will leave more work to be done while the program is running. Either way, we pay for the convenience of loosely coupling the environment.

Comparing paradigms

Programming paradigms are not rigorously defined. Programming languages are almost never simply imperative or declarative but combine these aspects in the paradigms they support. For example, imperative Java code consists of class, interface, and method declarations which are preserved by the Java compiler. Java is generally considered an object-oriented language, so apparently, the object-oriented paradigm admits both imperative and declarative elements. Moreover, no matter what language or paradigm you use, loosely coupling the environment is possible to some extent.

In practice, declarative allows you to go much further. Programs can still be declared to interact with the environment directly, but in most cases, it is just as easy to declare a ‘hole’ where interactions can be injected as needed. The functional programming language Haskell goes a step further by forcing all interactions with the environment to go through the IO-monad, which I imagine encourages writing loosely coupled programs.

Imperative languages, on the other hand, are often ‘close to the metal’. That means ‘tightly coupled to the nearest environment’. They require time and memory management instructions everywhere. In essence, every program is coded as an interaction between the programmer and a virtual machine. This makes the job of a translator much easier–you can have faster compilers that produce faster programs. It also means loose coupling is only possible for the more remote environment. Unfortunately, it requires more code because of all the machine instructions. The result is a slower program unless the translator understands what you intended. This way, imperative languages encourage tightly coupling the remote parts of the environment as well.

Paradigm shift

The imperative paradigms were needed when computers had small memories and slow processors. Both the performance of the translator and the resulting program limited how smart an interpreter could be. These days are past us. Moreover, modern multicore machines require instructions different from those hard-coded in imperative languages that were designed for old-fashioned single-core machines. Those are good reasons to shift away from imperative and towards declarative paradigms.

 

On Monads 2: monads are everywhere

Like most modern scientific theories, the theory of monads is descriptive rather than prescriptive. When applied to programming, the theory is not supposed to tell you how to program nor how to design your libraries or programming languages. Instead the theory helps to describe how a computer composes side effects when it executes a program.

In Java the type signature of a method tells us that it returns a value of a certain type, but that is certainly not all a method can do.

  1. A method can wind up in an endless loop or a deadlock and never return anything.
  2. A method can throw an exception which will abort the computation of the method that is calling it, unless caught. Java also allow throwing errors, which shut down the virtual machine.
  3. If the return type is a class, the method can return null instead of an instance of that class.
  4. A method can change the state of the object it is defined on, the state of objects that are passed to it as arguments, or the state of global variables. This in turn may change the behavior of all methods dependent on those states.
  5. A method can start a new thread and cause the virtual machine to perform computations after the method returns.
  6. A method can communicate with the world outside of the virtual machine by performing IO operations.

For all of these side effects ordinary mathematical functions between sets don’t model Java property. Instead, we need a ‘Java monad’ to capture all outcomes of a method and to explain how side effects are combined when methods are composed into larger methods.

Resisting monads is futile. You don’t avoid the monads but a ubiquitous language for composing side effects. That means that your refusal to conform makes it harder for your customers to find and recognize solutions to their problems in other people’s code. I grant that this is a prescription, but note that it comes from outside the theory of monads itself.

There are other ways monads could help, however. The Java monad is pretty messy and this can make Java programs hard to understand for programmers and also for static analysis tools. For that reason, good programming style often comes down to ‘writing purer functions’.

  1. Use for-loops instead of while-loops, or better yet, iterate through a collection by passing a closure to a library function.
  2. Don’t use exceptions for business logic.
  3. Avoid using null.
  4. Avoid global variables and make objects immutable.
  5. Don’t write asynchronous code if you don’t have to. Use a futures monad when you must.
  6. Decouple parts of your program that have to perform IO.

These precautions would probably not be necessary if Java had been pure by default and only let programmers access side effects through monads, instead or expecting programmers to have the discipline to avoid them.

On Monads

Monads are like lions. It is easy to show what a lion is, but afterwards the students won’t understand lions or be able to work with them.

I will use a Java8-like syntax and terminology for code. A monad consists of a generic type M<T> and two functions–M<T> unit(T t) and M<U> bind(M<T> mt, Function<T,M<U>> f)–that satisfy the monad laws:

  • bind(unit(x),f) == f(x)
  • bind(x,unit) == x
  • bind(bind(x,f),g) == bind(x,y -> bind(f.apply(y),g))

That is all.

To understand monads you need to know why people use them. Programs are usually composed of smaller programs and monads let you redefine how composition works. This way, you can extend all datatypes with new operators, while preserving existing ones.

Take the a typical list monad for example. Instances of the generic type List<T> have methods for working with lists as part of the generic type itself. The function unit sends and object x to a one element list unit(x). That way it makes list operations available on the object. On the other hand, unit(x.anyMethod(y)) turns the result of any method defined on x into a list. Finally the function bind(l,f) applies the list valued function f to every element of the list l and concatenates the results in order into one big list. That way we can apply list valued functions to lists like we can apply ordinary functions to ordinary objects. Hence List<T> has access to both the methods of T and of List and also to every possible combination of them.

You can use the list monad for non deterministic algorithms, where functions can have several different return values. Many other kinds of functions and other methods of composition have their own monads. For example, a futures monad takes care of non blocking asynchronous computations while hiding the callback hell that usually goes with it.

Working with monads is a matter of remembering that you introduce a new form of composition on purpose and that you usually neither have to leave it behind, nor want to do so. Generally you want to keep using the bind and unit functions to keep everything happening “inside the monad” until “the end of the world”, which is the main method of your program. As always, practice makes perfect.