DEV Community

Cover image for We Shape Our Tools: Why Functional Programming?
Keith
Keith

Posted on

We Shape Our Tools: Why Functional Programming?

Introduction

“If you want to build a ship, don’t drum up people to collect wood and don’t assign them tasks and work, but rather teach them to long for the endless immensity of the sea.”

Antoine de Saint-Exupéry.

When I first learned to write a computer program, computers’ memories were measured in kilobytes—4K, 16K, 32K. Large chunks of RAM were often taken up by screen memory—regions of RAM dedicated to what was shown on the screen. On a 32K machine, that could easily leave you with just 12K of usable space.

In the UK in the early 1980s, programming was typically done in assembler (Z80 or 6502) or in a dialect of BASIC, most of which didn’t even have the concept of functions or local variables (BBC BASIC being a notable exception—it supported both). Subroutines existed, but all variables were global. In short, neither BASIC nor assembler offered much in the way of support for structuring programs or managing complexity.

And yet people successfully wrote working programs—often with clever, convoluted logic designed to cope with the machines’ limitations in speed and storage. So how was this possible?

Because there’s only so complicated a program can get when you have 12K to play with. You could keep all that complexity in your head and still get along just fine. It was rare for more than one person to collaborate on the same program—there simply weren’t tools to support that kind of workflow.

Since then, computers’ memories have grown beyond measure. The computer I’m writing this on doesn’t have 32K; it has 64 GB. That’s over two million times as much memory. It is, of course, out of the question that anyone could hold anything like that in their head.

I regularly see articles about the computer that placed Neil Armstrong and Buzz Aldrin on the moon and the program that ran on its tiny memory—72K of ROM for the programs and 4K for ‘working storage’—and sometimes I think that programmers yearn for the days when computer systems were so small that they could be developed by a single individual. With only one person in charge, the program can become a personal work of art, with a consistent design aesthetic.

Attractive though it is, computers with more resources bring freedom to create more sophisticated and engaging applications. However, as applications grow, so does their complexity. It takes aesthetic judgment and constant effort to resist entropy—the effect where systems naturally tend toward disorder. The story of computer programming has always been a fight against entropy. As programs get larger, the more we need techniques to keep them understandable and manageable.

