WTFunctional: Be Declarative
Functional programming is one of the most important developments in programming, but one that has been understandably slow to be adopted and understood by many programmers and tech artists. Over a few posts, I’m going to try to go into the how and why of using a more functional style in your daily programming activities.
First up is demonstrating that functional programming is declarative: it makes your code more expressive and optimized.
Most programmers are used to seeing this:
list = [] for i = 0 to 10 do if i % 2 == 0: list.append(i) //list is now [0,2,4,6,8,10]
Less familiar would be:
list = range(0, 10).filter(lambda i: i % 2 == 0)
The first focuses on the how: increment i from 0 to 10, and append every even item and 0 to a list. This is an imperative style. The second focuses on the what: for each item from 0 to 10, select all even items. This is a declarative style, which is an aspect of functional programming. In this trivial case, the difference is, well, trivial. But the key differences are:
- The declarative style does not specify the enumeration mechanism- it uses the ‘range’ function, rather than incrementing explicitly (as a regular foreach loop does).
- The declarative style does not specify the filtering mechanism- it uses a ‘filter’ function, rather than an explicit ‘if’ statement.
- The declarative style does not specify the storage mechanism- it usually just returns any type that can be enumerated/iterated over, not a concrete type like a list/array/etc.
These differences create three key benefits:
- The abstracted enumeration mechanism means the enumeration mechanism can be optimized, and doesn’t have to be considered by the user.
- The abstracted filtering means the filtering can be optimized because its implementation is hidden from the user, and its intention is more explicit- this is the declarative part of it. We’ll see how to read a more complex statement next.
- The abstracted storage mechanism grows out of the other two abstractions- there may not be a storage mechanism at all, but possibly just generators– it really depends on what is expedient for the statement.
Let’s try out a more concrete example. In this case, we’ll be doing some complex enumeration- grouping, sorting, and projejcting. We want to get a collection of MyObject from active table rows that are ordered by date and then by ID.
dateAndItemsMap = dict() for row in myTable.rows: if row.isActive: if row.date not in dateAndItemsMap: dateAndItemsMap[row.date] = list() dateAndItemsMap[row.date].append(new MyObject(row)) sortedDates = dateAndItemsMap.values() sortedDates.sort() itemsSortedByDateThenId = list() for date in sortedDates: items = dateAndItemsMap[date] items.sort(lamba obj: obj.id) itemsSortedByDateThenId.extend(items)
Wow, that’s a lot of code! And not at all clear when reading it. Let’s read it: Create a dictionary, and for each row, if it is active, make sure the map has a list for the row’s date, and append a new MyObject to the list at row.date in the map. Then sort the keys, then iterate over the sorted keys, get the sorted list value, and keep extending the result list. That’s a mouthful, and I think that was pretty brief.
Let’s compare this to the declarative style:
myTable.rows.filter(lambda r: r.isActive).select(lambda r: MyObject(r)).order_by(lambda o: o.date).then_by(lambda o: o.id)
One line? One stinking line? Let’s read it: For each row that is active, select a new MyObject, and order those by dates, and then by id. Notice a) how the explanation expresses what you want, not how you want to get it, and b) the explanation reads very similar to the code.
This is why declarative programming rocks, right now. It is worth its weight in gold to learn how to use LINQ in C#, itertools in python, or whatever declarative querying mechanism your language hopefully has. Your code will become infinitely clearer.
The reason to be declarative will be even more awesome in the future, is when we can ‘prove’ software to be side-effect free (pure), and the compiler or runtime can automatically parallelize it and optimize it. This is one reason languages like SQL have been so effective- the software/hardware can actually reorder or adjust your query to optimize it, and those algorithms or optimizations can change because the language itself has no notion of how the algorithms for JOIN, GROUP_BY, etc. are implemented.
That makes sense, I hope, and it is just one benefit of learning about functional programming. Next up will probably be closures.