These methods provide a nice abstraction for picking elements from an array with filter() , and for modifying the values with map() . The operations compose together nicely, so we can write neat code to process arrays which focuses on intention rather than having to write explicit for-loops.
Making the simple things look simple
There are lots of things we do in Apex that are conceptually simple, but require chunks of boilerplate to achieve. As an experienced developer, you recognise the boilerplate, and translate it to the concept as you read the code. But, what if there’s some detail hidden in the boilerplate – would you spot it? And why waste time and mental energy typing out that boilerplate anyway?
Bringing filter() and map() to Apex saves us from the boilerplate, and highlights the important stuff.
The building block for this technique is the Lazy Iterator class mentioned in a previous post. This class supports filter(), map(), and more. It also does this in an efficient way, so that there are no intermediate lists created while you do your processing. For a deeper discussion on that, hop back to the original post.
Show me the code!
Time to stop making promises and show what I’m talking about. Look at all the things you can do with a list of Contacts…
Get all the values for a given field
Set accountsIds = new nebc.LazySObjectIterator(contacts) .mapValues(new nebc.FieldFromSObject(Contact.AccountId)) .filter(new nebc.IsNotNull()) .toSet(new Set());
This is more concise than a for-loop and gets straight to the heart of what it’s trying to achieve. It also shows how easily we can filter out null values. Note that the final function call could be toList() instead, if that’s what you wanted as output instead.
Copy one field to another
Ever needed to copy a picklist field to a text field so that you can index on it? Or copy a formula field to a normal field so you can roll it up?
new nebc.LazySObjectIterator(contacts) .forEach(new SObjectPutField(Contact.FirstName, new nebc.FieldFromSObject(Contact.LastName)));
The structure expresses our intent without having to write an explicit loop. And this generalises: we could filter first to only modify selected records.
Find based on a field value
Apex’s List has an indexOf() method, but it only allows searching for an exact match. We can be more flexible.
Contact foundContact = (Contact)new nebc.LazySObjectIterator(contacts) .filter(new nebc.IsSObjectFieldEqual(Contact.LastName, 'Doe')) .firstOrDefault(nebc.NoResult.NO_RESULT);
We do the search by filtering to just the Contacts where the LastName is Doe, then returning the first one encountered. If we had used toList() at the end, we could get all matches in a list.
Create a list of Tasks from the Contacts
Suppose we want to create a Task to follow up with each of the Contacts. We can do this as follows:
List tasks = new nebc.LazySObjectIterator(contacts) .mapValues(new nebc.SObjectFromPrototype(new Task(ActivityDate = Date.today().addDays(7))) .put(Task.WhoId, new nebc.FieldFromSObject(Contact.Id)) .put(Task.Subject, new nebc.Add('Follow up with ', new nebc.FieldFromSObject(Contact.LastName))) ) .toList(new List());
The overall process, is to use mapValues() to turn each Contact into a Task.
The mapping function uses the library function SObjectFromPrototype. This function allows you to create a new SObject from a prototype (the one which sets ActivityDate), and then set more fields on it using the list you are currently iterating over. We set the Task.WhoId to the Id of the Contact we’re iterating on. Then, we set the Task.Subject to a complex function using Add. The Add function here adds a constant string to another field pulled from the Contact we’re iterating on.
This shows very clearly how the Task is being put together from three different sources:
- Some data which is the same for all tasks
- Some data copied from the Contact
- Some data derived from the Contact
And it shows how the building blocks in this library can fit together for more complex aims.
Use Map lookups to write to the Contact
Another common scenario is having a Map<Id, SObject> of parent records, which you want to use to update the child records. For example, here we want to copy part of the Billing Address from the parent Account down to the Contact’s Mailing Address.
new nebc.LazySObjectIterator(contacts) .forEach(new nebc.SObjectPutField( Contact.MailingStreet, new nebc.Composition(new nebc.FieldFromSObject(Contact.AccountId), new nebc.GetFrom(accountMap)) .compose(new nebc.FieldFromSObject(Account.BillingStreet))));
Here, the key thing is the Composition function. This takes an arbitrary number of functions, and applies them all in order. So, we’re setting the value of the MailingStreet to the value from the composite function. And the composite is doing the following:
- Get AccountId from the current iteration value
- Get an Account from accountMap using the AccountId above
- Get the BillingStreet from the Account above
How can I use it? What’s next?
At Nebula, we’ve been using this internally for the past few months. It has made some code significantly more concise and easy to read (for those familiar with the library). You can torture most code into fitting into this style, but there are definitely situations where doing so loses clarity so we need to be wary of those.
If you want to try it, you can go to the repository (it’s under MIT license): https://bitbucket.org/nebulaconsulting/nebula-core/src/master/
What do we plan to do with it? I certainly plan to evolve it for internal use. I’d love the Apex team at Salesforce to implement some new features that would make is sleeker (generics for custom types, first-class functions).
The past few months has been a process of identifying useful higher-order building blocks (e.g. SObjectFromPrototype, GetFrom, etc.) which mean you rarely have to write your own custom classes for common use-cases. And we have built out more operations other than just filter() and mapValues(). One example is expand(), which allows you to produce more than one item from each one as you iterate e.g. to flatten a list of lists. Another example is take(), which allows you to grab the first N items from a list.
Documentation in the repository is coming along slowly – we’re at the point where it works for us internally so more documentation is of questionable value. If you do want to have a go with this, you can always ask me directly and if, by chance, that becomes overwhelming then documentation might become higher priority.