Functional programming offers tools to help keep a handle on complexity—in broad terms, to keep it in line with the size of a program, not increasing exponentially with it. In this and subsequent articles, I discuss the benefits of functional programming in general, specifically focusing on the ML family of languages (ML; OCaml; F#) and what they carry with them (e.g. strong, static typing).

Take a moment to consider how functional programming is typically presented: it was developed by logicians, mathematicians, linguistics experts, and computer scientists; the examples presented often solve mathematics problems; it’s based on lambda calculus and latterly category theory; its advocates are frequently university professors; and it tends to use phrases like ‘easy to reason about’ instead of simply saying ‘easy to understand’, or ‘easy to make sense of’. It should not be surprising that faced with this, most people run for the hills. Those who do make the effort to learn functional programming are often co-opted into the club and start using the aforementioned academic-sounding phrases, continuing the cycle.

However, plenty of mathematics went into the development of computers themselves and spilled over into the development of imperative languages. You don’t need to know about all of that to program in an imperative language, and so it is with functional languages.

Why write about this now?

Why did I want to write about functional programming in a world where AI coding agents are doing a quite remarkable job of being able to write code with the only input being plain English? How best to write a computer program is still an interesting topic, and it’s also the case that better programming languages allow for the encoding of business rules into the program itself, unlike for example, comments, those rules are like granite on the page. They are irrefutable and can be used by the large language model as context.

I have long been fascinated by what I view as to be quite simply the best form of programming; at least, for the majority of use cases. There is little doubt that functional programming has largely resisted mainstream adoption; at least, in its complete form, and I will look at why this is the case (and indeed, whether it is really the case).

Equally, it also refuses to die, with its ideas having heavily influences mainstream programming languages in fact, more now than ever, with some key features of functional languages for decades making their way into languages such as Javascript, C# and Java.

I recently developed a large (c. 60,000 lines of code), production web application in F# and found there to be no issues whatsoever as compared with developing it in a more mainstream language (it was a rewrite of an application I wrote twenty years ago in Java). I have to admit, I did expect to run into issues, due to F# being substantially less popular than more mainstream options, but I did not. I suspect the reason for this is that the extremely well-thought-through language is sitting upon the .NET platform, which has been maturing for a quarter of a century. My main regret was not doing this many years before I did, because the resultant application is just far more satisfying, feels like it has had far fewer defects and is far easier to understand than an equivalent sized application in a mainstream programming language.

Interestingly, I used large language models to help me make sense of the program (not so much to write it, as I wanted to do that myself) and it had no problem with F# at all. The UI is in Typescript, which is probably more than one hundred times as popular as F#, but my subjective assessment is that it is better at F# than Typescript. I asked it about this and its answer was exactly what I suspected, because the rules were so strong in the program itself, it made it easier to make sense of.

What’s good about it?

I will go into examples later, but right now, I wanted to talk about what it feels like. Functional programming is satisfying. In mainstream languages you regularly write code that feels like it’s just not quite right, but it’s the best you can do—functional programming regularly feels like you’re expressing the logic perfectly.

When you look at code, if you have defined your data structures well, you know that you have covered all the relevant cases and that these ‘invariants’ (rules that always hold true—we will discuss this more later) are enforced across the whole codebase—it’s a very satisfying feeling, and not one you tend to get from mainstream programming languages, where you are left feeling that you still have to keep a lot in your head.

Functional programming is a tool that substantially reduces the accumulation of errors in your application. It does this by:

  • Heavily encouraging techniques that force programs to remain simple even as they grow in size. This minimises cognitive load; that is, how hard you have to think about something to work out what’s going on
  • Allowing you to define your domain and its constraints in such a way that helps you manage edge cases, again, even as the program grows larger

Is it that different from mainstream programming?

The unfamiliar would be forgiven for thinking thinking that functional programming is fundamentally different from what we might call mainstream programming, and so to learn it would be learning something from scratch. This might be one reason for its limited uptake—the ‘QWERTY keyboard effect’—most people know it was designed to slow people down, but almost no-one is willing to put in the effort to learn a different keyboard layout. It is a different evolutionary branch , but it’s important to understand that whilst this makes it sound as though your existing knowledge of programming is useless, this is absolutely not the case—functional programming is not so different from that which you already know—it is very similar in some areas and differs in others, for example:

  • It still has boolean logic, decision making and looping (of a form), which make up the backbone of how programming languages get stuff done
  • Conversely, it does a bunch of things to either close off or de-emphasise things that tend to make programs harder to understand as they grow in size; for example, not having null, or ensuring you can't change the value of something referred to by an identifier, once you set it

It’s a lot easier than you think it is—it just takes a bit of practice—but then, learning to develop in e.g. Python when you are a Java developer takes a bit of practice. I don’t think the leap is that much greater, but it is a lot more satisfying. Not least because a couple of the examples above are well-understood to be issues in mainstream languages—it's just that they don't for the most part actually fix those issues.

A grain of sand in every gear

A little framing is required here. Historically, I have found explanations of the benefits of functional programming to be somewhat unsatisfying, often because the examples seem so simple that you take one look at them and conclude that it isn't really an issue—how can this small issue translate into a fundamental breakdown in understandability of a large program. This leads to a tension—if the writer uses a complicated example, the reader will likely switch off; if the writer uses a simple example, the leader may conclude there is no real problem.

What's really going on here is that the complexity emerges from lots of component parts, each with issues associated with them. The complexity multiplies the larger your program gets. Everyone is familiar with the feeling that when their application was small, it was easy to wrap their heads around, but the larger the application gets, particularly as more people get involved in its development, it gets harder and harder, often until the inevitable 'we need to rewrite it all' discussions start happening.

A lot of what gives functional programming its power is simply tackling the small issues in programming—closing the gaps that allow bugs to creep in and that make programs harder to understand.

Functional programming answers real-world problems with elegance. It places constraints on your programs that strongly encourage simplicity, by largely outlawing certain ways of programming known to increase complexity.

We will cover this later in more detail, but for now, null is a good example. Most mainstream programming languages allow for all objects to be either an object or null by default. The problem is that it is unclear what null means—it essentially means what the programmer chooses it to mean. However, if the programmer doesn't want it to have a null state (perhaps the ideal, since then every time you access that object, you know it's going to be there and not fail with some kind of null pointer exception), you don't have that choice. Because it's allowed at the source, every client of that object is obliged to code defensively. It’s possible that if you know that it's not null, you could avoid a null check, but that only works for a short time—you’ll forget. Others will not know, and there’s nothing telling you. Very quickly, this turns large applications into a gigantic mess of null checks obscuring the real logic. And then someone invents the ‘?’ thing as a shorthand for null checks. This doesn't fix the problem—it just makes a codebase will null checks all over it look less messy. It means that it's just as hard to make sense of what route code will take, because you just don't know what might or might not be null.

