Functions or for-loops?
A developer working in Javascript has access to some really convenient functions on Arrays. Functions like filter() and map() can avoid the need to write for-loops. Is this actually a good thing? Can we write Apex this way?
Let’s try it with for-loops
Suppose we want to write a trigger on Contact which runs when a Birthdate is added. The trigger should create a task for the next birthday. We could write this with a conventional for-loop (NB, the code below is using the trigger framework from Salesforce Trigger Handlers Driven by Custom Metadata. Hopefully, the intent is clear without necessarily knowing the trigger framework):
public without sharing class ContactBirthdayTasks implements AfterUpdate { private static final Date TODAY = Date.today(); public void handleAfterUpdate(List<Contact> oldList, List<Contact> newList) { List<Task> happyBirthdayTasks = new List<Task>(); for(Integer i=0; i < newList.size(); i++) { Contact newContact = newList[i]; if(newContact.Birthdate != null && oldList[i].Birthdate == null) { Date activityDate = Date.newInstance(TODAY.year(), newContact.Birthdate.month(), newContact.Birthdate.day()); if(activityDate < TODAY) { activityDate = activityDate.addYears(1); } happyBirthdayTasks.add(new Task( WhoId = newContact.Id, Subject = 'Happy Birthday', ActivityDate = activityDate )); } } insert happyBirthdayTasks; } }
Let’s try it lazy
How does it look if we write it in terms of filter() and map()? (NB, we have to call it mapValues() in Apex because map is a reserved word)
public without sharing class ContactBirthdayTasksLazy implements AfterUpdate { public void handleAfterUpdate(List<Contact> oldList, List<Contact> newList) { insert (List<Task>)new LazyTriggerContextPairIterator(oldList, newList) .filter(new BirthdayChangedFromNull()) .mapValues(new TriggerContextPair.NewRecordFromPair()) .mapValues(new BirthdayTaskFromContact()) .toList(new List<Task>()); } private class BirthdayChangedFromNull extends TriggerContextBooleanFunction { public override Boolean isTrueFor(SObject oldRecord, SObject newRecord) { Contact newContact = (Contact)newRecord; return newContact.Birthdate != null && ((Contact)oldRecord).Birthdate == null; } } private class BirthdayTaskFromContact implements Function { private final Date TODAY = Date.today(); public Object call(Object o) { Contact newContact = (Contact)o; return new Task( WhoId = newContact.Id, Subject = 'Happy Birthday', ActivityDate = getActivityDate(newContact.Birthdate) ); } private Date getActivityDate(Date birthdate) { Date activityDate = Date.newInstance(TODAY.year(), birthdate.month(), birthdate.day()); if(activityDate < TODAY) { activityDate = activityDate.addYears(1); } return activityDate; } } }
This version has many more lines of code (to be fair, many are blank or just braces), but is has split out the responsibilities nicely. First, let’s consider what filter() and mapValues() do:
- filter() picks out selected items from a collection by calling a function on each, to see if it should be included
- mapValues() transforms one collection into another collection of the same size, by calling a given function on each element
Once you are used to reading this sort of code, you can tell what the trigger does by reading handleAfterUpdate() as if it is written in plain english:
From the trigger context, filter to just the old/new pairs where the Birthdate has changed from null; pick the new records from the trigger context; create a birthday Task from each new Contact; finally, turn that into a List of Tasks.
This code satisfies a number of the criteria of clean code (see https://www.amazon.co.uk/dp/0132350882/). Everything in handleAfterUpdate() is done at the same level of abstraction. There is no loop counter getting in the way of reading the high-level intent. Furthermore, each of the components does exactly what it says in the name, and nothing more. Everything has a single responsibility. And since everything has a name and a single responsibility, we don’t need any comments to explain ourselves.
I’m not saying that for-loops are bad, but using filter() and mapValues() is certainly cleaner when they are a good fit for the problem at-hand.
So what’s lazy about all this?
The “lazy” in the title of this article refers to lazy evaluation. It’s a key concept in functional programming languages, but it can also be implemented in other languages. If you’re new to it, I’d highly recommend the following video:
Ironically for a talk about laziness, the delivery in that video is so fast that you might need to take the odd break as you watch it.
Understanding the power of lazy evaluation
We can understand the power of lazy evaluation by considering an eager implementation of filter() and mapValues() . Suppose filter() and mapValue() return lists, then we could implement them as something like this:
public static List<Object> filter(List<Object> toFilter, FilterFunction filterFunction) { List<Object> returnVal = new List<Object>(); for(Integer i=0; i < toFilter.size(); i++) { Object thisElement = toFilter[i]; if(filterFunction.matches(thisElement, i, toFilter)) { returnVal.add(thisElement); } } return returnVal; } public static List<Object> mapValues(List<Object> toMap, Function mappingFunction) { List<Object> returnVal = new List<Object>(); for(Integer i=0; i < toMap.size(); i++) { returnVal.add(mappingFunction.call(toMap[i])); } return returnVal; }
These are eager implementations – as soon as you call filter() or mapValues() , they do that to the entire list. So, what if we run the birthday trigger above on 100 Contacts, 50 of which had a new Birthdate? An implementation like this would iterate 100 times for the filter() , then 50 times for each call to mapValues(). So, it would take a total of 200 iterations, whereas the for-loop implementation would take only 100 iterations.
Lazy evaluation avoids this problem. A lazy implementation of filter() and mapValues() doesn’t return a list, it returns an iterator. And it only starts doing any work when someone starts asking that iterator to provide values. In the birthday trigger, nothing much happens during the filter() and mapValues() calls. Each of those functions just adds another layer around the iterator (see diagram, below). All the work starts inside toList() . toList() is pulling values from the iterator one-by-one. Each of those values passes through two calls to mapValues(), and one call to filter(). Just like the for-loop implementation, the total number of iterations ends up being 100.
So, using a lazy implementation of filter() and mapValues() has similar performance (in terms of O(n)) as a for-loop. It may take a bit more time, but it won’t blow up badly. Beware the eager implementations, though! In particular, notice that the Javascript implementations are eager – they return entire lists. So, they should be used with caution on large amounts of data – especially because their neat appearance belies the performance cost.
Show me the code!
Implementing a framework for these lazy functions in Apex is not all that hard, so I’ve published our code for it on Bitbucket. As mentioned above, it’s all about iterators. The core class is LazyIterator , this uses the Decorator Pattern to add functionality to an iterator. As is normal for this pattern, LazyIterator contains an Iterator and also implements the Iterator interface. It forwards calls to next() and hasNext() to the internal Iterator . LazyIterator then adds the functions we’re interested in: filter(), and mapValues() .
global virtual class LazyIterator implements Iterator<Object> { protected Iterator<Object> iterator; global LazyIterator(Iterator<Object> iterator) { this.iterator = iterator; } global virtual Boolean hasNext() { return iterator.hasNext(); } global virtual Object next() { return iterator.next(); } global Object firstOrDefault(Object defaultValue) { if(hasNext()) { return next(); } else { return defaultValue; } } global virtual List<Object> toList(List<Object> toFill) { List<Object> returnVal = toFill; while(hasNext()) { returnVal.add(next()); } return returnVal; } global LazyIterator filter(BooleanFunction matchingFunction) { return new LazyFilterIterator(this, matchingFunction); } global LazyIterator mapValues(Function mappingFunction) { return new LazyMapIterator(this, mappingFunction); } global void forEach(Function callingFunction) { while(hasNext()) { callingFunction.call(next()); } } }
mapValues() is implemented as a subclass of LazyIterator where calling next() is no longer just delegated. Now, it calls the mappingFunction on the result that it gets from its internal iterator:
public class LazyMapIterator extends LazyIterator { private Function mappingFunction; public LazyMapIterator(Iterator<Object> iterator, Function mappingFunction) { super(iterator); this.mappingFunction = mappingFunction; } public override Object next() { return (Object)mappingFunction.call(iterator.next()); } }
filter() is also implemented with a subclass of LazyIterator . This time, we override both next() and hasNext() . Now the iterator operations test the potential next value against matchingFunction by peeking ahead by one value:
public class LazyFilterIterator extends LazyIterator { private BooleanFunction matchingFunction; private Object peek; private Boolean peekIsValid; public LazyFilterIterator(Iterator<Object> iterator, BooleanFunction matchingFunction) { super(iterator); this.matchingFunction = matchingFunction; this.peekIsValid = false; } private void peek() { if(iterator.hasNext()) { peek = iterator.next(); peekIsValid = true; } else { peekIsValid = false; } } public override Boolean hasNext() { if (!peekIsValid) { peek(); } while(peekIsValid) { if(matchingFunction.isTrueFor(peek)) { return true; } else { peek(); } } return false; } public override Object next() { if(hasNext()) { peekIsValid = false; return peek; } else { throw new NoSuchElementException(); } } }
Conceptually, that’s it. There are some other classes in there to suit particular scenarios e.g. facilitating trigger handlers by iterating over pairs with the new/old records. But these just add convenience to the core idea.
Why didn’t you use Callable?
The Function interface used in these classes looks a bit like the standard Callable interface provided by Salesforce. One of the reasons that Callable exists is that you don’t need any dependencies to use it – it is included in the Salesforce system. At Nebula, Function is included in a common library that we install for all customers anyway, so it is likewise always available.
Also, in my opinion, Callable.call(String action, Map
What about Apex Lambda?
There is an existing Github repo called Apex Lambda which has similar aims. It’s great but, in its initial form, it did not support lazy evaluation. It’s moving in that direction, but is still oriented around Lists rather than Iterators. We also like our own version because some of the interfaces mentioned here are used for other functions in our library.
What about all that casting?
There is some ugliness involved because we can’t write our own templated types in Apex. For example:
private class BirthdayChangedFromNull extends TriggerContextBooleanFunction { public override Boolean isTrueFor(SObject oldRecord, SObject newRecord) { Contact newContact = (Contact)newRecord; ... } }
In this context, we know that the arguments will be Contacts. So it would be great if we could write:
private class BirthdayChangedFromNull extends TriggerContextBooleanFunction<Contact> { public override Boolean isTrueFor(Contact oldRecord, Contact newRecord) { ... } }
Sadly, this is not currently possible. Since there is a new Apex compiler, maybe one day it will be possible!
As usual, if you find this interesting/useful or have questions/discussions, then feel free to contact me aidan@nebulaconsulting.co.uk or @AidanHarding on Twitter.