https://images.prismic.io/tripshot-new/f2129683-ab0f-4083-b307-a506174e3e7c_engineering_blog-post-thumbnail.png?ixlib=gatsbyFP&auto=compress%2Cformat&fit=max

Pattern Synonyms for Non-Empty Lists

Alleviate the pain of awkward code syntax

At TripShot, we love the type safety of Haskell, so we frequently use the NonEmpty list type. But we found it awkward to construct short, literal lists with it. So, we introduced our own pattern synonyms, NE1 .. NE4, that make creating literal non-empty lists easier, and we get handy pattern-matching as well.

Non-empty lists

[Data.List.NonEmpty] is a datatype that represents a list that is guaranteed not to be empty. It’s useful in cases where you know you must have at least one of something.

For example, we use NonEmpty to represent lists of filters to apply to database queries. We have a function with a signature something like this:

1getRidesBy :: Database m => NonEmpty RideFilter -> m [Ride]

We don’t want to allow an empty list of filters, since that could significantly impair performance by retrieving the entire table. Also, we don’t have to be careful to avoid creating an empty WHERE clause, which is invalid SQL syntax.

Since we are writing specific queries in the code requiring certain filters, we need to supply literal lists of filters. There are a couple of ways supplied by the NonEmpty module to create these. Suppose we want to apply two filters. We can construct a NonEmpty by writing the first filter, the :| operator, and the second filter embedded in a list:

1UserFilter userId :| [StopFilter stopId]

This is awkward to write and not uniform: the elements are treated differently in terms of how they are written, even though there is nothing special about the first one. If we want to remove the first one from the list, we have to rewrite it as StopFilter stopId :| [] or singleton (StopFilter stopId).

Another way to write it is using fromList:

1import qualified Data.List.NonEmpty as NE 2NE.fromList [UserFilter userId, StopFilter stopId]

That’s nicely uniform: you can easily add or remove an element in the list without affecting how the others are written. But, it returns a Maybe (NonEmpty RideFilter). Even though we know it’s not empty, we’re forced to call a partial function such as fromJustNote to handle the Nothing case!

Pattern synonyms to the rescue

The problem is that NonEmpty lists do not get their own syntax, like lists or tuples. But function calls have fixed numbers of arguments and a nicely uniform syntax. Taking a cue from functions named with a number depending on the argument count, such as zipWith3, we could make functions nonEmpty1, nonEmpty2, etc., so our filter example would become:

1nonEmpty2 (UserFilter userId) (StopFilter stopId)

However, we decided it was tidier to define pattern synonyms:

1{-# LANGUAGE PatternSynonyms #-} 2pattern NE1 :: a -> NonEmpty a 3pattern NE1 x = x :| [] 4pattern NE2 :: a -> a -> NonEmpty a 5pattern NE2 x y = x :| [y] 6pattern NE3 :: a -> a -> a -> NonEmpty a 7pattern NE3 x y z = x :| [y, z] 8-- ... however many you need ...

Now we can write the filter list as NE2 (UserFilter userId) (StopFilter stopId).

These synonyms:

  • Are nicely terse, thus easy to write
  • Suggest the NonEmpty type because NE is often used as an abbreviation for NonEmpty
  • Live in the namespace of patterns and constructors, which is less cluttered than that of variables
  • Suggest you are building a data structure since they look like constructors
  • Are bidirectional, so we can pattern match on them as well

Conclusion

We love NonEmpty, but we felt a pain point in our construction of literal non-empty lists. Pattern synonyms turned out to be a great solution for us. They can help alleviate the pain of awkward syntax. We are already using these pattern synonyms 64 times in our code base.