There is a big difference between guarantees that a program enforces and promises its authors must remember to keep. In the example where variables may also be null, the programmer must either always deal with variables potentially being null, or remember which ones could be null. Because the latter case is unrealistic in a large codebase, null checks proliferate.

The key point here is that the issue compounds—one object allowing null isn’t just a single problem, it’s a problem wherever the object is used. A far better solution is quite straightforward—it’s to not have null at all. If you don’t have null, then you don’t have null checks and hey ho, everything is better! Is it that easy? Well, no—there are circumstances where you may want a piece of data’s existence to be optional, but functional programming languages provide the tools for this, as we will get into later. The key insight is that in practice, it’s only a small proportion of your data that fits into this category—most data can happily do without the ‘null’ state, so by making the default position that data is not nullable, and by providing watertight solutions for when data is optional, you get the best of both worlds.

Modern languages have increasingly moved in this direction. Swift and Kotlin made ordinary types non-nullable from the outset; TypeScript and C# added checking to distinguish values that may be absent from those that must be present. However, retrofitting this to an established language is more difficult: existing code and libraries were written under different assumptions, and enabling stricter checks often means the compiler throws up a huge bank of red lights. It is a good example of how rigorous work done many decades ago continues to influence programming language design today.

To see how this works in practice, consider the following function. It’s asking if a user on an internet message board is an administrator of a given message board. The signature in F# looks like this:

let isAdminOfBoard (userInfo: UserInfo) (boardId: BoardId): bool =
Enter fullscreen mode Exit fullscreen mode

In English, if you call isAdminOfBoard giving it a UserInfo record and a BoardId, which is behind the type alias, an integer, it will return true or false. The whole function is as follows:

let isAdminOfBoard (userInfo: UserInfo) (boardId: BoardId): bool =
    match userInfo.userState with
    | LoggedOut -> false
    | LoggedIn user ->
        match user.role with
        | Root | SiteAdmin -> true
        | BoardAdmin boards -> Set.contains boardId boards
        | User -> false
Enter fullscreen mode Exit fullscreen mode

Note that ‘LoggedOut’, ‘LoggedIn’, ‘Root’ etc. are not strings—we are not comparing strings here, these are tags that form part of the type as it is defined. For example the UserState record is defined as follows, meaning a user state instance is either LoggedOut, or LoggedIn—in which case you get a User record along with it—the type is defined as follows:

type UserState =
    | LoggedOut
    | LoggedIn of User
Enter fullscreen mode Exit fullscreen mode

