List comprehension is a method from mathematics to describe a collection of values in a very compact and elegant way. This notation has been implemented by a lot of languages where it primarily is used to construct and populate a list in a single expression instead of population an empty list with a multitude of loops and if-statements. Because of its connections to mathematics and expression based nature list comprehension works incredibly well within functional programming.

An example of list comprehension in Haskell

It does then not come as a surprise than languages like Haskell and F# come with a rich and complete implementation of this technique, but also languages that are popular by data scientists like Python and R offer list comprehension as a language feature. This because it works really nice when programming mathematical formulas that for example are based on matrices or sets.

What is list comprehension?

But enough talk about programming languages, what is list comprehension and how does is work? List comprehension allows you to create your collection with an expression that exists out of three parts: the output expression, the input sets and the predicates that filter the input set.

An example of list comprehension in Python

Here var * 2 is the expression that creates the value in the lists. This expression will be used for every item in the input list. But this list will be filtered by the expression after the if: var % 2 == 0 . For small lists this might just look like some syntax sugar, but with big multidimensional collections this notation will be become a real lifesaver! And list comprehension doesn’t just work with lists, it can also be used to quickly create a map from key-value pairs.

But this is not everything. With the fancy collection syntax comes also a rich standard library to create collections from ranges, repeating sequences, characters, strings, merged collections and more. The combinations of those two things makes lists comprehension a powerful tool in a modern (functional) programming language.

Just some of the ways to create a populated list in Haskell

In this article I will walk you through a list comprehension library that I created for TypeScript. It allows you to use regular TypeScript code while still enjoying all the advantages of list comprehension. I will mainly focus on how the api guarantees type safety when using multiple input sets to create multidimensional collections. The result will look like shown below:

Designing the interface

We will start of by setting up the type for the array function that will do the actual list comprehension. For this I will be using a so-called callable interface because they do allow us to use all the function related features the type system has to offer. For a list comprehension expression we need three things: the input sets, the function that creates the items and the predicates that filter the entries of the inputs.

I changed the standard order because if we provide the input collection first TypeScript can use that to infer the generic type i for the arguments of the out function of which it can inference the generic o . With this little change we make it easier to use the library because you won’t have to explicitly provide the generic type arguments. For the predicates I used a rest-argument so we can define any amount of predicates we want (or none).

Working with multiple input sources

List comprehension becomes really powerful when used on multiple inputs. I we want to allow multiple inputs in our library we have to get a bit creative with the definition of the external API. I’m going to use overloads of our array function to create definitions of comprehension with two or more inputs. In this article I will limit all the code samples to three inputs to keep it somewhat short, but the library goes up to ten.

The additional overloads of our function

With those overload you can start of with giving the function all the data sources you want and after that it will know how many arguments the out and predicate functions are going to get. While this works like a charm with regards to the type inference, it is a bit of a pain to write a single function that implements this interface. Remember, overloads in TypeScript only exists on compile time! On runtime we have just one implementation that needs to perform runtime type checks to figure out which overload we current are.

To implement this I have decided to threat all the arguments as an untyped array. In this array we can search for the first index of a function. We know that this is the out function. Based on this information we can split the args array into an array with inputs, the out function and an array with predicates. This is a bit of a hassle, but still completely save because our interface grantees the correctest of the assumptions we are making.

Image having to figure out what this function expect as arguments without types…

But how do we combine the inputs sources to sets of arguments for the out function? There are two ways we can do this: zip the inputs or perform a cross-join. If we would zip the inputs we would combine the first items of all the inputs together, the second items of all the arrays together and so on. If we cross-join we combine all elements of the first array with all the elements of the second arrays, the second item of the first array with all the elements of the second array and so on. So a cross-join creates a lot more pairs then a zip.

zip vs. cross-join with [ 1, 2, 3 ] and [ 4, 5, 6 ] as inputs

I have decided to use the cross-join technique because it is the most common one to use with list comprehension. It works the same as a nested for-loop when compared to imperative programming. If you want to use the zip technique you can use the zip function from the library to first zip the inputs and provide the result as a single input.

The code used to create a cross-join out of an array of arrays that respects the ordering of the input

More than arrays

At the beginning of this article I already mentioned that list comprehension is not just limited to lists. In the library there is also Set and Map comprehension for easy construction of maps and sets. Map comprehension is bit different from array and set comprehension because your out function has to return key-value pairs which will be used to construct the map. This means that duplicate keys will override each other.

Creating the right input

The usability of list comprehension is very dependent on how flexible the language is in creating the required input collections. Most languages that have list comprehension built-in as a language feature also come with exhaustive features for generating ranges, repeating sequences and other possible list definitions.

Those features are the second thing the library offers. There are a multitude of functions that create populated arrays that can be used. The first two are the well-known range and repeat:

When talking about the difference between cross-joins and zipping I promised that there is a zip function, so here it is. This is also function that uses overloads to make sure the tuples are properly typed and TypeScript knows which index has which type instead of merging the types and making it an array of a single type:

The last one is the character range. This function uses a map with the alphabet to create ranges of characters based on their order in the alphabet:

With those functions and the correct predicates you can make optimal use of list comprehension for creating arrays, maps and sets in TypeScript.

Practical examples

I will finish of with a two practical examples to show you how list comprehension can be used in practice. I will show one example that uses the cross-join technique and one that uses the zip technique. Both examples do show that it is important that the comprehension code respects the ordering of the inputs.

Lets say we want to group a list of titles with the letter they start with, sorted on there position in the alphabet. This could be used on an overview page that shows a large list of links in a clear way.

Creating a matrix of coordinates would be a classic example of when to use a nested for-loop. But with list comprehension can use range and zip functions to do this in a completely functional way:

Conclusion