Gulf of execution
In his book The design of everyday things, Norman introduced the terms gulf of evaluation
and gulf of execution
. The latter is the difference between the intentions of the user and what the system allows them to do or how well the system supports those actions.
This describes perfectly my experience as I moved from Golang to Java.
I find myself asking questions like:
- What is the least painful way of iterating over a map?
- How do I make lazy functions?
- How do I run something in a different thread?
- How to get the path of folder and files?
This post documents my learning through my struggle.
Ways of iterating over the map
It really depends on whether you need the keys, the values or both. In Golang, there would be one way of iterating over maps
for k, v := range xs { // drop the variable k or v if only one of them is necessary
...
}
However, there seems to be so many ways of doing it in Java
// Modern way of doing it
for (Entry<K, V> e: m.entrySet()) {
f(e.getKey(), e.getValue());
}
for (V value: m.values()) {}
for (K key: m.keySet()) {}
// The safe way if you want to remove things mid-iteration
// For older version of java
Iterator it = m.entrySet().iterator();
for (it.hasNext()) {
Map.Entry p = (Map.Entry)it.next();
...
it.remove(); // Avoids concurrent modification exception
}
It seems like a gotcha in Java to not be able to do this
package Map;
import java.util.HashMap;
import java.util.Map;
public class MyMap {
public static void main(String[] args) {
Map<String, Integer> m = new HashMap<>();
m.put("a", 1);
m.put("b", 2);
m.put("c", 3);
m.put("d", 4);
for (Map.Entry<String, Integer> e : m.entrySet()) {
System.out.println(e.getKey());
if (e.getValue() %2 == 0) {
m.remove(e.getKey());
}
}
}
}
package main
import (
"fmt"
)
func main() {
m := map[string]int{
"a": 1,
"b": 2,
"c": 3,
"d": 4,
}
for k, v := range m {
if v%2 == 0 {
delete(m, k)
}
}
fmt.Printf("%v", m)
}
Higher order functions
One thing I really like about programming languages is the support for higher order functions. That is being able to treat functions as a value and pass them around.
Java supports this through the Function<A,B>
class, which was a fairly new feature.
A common concept is delayed evaluation. Where the function is evaluated only at call site instead of at definition.
Instead of f: A
, we want f: () => A
In golang, there is one straight forward way to do it.
func () A {...}
In Java, there are several ways, depending on what you want to do.
For example, if you wanted a lazy function, it would be rather awkward to define it as Function<Void, A>
.
static Function<Void, Integer> f = () -> 9; // Will not compile
static Function<Void, Integer> f = (Void v) -> 9; // This is ok
...
f.apply(null); // Ugly but works
The correct class is Supplier<A>
. The adage goes as follows:
- Use
Supplier<A>
if the method takes no argument. - Use
Consumer<A>
if the result returns value. - Use
Runnable
if both method and results are void. - Use
Predicate<T>
is the same asFunction<T,Boolean>
. c.f.list.removeIf()
- UnaryOperator is the same as
Function<T,T>
. c.f.list.replaceAll()
The interesting thing to note here is how Runnable, typically grouped with Callable() => ()
.
Concurrency
Another useful pattern is concurrency. For example, starting a blocking call to wait for user input, or watching for file change. Very seldom should the main thread be blocked.
func main() {
go func() {
doBlocking()
}()
doMainStuff()
}
In java, it is slightly nicer now with lambdas.
public static void main(String[] args) {
// This is one way of doing it
new Thread(() -> {
// Do long stuff
}).start();
// This way of doing it takes care of retrying the task automatically
Executors.newSingleThreadScheduledExecutor().scheduleWithFixedDelay(() -> {
try {
f();
} catch (Exception e) {
System.out.println("Failed, but we'll try again");
System.out.println(LocalTime.now());
}
}, 0, 3, TimeUnit.SECONDS);
// Failed, but we'll try again
// 01:00:03.432528
// Failed, but we'll try again
// 01:00:06.440588400
// Failed, but we'll try again
// 01:00:09.441395400
// Failed, but we'll try again
// 01:00:12.442236700
}
public static void f() throws Exception {
throw new Exception("Foo");
}
Joining paths and getting variables
It is very common to want to pass in run time parameters to control program flow. Typically, this is done via program arguments, flag values or environment variables.
As a motivating example, suppose we want to create a config directory to store files for our program. A home directory is reasonable, but how do we take into account the different user environments?
The JVM injects certain default platform aware properties and this makes the task bearable.
public static void main(String[] args) {
Path p = Paths.get(System.getProperty("user.home"), "code");
System.out.println(p);
}
When I first saw the parameters to starting a Java program, I was daunted by the many (unreadable) flag values such as -Dfoo
. Only when I started playing around with production code did I realize they were essentially passing in system properties. The following snippet is how one would access that value passed in this way.
System.out.println(System.getProperty("foo"));
Conclusion
Programming in Java is less painful now, as long as you fill the void.