The UserInfo record doesn’t immediately give access to whether the user is an admin or not—it only gives access to the user’s state, which might be logged out or logged in. Clearly a logged out user cannot be an administrator, so we are forced to traverse the logic. If the user is ‘LoggedOut’, then they’re not an admin. If the user is ‘LoggedIn’, then we, at this point only, get access to the User record, which we can then ask what the user’s role is:

  • If the user is Root user or a SiteAdmin, then they are an admin
  • If the user is a simple ‘User’, then they are not an admin
  • If the user is a board administrator, then this provides us with a set of boards that the user is an administrator of. If one of the boards in this set is the one we’re interested in (i.e. passed as the boardId to the function), then the user is an administrator

Nothing here can be null, so there are no null checks. The ‘invariants’ are clear in the data structures—the user is either logged out or logged in. Only if they are logged in does having a role make any sense, so that data can only be accessed in the LoggedIn branch. Similarly, only if you are a board admin does a set of boards that you administrate make any sense, and as such, is only accessible to the program if the passed user is a board administrator.

We could consider how this piece of code might look in e.g. a traditional Java codebase—no doubt there are various designs, and modern Java provides a few more, albeit convoluted, inelegant options (inspired by capabilities within functional programming languages), but in a typical Java program, it may look like this:

public boolean isAdminOfBoard(boolean isLoggedIn, int boardId, User user, Set<Integer> adminOfBoards) {
    if (!isLoggedIn || user == null || user.role == null) {
        return false;
    }
    switch (user.role) {
        case Root:
        case SiteAdmin:
            return true;
        case BoardAdmin:
            return adminOfBoards != null && adminOfBoards.contains(boardId);
        case User:
        default:
            return false;
    }
}
Enter fullscreen mode Exit fullscreen mode

This code has a selection of issues associated with it, including:

  1. You can have isLoggedIn=true but with no supplied User object
  2. You can have the opposite, isLoggedIn could be false but with a supplied User object
  3. You can have a board administrator with no list of boards that they administrate
  4. You can have an ordinary user (or a Root or SiteAdmin user, for that matter) with board permissions
  5. You have a raft of null checks to do… And the logic is unclear—the correct position is that if you are logged in, a User record exists, and this logic does express that correctly, but it’s not clear—it doesn’t say that, it does it by bailing out if a user isn’t logged in or if anything is null
  6. In this context, the default position is required in Java, because it cannot be certain all options have been covered by the other cases. This means that if you add another case e.g. ThreadAdmin then the compiler will not tell you there is anything missing, because the default case will pick it up
  7. The ‘BoardAdmin’ null check relies on the order of execution and logical shortcutting, i.e. we know that adminOfBoards.contains will only be executed if adminOfBoards is not null, because the null check precedes it

A traditional Java program has limited tools with which to more strictly model this data and enforce its legality more strongly. By contrast, the F# version:

  • Only gives you access to the User record if you are in the LoggedIn state, covering issues 1 and 2
  • Only gives you access to the set of boards you administer, if your role is board administrator, covering points 3 and 4
  • Has values that cannot be null, so no need to check for that, covering point 7
  • Will warn you if you have not covered all cases exhaustively. You can provide a default branch covering anything not yet covered, but you do not have to. This covers point 6
  • In fact, the logic is sound and complete—no illegal data is representable, and all at compile time—no checks need to be made by the program itself at runtime to ensure correctness

In the Java version, logic in the application is actually held within the values of the data, not their types, by which I mean that having a set of integers at all actually indicates that you should be a board administrator (as well as the role enum). Similarly, having a non-null user should indicate you’re logged in, as well as isLoggedIn being true. Every part of the application that creates or changes these values must keep them consistent, and every part that uses them must either trust that this has happened or check for itself. These are small rules, but the responsibility for maintaining them spreads throughout the codebase.

This is what we mean by tackling the problem at source. If we get rid of the source of the issue, many problems across the codebase disappear. If you get rid of all of the problems at source, then the possibility of making all of the code base far easier to understand, and hence maintain more easily.

