-
Notifications
You must be signed in to change notification settings - Fork 25
Usage Tips
DISCLAIMER: Always use source control and have a clean build with all changes committed and pushed before trying anything new. Good test coverage is also highly recommended.
The following declarations require StaticImports:
static import org.organicdesign.fp.StaticImports.*
// Obsolete
static final Map<String,String> SHORT_NAME_HASH;
static {
Map<String,String> m = new HashMap<>();
m.put(SERVER_NAME_DEMO, "demo");
m.put(SERVER_NAME_UAT, "uat");
m.put(SERVER_NAME_INTEGRATION, "integration");
m.put(SERVER_NAME_DEV, "dev");
SHORT_NAME_HASH = Collections.unmodifiableMap(m);
}
// Preferred
static final ImMap<String,String> SHORT_NAME_HASH =
map(tup(SERVER_NAME_DEMO, "demo"),
tup(SERVER_NAME_UAT, "uat"),
tup(SERVER_NAME_INTEGRATION, "integration"),
tup(SERVER_NAME_DEV, "dev"));
// Obsolete
Set<String> validModes = Collections.unmodifiableSet(
new HashSet<>(Arrays.asList(MODE_PREADD, MODE_ADD,
MODE_PREUPDATE, MODE_UPDATE)));
// Preferred
ImSet<String> validModes = set(MODE_PREADD, MODE_ADD,
MODE_PREUPDATE, MODE_UPDATE);
// Obsolete
List<String> timeZones =
Collections.unmodifiableList(Arrays.asList(
"Africa/Cairo",
"Africa/Johannesburg",
"America/Anchorage"));
// Preferred
ImList<String> timeZones =
vec("Africa/Cairo",
"Africa/Johannesburg",
"America/Anchorage");
Each of the above works no matter how big your lists, sets, or maps are. They work on SortedSet and SortedMap as well. If you try sortedSet(...)
on something that doesn't implement Comparable, you'll have to supply a Comparator.
The most efficient way to build a Paguro transform from a mutable Java.util collection is to wrap it with
static import org.organicdesign.fp.StaticImports.*
xform(someCollection)
This makes a lightweight transformation builder using the original mutable collection as a data source. There are no extra layers to slow things down.
This is a great way to simplify your code and prevent bugs at the same time. For instance this
Set<Company> activeCompanies = new HashSet<>();
for (User u : someUsers) {
if (!u.isDeleted()) {
activeCompanies.add(u.getCompany());
}
}
Becomes:
ImSet<Company> activeCompanies = xform(someUsers)
.filter(u -> !u.isDeleted())
.map(u -> u.getCompany())
.toImSet();
Did you know that all Java Enums have a values() method that returns an array of the values of that enum in order? I often have a code-name field for the items in an enum, with a conversion function to convert from the code to the Enum value like so:
public enum ColorVal {
RED('R'),
GREEN('G'),
BLUE('B');
private final Character ch;
ColorVal(Character c) { ch = c; }
public Character ch() { return ch; }
// This is a lot of code to make a map!
static final Map<Character,ColorVal> charMap;
static {
Map<Character,ColorVal> tempMap = new HashMap<>();
for (ColorVal v : values()) {
tempMap.put(v.ch(), v);
}
charMap = Collections.unmodifiableMap(tempMap);
}
public static ColorVal fromChar(Character c) { return charMap.get(c); }
}
To make the map, we think about at least 5 items:
- Map interface vs. HashMap implementation
- Static initializer blocks
- A for-loop
- The method to add an item to the map
- Unmodifiable collections which are filled with methods that throw runtime exceptions. If you don't use an unmodifiable collection, then you must make it private to prevent accidental modification-in-place. Even private, a new coder might be tempted to add just one more item to the map within this class file... Far better to always use Unmodifiable or Immutable collections.
Is it better with Java 8 Streams?
static final Map<Character,ColorVal> charMap = Collections.unmodifiableMap(
Stream.of(values())
.collect(Collectors.toMap(ColorVal::ch, Function.identity())));
We use fewer lines of code and don't have to use a static initializer block, so that's better. We also don't have to think about Map vs. HashMap. But now we must think about:
- Function references
- The Identity Function
- Streams separate from collections
- Collectors separate from streams and collections
- Unmodifiable collections which are filled with methods that throw runtime exceptions.
Here's Paguro:
static final ImMap<Character,ColorVal> charMapP =
xformArray(values())
.toImMap(v -> tup(v.ch(), v));
Here, we must think about:
-
xformArray()
a transducer to wrap thevalues()
function (which returns an array) for Paguro stream processing. - Producing an ImMap (Immutable Map)
- A lambda that says, "for each value, create a pair of Character to Value" which echoes the generic type signature, so there's no surprises.
The resulting ImMap is immutable and free to share without worry of mutation-in-place, clear about which methods are deprecated, and easily modifiable by making lightweight copies.
Also, the second bullet point is "Producing a(n Im)Map" which is our goal. I didn't state that in the previous examples, but I think there's some clarity here about what we're doing that gets obscured by details in the other examples.
There is a commented unit test that this example was based on. The unit test takes this example further by extending the map for different uses.
Iterables are good because because they are immutable and safe to share, even across threads. Use them as much as you like.
Iterators are bad because they are single-shot, mutable, and not at all concurrency friendly. Don't use them directly unless you are developing your own collections or transformation API. Paguro has to support Iterators for compatibility with existing Java collections and relies on them internally for speed. In short: Paguro uses Iterators so you don't have to.
- Iterable is desirable. Iterator is a devastator.
- I'm able to avoid the gator.
In general, Paguro lets you transform data without exposing any mutation or side effects. But sometimes you need to mutate in place. On those rare occasions, you may be tempted to try something like the following:
int counter = 0;
Function1<Integer,String> f = i -> {
// ERROR: Variable used in lambda expression should be effectively final
counter++;
return ordinal(i);
});
counter; // 0
f.apply(3); // "3rd"
counter; // 1
f.apply(2); // "2nd"
counter; // 2
f.apply(1); // "1st"
counter // 3
The best work-around for this Java 8 limitation is to use java.util.concurrent.atomic.Atomic...
:
final AtomicInteger counter = new AtomicInteger(0);
Function1<Integer,String> f = i -> {
counter.getAndIncrement();
return ordinal(i);
});
counter.get(); // 0
f.apply(3); // "3rd"
counter.get(); // 1
f.apply(2); // "2nd"
counter.get(); // 2
f.apply(1); // "1st"
counter.get(); // 3
AtomicInteger and AtomicReference are thread-safe, performant, and very well designed. Use them when you come across this limitation.
The above code example was taken from Function1Test.testMemoize() where we needed to count how many times a function was called to prove that it was being memoized.