Feeling my way through Functional Programming

I’ve recently started using Elm to create front-end applications, and I’ve been so impressed with the compiler and the general ease with which I have been able to quickly produce single page web apps that I have decided to start moving my whole stack towards a functional paradigm. I’m learning Haskell, with the intention of then going on to learn Yesod so that I can have the low-maintenance benefits of a functional stack. Very important for lone-wolf web developers like me.

Being a developer with an OOP background, the toughest thing I have come up against is how to think about a functional program’s architecture. This is an ongoing process for me, and I am a long way from being competent in this area in any regard, but nonetheless I have recently encountered my first epiphany in my journey from OOP to functional program architecture.

When I started to learn Elm, one of the paradigms I picked up on was that behaviour and state are separate. In OOP they are mixed together: an object has state, and it has methods which affect that state. In functional programming this is not the case. Data is stored as primitives or objects (the nearest thing to a class is a Record), and the functions are stored in modules. If you want to update a record* then you pass that record to a function which will make the change. It cannot change itself because it has no behaviour of its own.

Initially, I solved this problem in my head by saying “OK, behaviour is in the module, data is in the record (which can be defined in the module) therefore I can create modules that are like classes, except that you pass the record to the function instead of the function being part of the record. No problem.”

However, this hasn’t worked. The reason it hasn’t worked is that records can nest other records (like a one-to-one or one-to-many relationship), and when modelling a data graph you often have bi-directional relationships, i.e. A -> B and B -> A. If A and B are defined in separate modules, then you create a circular dependency which elm will not permit. This even applies if you try and model a join relationship such as A->C->B and B->C->A.

For instance:


module A exposing (A,functions)
type alias A = 
    { name: String
    , rel: B
    }
{|- Functions for A -}

module B exposing (B,functions)
type alias B = 
    { name: String
    , rel: A
    }
{|- Functions for B -}

So defining records in modules and putting associated functions in that module as if they were a class does not work.

However, it then occurred to me that you can put all your models together in the same file. This cures the circular dependency problem but feels unnatural since I have trained myself to solve problems by writing code where each file relates to a specific subdomain of the problem I am trying to solve. But this is an OOP way of thinking. So I stepped back and asked myself, what does it feel like to have all the models in one file, and functions that can affect those models in different files?

It feels like a toolbox. In a toolbox you keep all the screws in one place, and the hammer and the screwdrivers in another. When you want to put a screw in the wall, you don’t ask the screw to do it itself, you select the screw and a screwdriver to fit it and do some screwing.

Clearly, this is a highly ambiguous and imprecise way of viewing how a functional program is constructed but I think it represents some progress. It actually quite nicely follows the way that dependencies are declared in elm since you do not have to import the entire module, just the bits that are relevant to the dependency, so you don’t have to suddenly clutter your program with functions that are not useful. If I want the screwdriver I can use this syntax:

import Toolbox exposing (Screwdriver)

So all my tools still live in the box, and the only one I’ve taken out is the screwdriver.

Viewed like this, things seem quite simple again. Whether or not this is the case in 6 weeks time remains to be seen…

* You can’t actually change a record, everything in a functional language is immutable. You create a copy where the new value is in the place of the old one.

Comments

Popular Posts