Since the mid-1980s, after the ‘structured programming’ movement, significant emphasis has been placed on how best to organise your code, i.e. in classes, with well-defined purposes, but arguably concentrated less on how to make the mechanics of your programs better, and functional programming considers a lot of this, including:

  • How best to define our pieces of data such that we can both accurately represent the domain we’re working in and have our program make decisions based on them?
  • What should an instruction to the computer look like? What qualities should it have?
  • How best to operate on lists of data? Typically, we loop over them and often change the contents in-place—is this the best approach?
  • How to write an application at a higher level of abstraction than is typically the case. An example of this higher level of abstraction in mainstream programming is the 'foreach' command, which has become common in recent decades—so instead of writing the iterator yourself (e.g. in C: for (i=0; i<list.length; i++) etc.) the foreach command does that for you, so you don't get it wrong

We will look at the building blocks of programs: data, instructions, procedures and functions from the ground up to assess what's good and what's not good about them in mainstream programming languages and what can be done to improve them. If those constituent parts of programming languages are improved, much like with the null example, our programs overall will become considerably simpler and hence, easier to understand and maintain.

The ‘modern Java’ version of the program above I referred to does exist and covers some aspects of the guarantees that the F# version does; however, to support this, it involves interfaces and inheritance restrictions—in essence, it’s trying to fit something elegant and simple into a pre-existing object-orientation framework, where it really does not need to. The essence of the issue needs neither interfaces nor inheritance to be expressed clearly. This is a common challenge in mainstream languages taking on FP capabilities—it is hard to incorporate them whilst maintaining existing language conventions. The functional versions are as simple as they need to be and no simpler.

Tackling some myths

A different paradigm?

First of all, is it a different paradigm? It’s often stated that it is. But is it really something so alien? Well, the odd thing is that whilst it does come from a quite different place, but once familiar with it, you may notice many more similarities than differences.

FP being described as a different paradigm in part comes from the fact that decades ago, it did look really very different from mainstream programming, but because quite a number of programming language capabilities (e.g. garbage collection) have made their way into mainstream languages, there are fewer differences.

Languages such as Python, Java, C#, Javascript etc. are sometimes described as imperative languages. They share a lot in common in terms of how they work, but why? Why do they work the way that they do? It turns out you have to look at how the machine that is the computer works at its lowest level to make sense of this. At its essence, a computer does the following:

  • Takes something from main memory and put it in a register; maybe more than once
  • Do something to the values now in registers (e.g. add two of them together)—this might put a result in another register
  • Put some stuff back from the registers to somewhere in main memory
  • Repeat this until completion

This is all very low-level, but the key idea is that it involves getting stuff from main memory, doing something to it, and then putting it back—changing what’s in main memory. A program is something whose job is to take some inputs and output some form of result, which it does by performing simple operations and repeatedly changing what’s in main memory.

Imperative programming languages layer capabilities on top of this to make things simpler, e.g. in C, you can define a record structure (a ‘struct’) and use this to mask a piece of main memory in order to make it more human-readable and pick the pieces of data out more easily than if it were a simple array of bytes. There’s a heap, which is just a data structure layered on top of main memory by the C runtime in order to make it easier to allocate and deallocate memory and get access to it, instead of doing some arithmetic on your calculator to figure out what exact memory location to put things into. The C ‘malloc’ function does this—you call it, and it returns you the memory location of the piece of memory you asked for. It’s just one step removed from writing the data into a memory location of your own choosing.

Looping and decision making are compiled into branch instructions in assembler. Imperative languages differ in how much abstraction they provide here. C is quite low-level, you’re often just incrementing a pointer across an array, which is a contiguous section of main memory—not much higher level than if you wrote the same thing in assembler, which is consequently, not much higher level than how the computer itself works—it’s just more human readable.

Classes in Java are not that different from structs or records. There are some more sophisticated tools for organising code, but fundamentally, it’s about getting something from main memory, manipulating it, and putting it back.

