Using monadic for syntax with option types

In the past, I’ve introduced Option types into Java projects using the Guava library. Smart, seasoned programmers who hadn’t really tried much functional programming asked me to explain why “Optional[Double]” is better than Double, since we could just use null to represent the empty state. I feel like my answers at the time needed improvement, so the following is my attempt to demonstrate what’s terrible about nulls in Java, and how Scala improved upon it.

Suppose we want to process a text file that has 3 numbers, separated by a delimiter. We then want to perform some arithmetic operations on the values.

2.3,56.0,24.0
35.66,34.33,74.0
...

A typical approach that one might take in Java would be:

  • read line by line
  • split the string with delimiter
  • parse each number
  • use a custom coded conditional statement to avoid null pointer exceptions
  • for good measure, try-catch and ignore bad values
  • finally perform our math
  • return the values

It ends up looking something like this:

import java.io.*;
public class AvgJava {
    public Double parseDouble(String d) {
try {
return Double.parseDouble(d) ;
} catch (Exception e) {
return null;
}
}
    public Double avg(String line) {
String[] tokens = line.split(",");
if (tokens.length == 3) {
Double d1 = parseDouble(tokens[0]);
Double d2 = parseDouble(tokens[1]);
Double d3 = parseDouble(tokens[2]);
if (d1 != null && d2 != null && d3 != null) {
return (d1+d2+d3) / 3;
}
}
return null;
}
    public static void main(String[] args) {
AvgJava avgJava = new AvgJava();
try {
FileInputStream fstream = new FileInputStream(args[1]);
BufferedReader br = new BufferedReader(new InputStreamReader(fstream));
String strLine;
            while ((strLine = br.readLine()) != null)   {
Double av = avgJava.avg(strLine);
if (av != null) {
System.out.println("average: " + av);
}
}
br.close();
        } catch (IOException e) {
e.printStackTrace();
}
}
}

There’s so much wrong with this picture.

  • Using null to represent anything semantic is bad because its untyped, and because it frequently leads to a NullPointerException.
  • All variables can be null, so its always a lurking danger.
  • tedious conditional logic unrelated to the problem at hand
  • exception handling for program flow leads to all manner of difficulty
  • our code should represent the “empty” case where no number is available, and null does this poorly

Since it was a plain Java project, I struggled to provide a convincing explanation to my colleague. I made all the points above, and I stand by them. But frankly, they’re all somewhat weak without the monadic for syntax available in Scala, (or similarly, monadic do syntax in Haskell).

So here’s an equivalent Scala version:

import java.io.File
import scala.io.Source
import scala.util.Try
object average {
  def avg(line:String) : Option[Double] = {
val numstr = line.split(",")
for ( //failure to index or parse results in "None"
d1 <- Try(numstr(0).toDouble).toOption;
d2 <- Try(numstr(1).toDouble).toOption;
d3 <- Try(numstr(2).toDouble).toOption
) yield (d1+d2+d3) / 3
}
  def main(args: Array[String]): Unit = {
Source.fromFile(new File(args(1)))
.getLines
.flatMap(avg) //ignores "None" values
.foreach( av => println("average: " + av) )
}
}

Firstly, the resulting program is much briefer, which is a virtue in its own right. But its also clearer. All the same cases are handled: lines with invalid strings and missing values will be ignored. Only complete valid lines will be processed. Just as important, the intent of the code now shines through.

My original point about intent still stands: Option[Double] clearly indicates to the reader that we know we won’t always get a value. Sometimes, people will make typos. Sometimes files will have corrupted lines. We’re prepared to handle that!

Like what you read? Give Jim Snavely a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.