javascalajava-8

Does Scala's Partial Function have a Java Equivalent?


Scala has partial functions which are functions that only apply to some values of the input type, but not all:

val isEven: PartialFunction[Int, String] = {
  case x if x % 2 == 0 => x+" is even"
} 

assert(isEven(10) equalsIgnoreCase "10 is even")
assert(isEven.isDefinedAt(11) == false)

And, even more useful, scala allows the "partialness" to be applied to the sub-type of a trait:

sealed trait BaseTrait

case class Foo(i : Int) extends BaseTrait
case class Bar(s : String) extends BaseTrait

val fooPartialFunc : PartialFunction[BaseTrait, Int] = {
  case f : Foo => 42 + f.i
}

assert(fooPartialFunc(Foo(8)) == 50)
assert(fooPartialFunc.isDefinedAt(Bar("hello")) == false)

What is the equivalent in java 8?

Most google results confuse "partial function" with currying, e.g. "partially applied function".


Solution

  • A Java 8 Idiomatic, But Not Entirely Faithful, Approach

    The most typical thing in Java 8 is going to be the Optional class:

    import static org.hamcrest.Matchers.equalTo;
    import static org.hamcrest.Matchers.is;
    import static org.junit.Assert.assertThat;
    
    import org.junit.Test;
    
    import java.util.Optional;
    
    public class OptionalAsPartialFunction {
    
      Optional<String> isEven(final int x) {
        return Optional.of(x)
            .filter(i -> i % 2 == 0)
            .map(i -> i + " is even");
      }
    
      @Test
      public void example() {
        assertThat(isEven(10).get(), equalTo("10 is even"));
        assertThat(isEven(11).isPresent(), is(false));
      }
    }
    

    If you're familiar with Optional, then you'll see that the string concatenation i + " is even" is only evaluated if the filter condition, i % 2 == 0, is true. If you're not familiar with with Java's Optional, you can write this out with an if/else, as well:

    Optional<String> isEven(final int x) {
      if (x % 2 == 0) {
        return Optional.of(x + " is even");
      } else {
        return Optional.empty();
      }
    }
    

    This should make is completely clear that the string concatenation is evaluated if and only if the guard condition evaluates to true.

    A More Faithful, But Less Idiomatic (in Java) Approach

    With the naive approach, you're calling the function, and getting an Optional which either contains the actual value of the function or it doesn't. But Scala's PartialFunction is a little bit more complex. From the documentation you linked to:

    Even if isDefinedAt returns true for an a: A, calling apply(a) may still throw an exception, so the following code is legal:

    val f: PartialFunction[Int, Any] = { case _ => 1/0 }
    

    So we'd like to be able to check whether the function "is defined" for an input, even if trying to compute if for that input would actually be an error.

    So a more faithful approach would be to use an Optional<Supplier<...>>. The outer Optional lets you know whether there's a computation to perform, and the inner Supplier lets you perform that computation (if you choose). So the example would become:

      Optional<Supplier<String>> isEven(final int x) {
        return Optional.of(x)
            .filter(i -> i % 2 == 0)
            .map(i -> () -> i + " is even");
      }
    

    or, with an if/else:

    Optional<Supplier<String>> isEven(final int x) {
      if (x % 2 == 0) {
        return Optional.of(() -> x + " is even");
      } else {
        return Optional.empty();
      }
    }
    

    and isPresent() still checks whether the function is defined, but get() will now return the Supplier whose get() method will actually compute the value:

      @Test
      public void example() {
        assertThat("isPresent() checks whether the function is defined for the input", //
            isEven(10).isPresent(), equalTo(true));
        assertThat("get() returns the supplier that actually computes the value", //
            isEven(10).get().get(), equalTo("10 is even"));
        assertThat("isPresent() checks whether the function is defined for the input", //
            isEven(11).isPresent(), is(false));
      }