The common factor here is that imperative programming languages provide a set of tools that take the raw operation of the computer (get stuff from memory, manipulate it; put it back) and abstract it to a greater or lesser degree to make it easier to use—more human readable, but still organised around reading and changing values held in memory.

As I mentioned, in day-to-day use, functional programming languages also have struct-type capabilities, and they have equivalents of loops (a means of performing some kind of operation to a multiple elements of data), so that’s not where the difference really lies. It’s that the design of functional programming languages did not start with the computer itself and how it works. It did not start with taking stuff from main memory, manipulating it, and putting it back in main memory. In fact, the foundations of functional programming predate modern computers entirely.

It starts with someone thinking “How do I compute things?”… For example, imagine we would like a machine (application) that gets me a list of all the employees in an organisation that earn more than £50,000. When this was first considered, it wasn’t done thinking about how one would use a computer to do that, because the computer (or more technically, the modern, stored-program computer) would not exist for another ten years or so.

So it’s turned on its head… Rather than: “How do we use this machine, which operates in a certain way, to compute something?”, the thinking was “How do we compute anything?” (at a point at which we did not have a machine with which to do it). The person here was Alonzo Church, and he invented the Lambda calculus in the 1930s, which I am not going to go into here—if you’re interested, there are plenty of things you can read, but essentially, it provides the basis for how, in mathematical notation, computations can take place. The important thing for our purposes is not the detail of this, it’s to understand that this is the basis for functional programming languages—and that the language design is independent of the computer that runs them—it’s not an abstraction of how the machine works.

Independently, denotational semantics was developed in the late 1960s by Dana Scott and Christopher Strachey (a friend of Alan Turing’s). It aimed to provide a precise, mathematical description of what a program means. The ML family of functional programming languages, which include F#, OCaml, are based on these precise definitions. The lineage is that denotational semantics led to Robin Milner doing work in program ‘proofs’; that is, how do I know this program does what it says it’s going to do? Perhaps more strongly, how can I prove it’s going to do what it says it will. Tools were built to support those proofs. This required expressing a program’s rules precisely enough for a computer to be able to check them—work that helped shape the rules the ML programming language could enforce automatically.

Lambda calculus and denotational semantics are all quite heavy and mathematical, but a user of these languages need not know any of it. It was used to arrive at an elegant solution to a programmer’s practical problems; it’s not something the programmer needs to know themselves unless they happen to be interested. Garbage collection is a good example—a lot of thinking went into making it work, and work efficiently, but you don’t need to know that to benefit from it. The practical upshot of the work on ML in the 1970s is that the language is able to enforce many more rules than traditional programming languages tend to, which means the programmer no longer needs to remember and check them themselves.

So what actually is it?

There is no clear definition. In part because techniques and approaches from functional programming languages have slowly migrated to mainstream languages over the years, so what might once have been the domain of FP exclusively, now is not, like the aforementioned garbage collection. Nowadays we might say that functional programming languages:

Emphasise immutability—not changing something’s value once you’ve set it, so instead of ‘overwriting’ a value with a newly computer version (e.g. i = i + 1), you could compute a new value (e.g. x = i + 1). If you come from a mainstream programming language background, this is so common that you will most likely wonder why changing the value of something is an issue, but the larger your program gets, when values are never changed, it becomes far easier to work out what’s going on—it feels like you are always ‘moving forwards’—you don't have to keep a record in your head of all the variables that are changing. A practical upshot is that you find you need to use a debugger less, because it’s much easier to make sense of what’s going on without it. Think about how you tend to use a debugger—you often use it to inspect the values of variables changing as you move through the program. Because they do not change, this is no longer necessary

The functional languages we are exploring here (Lisp is somewhat different in this regard) provide sophisticated mechanisms for defining data structures that come with strict guarantees. Traditional programming languages provide a way of saying that a data structure has X, and Y, and Z associated with it, so for example, a user in our internet message board may have an ID, a user name and a nickname. It has all of the above. What they usually do not provide is an analogous mechanism to say that you want X or Y.

