Chip's Tips for Developers

Contains coding, but not narcotic.

Mapping and filtering Arraylists with the ls class for Synergy/DE

August 4th, 2009 1:12:06 pm pst by Sterling Camden

The next logical addition to ls, my ArrayList extension class for Synergy/DE, would be methods for mapping and filtering elements.  The Synergy/DE statement FOREACH works just fine on an ls.  That’s sufficient for doing what Ruby’s Array#each does, since the return value from Array#each is just the array itself.  But when it comes to mapping, filtering, or counting elements that meet a test, we’d like to be able to return a new ls or an integer – and FOREACH does not return a value.  A more functional syntax is called for, but then we have the problem of how to provide the code that does the testing or mapping, since Synergy/DE does not support lambdas (anonymous, inline functions).

As with the methods for sorting, the answer is to take a functor as an argument:  a member of a class that wraps a specific type of function.  In functional languages, this nounification of a verb is completely unnecessary because functions are first-class objects, but it’s the best we can do in an OOP model that derives utlimately from Java.

Thus, we introduce two new types of functors:

  • MapObject – maps an object to another object.  This abstract class has one method that must be overridden in derived classes:  map.  It takes an object as its argument, and returns an object.  Derived classes decide how to map one object to another.
  • MapBoolean – maps an object to a boolean result.  This abstract class has one method that must be overridden in derived classes: test.  It takes an object as its argument, and returns a boolean.  Derived classes decide whether the object meets a specific test or not.

I could have derived MapBoolean from MapObject, but decided against it.  If MapBoolean.test were an override for MapObject.map, then it would have to return an object instead of a primitive boolean.  Sure, I could have boxed that, but what would I gain from all that boxing and casting?  I’d gain the ability to map objects to an array of ones and zeros – but that can be performed anyway using MapIf below.  I’m getting ahead of myself, though.

Given these two classes of mapping functors, we can add the following methods to ls:

The first (map and map$) take a MapObject as an argument, and apply its map method to each element.  The others (removeif, removeif$, and countif) take a MapBoolean as an argument, and apply its test method to each element.

Now I hear you sighing, “You mean I have to declare a class just to write my mapping/filtering code?”  Actually, it turns out that you can do a surprising number of mapping and filtering operations using only the provided derived classes and operator overloads.

For instance, if you want to count all the elements whose numeric value is greater than 500, use:

myls.countif(new CompareVar() > 500)

This says, use the comparison logic in CompareVar (which uses Var’s built-in comparisons) to compare each object as greater than 500.  How does this work?

I added comparison operator overloads to the Compare class that return a derived class of MapBoolean that invokes the comparison on each object against the right-hand term.  If the right-hand term is primitive (as in the example above), it is auto-boxed as a Var.  I also added Boolean operator overloads for MapBoolean, so you can combine tests:

myls.removeif((new CompareString() == “fred”) || (new CompareString() == “wilma”))

This snuffs both the Flintstones.  You could reuse the same CompareString object in both tests, but then you’d need a variable for it:

begin
  data cmp, @Compare, new CompareString()
  myls.removeif$(cmp == “fred” || cmp == “wilma”)
end

To enable similar logic for the map and map$ methods, I created a derived class of MapObject called MapIf:

myls.map$(new MapIf(new CompareVar() > limit, limit))

Any element that’s greater than limit will be set to limit.   MapIf can also take an else value:

myls.map(new MapIf(new MapNull(), 0, 1))

Produces a list that contains a 0 for every null element, and a 1 for every non-null element.

If the result of a MapIf is also a MapObject, we recurse.  This allows for nested ifs:


data cmp, @Compare, new CompareVarInt()
myls.map(new MapIf(cmp < 5,
                   new MapIf(cmp > 2, “3 thru 5”, “less than 3”),
                   new MapIf(cmp < 10, “6 thru 9”, “greater than 9”)))

as well as conditional mapping:

myls.map(new MapIf(new MapNonNull(), new MapAlpha()))

This example avoids MapAlpha’s translation of nulls to “” – leaving them null.

Of course, this all begs for two more extensions:  a way to lazily invoke the mathematical operators, and support for Regular expressions.  Another useful mapper might be one to generate an alist by mapping each object to a key => value pair.  I’ll save those for later.

UPDATE 2009-08-05: I went ahead and added two new MapObject functor classes:  MapKey (generates an alist from a list of objects, using another MapObject to create the key) and MapAssoc (maps keys to associated objects in a separate alist).

Posted in SynergyDE | 1 Comment » RSS 2.0 | Sphere it!

One Response to “Mapping and filtering Arraylists with the ls class for Synergy/DE”

  1. [...] mentioned in my last post on my ls class for Synergy/DE that it would be nice to include the mathematical operators in my support for lazy evaluation of [...]

Leave a Reply

Better Tag Cloud