The UserState record described above is an example of this. A user’s state is either LoggedOut, or it’s LoggedIn. And if the user is LoggedIn, you also have an associated User record. None of these values can be null. You can guarantee they exist, so you never have to check for null. If you have a UserState record, you can guarantee it will be LoggedOut or LoggedIn, and if it’s LoggedIn, you can guarantee that there will be a non-null User record that is carried along with it.

Type definitions can also refer to themselves, which can elegantly describe naturally recursing structures such as trees, e.g. this binary tree:

type Tree =
    | Empty
    | Node of  {| value: int
                  left: Tree
                  right: Tree |}
Enter fullscreen mode Exit fullscreen mode

Note that the left and right fields of the node refer to Trees. What this is saying is that a Tree can be either Empty or a Node, and if it’s a node, it has a guaranteed int value, and a left and right branch (‘{|…|}’ being syntax for an anonymous record in F#). Each branch is a Tree, so each branch can be Empty, or it can be a Node and so on.

This data structure also demonstrates the both ‘and’ and ‘or’ capabilities of what are referred to as algebraic data types—a tree can be either Empty, or a Node, and if it’s a Node it contains a ‘value’ and a ‘left’ and a ‘right’. The combination of the two can be extremely powerful, as it substitutes for what null is often used for in mainstream programming, for example: “if it’s in this branch, then X is null, because I know it’s not valid”—rules such as these are in the programmers’ heads—algebraic data types allow you to encode them in the program itself.

Emphasise expressions over *statements*. We will explore this distinction more later, but expressions produce values that other expressions can use. This may initially seem like one of the more boring aspects of FP, but it leads to an interesting question: if a statement (or procedure, or subroutine) returns no useful value, how does it accomplish anything? Its useful work must consist of changing something elsewhere—perhaps a variable outside of its scope, a file, or what appears on the screen. In other words, it must modify the state of the system in some sense. Returning a value and changing state are different ways for a computation to achieve something, and this distinction will become increasingly important as we explore functional programming.

Treat functions similarly to data and expressions—they may be substituted for one another. Functions can be treated as data and passed around as though they are data. Initially, this sounds confusing, but you may be familiar with function such as map, which exist in various different forms in more modern dialects of mainstream programming languages. Map is a function that does something to every element in a passed collection of items—the something that it does is defined by the function that you pass it. For example:

[1; 2; 3; 4; 5]
|> List.map (fun x -> x * 2)
Enter fullscreen mode Exit fullscreen mode

Results in a new list: [2; 4; 6; 8; 10]. Here we are taking the input list and ‘piping’ it into the map function. What this does, is to pass it as the last parameter of the List.map function, but in a way that neatly allows for multiple transformations to happen—for example:

[1; 2; 3; 4; 5]
|> List.map (fun x -> x * 2)
|> List.filter (fun x -> x > 5)
Enter fullscreen mode Exit fullscreen mode

With this expression returning: [6; 8; 10], because we first multiple each element in the list by 2, and then filter to only allow through items > 5.

The appeal of functional programming for me has been:

  • Programming, but with the bad bits removed
  • A feeling that you’re writing logic that ‘in the small’, feels complete and true, and as you build your program, continues to feel that way as each component clicks neatly into the next, building into something larger
  • A feeling that the people who invented this stuff really properly thought it through, rigorously, because everything just feels like it clicks together so naturally—it’s what made me finally start using this stuff for practical applications after years of hoping that it would move into the mainstream—understanding that the syntax is part of it, and it’s not just as simple as bolting it on to existing (e.g.) C-style languages
  • Having to keep less in my head, as I know that these rules have been embedded into the application I’m building, and each time I do that, I know it will keep me right
  • Passes a test I’ve had in my head for over forty years—if I can read code I wrote over six months ago and make sense of it, then it was good code

Next time, we will explore the building blocks and consider how they work within a traditional, imperative programming context and how they differ within functional programming.

Top comments (0)