Do Scala Option.or Null in Java

Do Scala Option.or Null in Java

Note that there was no way to change Scala's Some (a) to ʻaandNone to null`.

It has become quite long, so if you are in a hurry, we recommend that you scan around the headings of each section.

Conclusion for the time being

How to use only the scala.Option method that can be used from Java without using the ʻif` statement

option.fold(() -> null, some -> some)

Target

--Those who have some knowledge of Java and have used Scala's [ʻOption` class] api-option --Java 8 and Scala 2.12 ――However, the general procedure should not change even if the version is different.

background

As I gradually rewrite a program of a reasonable scale written in Java to Scala, I want to change the part using null to ʻOption`.

<!-Do you want to explain why you don't want to use null? ->

For example, consider porting a Java method to Scala that returns an extension from a filename string. This method returns the string after the dot if the filename contains a dot (.), or null if it does not.

FileName.java


public class FileName {
    public static String getExtension(final String filename) {
        final int dotIdx = filename.lastIndexOf('.');
        if (dotIdx >= 0) {
            return filename.substring(dotIdx);
        } else {
            return null;
        }
    }
}

An example of a program that uses this class is as follows.

Program.java


public class Program {
    public static void main(final String[] args) {
        printExtension("foo.txt");
        printExtension("hosts");
    }

    public static void printExtension(final String filename) {
        final String ext = FileName.getExtension(filename);
        if (ext != null) {
            System.out.println("\"" + filename + "\"The extension of\"" + ext + "\"is.");
        } else {
            System.out.println("\"" + filename + "\"Has no extension.");
        }
    }
}

//Execution result:
// "foo.txt"The extension of"txt"is.
// "hosts"Has no extension.

Let's rewrite the above FileName class in Scala. I don't want to use null, so the return type of getExtension should be ʻOption [String]`.

FileName.scala


object FileName {
  def getExtension(filename: String): Option[String] =
    filename.lastIndexOf('.') match {
      case dotIdx if dotIdx >= 0 =>
        Some(filename.substring(dotIdx))
      case _ =>
        None
    }
}

Since the return type of FileName.getExtension has changed, Program.java will not compile as it is. Porting Program to Scala is the most legitimate way to do it, but here we want to handle the case where the amount of Java code is huge and it is not practical to port everything to Scala, so Program is Consider using FileName.getExtension somehow in Java.

policy

The process of converting Some (a) to ʻaandNone to null` is ** Java **. This is the purpose of this article.

I think that it is a standard practice to create a helper method on the Scala side for this kind of conversion process, but I think that it may not be realistic due to the large number of methods to be ported, so here. I'll stick to ** Java side ** somehow (If you use Scala's macro-paradise, it will automatically generate a wrapper method that returns ʻA or null instead of ʻOption [A]. Maybe you can make an annotation. Please make someone).

Safe Hand: Utility Class

Probably the most common sense hand.

ScalaCompat.java


import scala.Option;

public class ScalaCompat {
    public static <A> A optionOrNull(final Option<A> o) {
        if (o.isDefined())
            return o.get();
        else
            return null;
    }
}

--ʻO.isDefined () returns true if ʻo is Some (a), and returns false if None. --ʻO.get () returns ʻa if ʻo is Some (a) and throws an exception if None`.

So, first use ʻo.isDefined () to check if ʻo has a value, and after finding out that it is Some (a), take out ʻa with ʻo.get and If o doesn't have a value, it returns null without trying to retrieve it.

It's safe, but it has some drawbacks.

a. You have to create a new class.

Having to create a new class causes the following troubles.

--Choose a package --Think about the class name --You must continue to use this class in the future. Otherwise code duplication will occur

I think it's a silly story, but from the perspective of maintaining the code, it's surprisingly annoying.

Also, you probably already have a library with similar classes, even if you didn't write it yourself, but I'm hesitant to increase the dependency just for this.

b. Cannot be called as an instance method.

If you're doing a methodchain with code that abuses (or ignores, to be exact) null, introducing a utility class like the one above will significantly reduce readability.

For example, consider the following code. Suppose all get methods can return null. (Image like java.util.Map <String, *>)

person.get("account").get("balance").get("USD")

Replace this get method with a method that returns ʻOption` and try using a utility class. In order to maintain the existing behavior, we dare to omit the null check that is originally required.

ScalaCompat.optionOrNull(ScalaCompat.optionOrNull(ScalaCompat.optionOrNull(person.get("account")).get("balance")).get("USD"))

Can you see how hard it is to read and write? "Check null! "," Change the structure of the code! "," Port it to Scala! ", Etc., but in the reality of time constraints, other methods of the same class Due to the situation such as a large number, it is necessary to refactor "as Java" "without checking null (= without changing the condition that NPE occurs), and the method under such a situation The static methods of the chain and utility classes are the worst match.

Variant 1: ʻif` statement

It's not difficult, it's just a matter of using the ʻif` statement.

final Option<Account> account = person.get("account");
if (account.isDefined()) {
  final Option<Balance> balance = account.get().get("balance");
  if (balance.isDefined()) {
    final Option<Double> usd = balance.get().get("USD");
    if (usd.isDefined()) {
      return usd;
    } else {
      return null;
    }
  }
}
throw new NullPointerException();

The code, which was originally one line, has expanded to 13 lines. In addition to the increased number of lines, it is also necessary to specify the type to assign to a local variable once. Scala has a for comprehension for these times, but unfortunately Java doesn't do that.

Variant 2: try statement

ʻOption.get throws aNoSuchElementExceptionforNone`, so you can catch it and send an NPE instead.

try {
  person.get("account").get().get("balance").get().get("USD").get();
} catch (final NoSuchElementException e) {
  throw new NullPointerException();
}

It can be written relatively concisely. It looks fine if you can tolerate one line increasing to four lines. However, this approach has some sober but annoying problems. What if person.get ("account ") itself throws a NoSuchElementException instead of ʻOption.get ()`? The original code throws the exception as is, but this code also replaces it with an NPE. It catches exceptions that shouldn't be caught, which can secretly cause bugs.

I think it depends on the case ... In this article, all of these are considered impractical. Below, let's consider how to make ʻOption`` null "using the instance method of scala.Option` ".

Failure 1: ʻOption.orNull`

ʻOption has a method called ʻorNull. This is a very custom method that returns ʻa if ʻOption itself isSome (a)and null for None, but unfortunately it is difficult to use from Java. You can see why by looking at the declaration of ʻOption.orNull`. According to the [Scala API Reference] api-option-ornull, the signature is

final def orNull[A1 >: A](implicit:ev<:<[Null,A1]): A1

The problem is this ʻimplicit argument. In a nutshell, this is used to prevent the type argument ʻA from being used for types that cannot be null, such as ʻInt (detailed description: [StackOverflow] [so-option]. -ornull]). You don't have to worry about it because the compiler automatically fills the ʻimplicit argument for use in Scala, but in Java you have to fill it by hand, which is a very tedious task.

What if I had to actually fill it in by hand? Let's think about it because it's a big deal.

According to javap, as of Scala-2.12.6, ʻOption.orNull` seems to be compiled into a method like this:

$ javap scala/Option.class | grep orNull
  public final <A1> A1 orNull(scala.Predef$$less$colon$less<scala.runtime.Null$, A1>)
...

You can see that the ʻimplicitargument is compiled into just an argument. The type is<: <, a member class of Scala's standard library, scala.Predef $, and a subclass of Function1`.

[Scala standard library scala / Predef.scala] [api-predef- <: <] quotes only the main points

scala/Predef.scala


object Predef {
  //Mystery parameter type(1)
  sealed abstract class <:<[-From, +To] extends (From => To) with Serializable
  // <:<Only instance of(2)
  private[this] final val singleton_<:< = new <:<[Any,Any] { def apply(x: Any): Any = x }
  //Source of implicit value(3)
  implicit def $conforms[A]: A <:< A = singleton_<:<.asInstanceOf[A <:< A]

I will explain each point.

--(1): Definition of the type of mystery parameter (identity of implicit argument). It is no different from Function1 [From, To] except that it mixins Serializable. --(2): (1) is the sealed class, so this is the only instance. If you look closely at the contents of def apply, it is practically the same as ʻidentity. --(3): ʻimplicit def function that does not take a value parameter. Therefore, like the ʻimplicit val value, this value is used implicitly when passing the ʻimplicit parameter.

Taken together, the implicit parameter implicitly passed by the Scala compiler when calling ʻOption.orNull is (3) , and its contents are (2) (cast), which is effectively In the end, it is ʻidentity.

To be a little more specific, let's trace a program that calls # orNull forNone: Option [X]with any type X.

val o = Option.empty[X]

val x: X
  = o.orNull
  //Since the expression type is X, X is selected for the orNull type argument A1.
  // <:<Is reversal for type 1 arguments, Null<:Because it is X(Null <:< X) >: (X <:< X)Is.
  // (X <:<X is null<:<It is a subclass of X.)
  //Therefore Predef.$conforms[X]: X <:<X is selected for the implicit argument.(☆)
  = o.orNull(Predef.$conforms[X])
  = o.orNull(singleton_<:<.asInstanceOf[X <:< X])
  // singleton_<:<Note that is essentially identity,
  ~= o.orNull(identity: X => X)

However, singleton_ <: < is private [this], so if you want to specify it explicitly, you can specify Predef. $ Conforms [X]. If you write this in Java

scala.Predef.<X>$conforms()

It means that.

To summarize the above, when calling ʻOption.orNull` in Java, it will be as follows.

final scala.Option<A> option = ...;
final A aOrNull = option.orNull(scala.Predef.<A>$conforms());

I see. But this doesn't compile. ʻA <: <B is rebellious about ʻA because it's only in Scala and is treated as immutable in Java. The type of scala.Predef. <A> $ conforms () is scala.Predef $$ less $ common $ less <A, A> when written in Java, but the implicit argument type is <A1> scala Since .Predef $$ less $ common $ less <scala.runtime.Null $, A1> , both ʻA and ʻA1 must bescala.runtime.Null $ .

Therefore, it should be as follows.

final scala.Option<A> option = ...;
final A aOrNull = (A)(Object) option.orNull(scala.Predef.<scala.runtime.Null$>$conforms());

--By specifying scala.runtime.Null $ in the generics argument of $ conforms, you can get through non-denaturation (hack). --As a result, the return type of # orNull will bescala.runtime.Null $, so recast it to ʻA (hack). --Since it is a cast between unrelated types, we will use the double cast technique to cast to ʻObject once (super hack).

If you rewrite Program.java using this, it will be as follows.

Program.java


public class Program {
    public static void main(final String[] args) {
        printExtension("foo.txt");
        printExtension("hosts");
    }

    public static void printExtension(final String filename) {
        final String ext = (String)(Object) FileName.getExtension(filename).orNull(scala.Predef.<scala.runtime.Null$>$conforms());
        if (ext != null) {
            System.out.println("\"" + filename + "\"The extension of\"" + ext + "\"is.");
        } else {
            System.out.println("\"" + filename + "\"Has no extension.");
        }
    }
}

//Execution result:
// "foo.txt"The extension of"txt"is.
// "hosts"Has no extension.

I tried it with half a joke, but the result was more hacky than I expected. The reason this program works (becomes) as intended is that Predef.singleton_ <: < is effectively ʻidentity` and has no compile-time or run-time type concerns.

In the first place, the implicit argument of ʻOption.orNull has no particular meaning in the value itself (it's just ʻidentity), but in" whether or not the value exists ". For example, as a counterexample, if you try to call ʻOption [Int] .orNull, the compiler will look for a value of type Null <: <Int. However, the only implicit value, ʻimplicit def $ conforms [A]: A <: <If you substitute ʻInt for ʻA of A, you get ʻInt <: <Int, but Null <: IntDoes not hold, so(Null <: <Int)>: (Int <: <Int)does not hold (the above(☆) fails). Therefore, the implicit argument corresponding to ʻOption [Int] .orNull cannot be found, and it is possible to make a ** compile-time ** error.

In Java (not explained, but in Scala), you can force the implicit argument to be passed manually by abusing the cast, but the compile-time check will not work.

If for some reason you absolutely must call ʻOption.orNull` from Java, it might be a good idea to try it as a last resort. I do not recommend it at all.

Failure 2: ʻOption.getOrElse (null)`

There are several methods of type ʻOption [A] => A or close to it (ʻor Null is one of them), but the typical one is getOrElse.

Looking at the declaration of ʻOption.getOrElse` in [API Reference] api-option-getorelse:

final def getOrElse[B >: A](default:=>B): B

Unlike ʻor Null, there is no implicit argument. The argument default is the value returned instead if ʻOption is None, so it seems like you should specify null.

TooBad_NPE.java


public class TooBad_NPE {
    public static void main(final String[] args) {
        final scala.Option<String> some = scala.Option.apply("foo");  // Some("foo")
        final String foo = some.getOrElse(null);
        System.out.println(foo);                                      // foo
        final scala.Option<String> none = scala.Option.apply(null);   // None
        final String shouldBeNull = none.getOrElse(null);             // !! throw NullPointerException !!
        System.out.println(shouldBeNull);
    }
}

The compilation will pass. It also works as expected when ʻOption is Some. However, if ʻOption is None, NPE will be sent.

The cause is still in the getOrElse signature. If you look closely at the declaration, it says default:=>B. The type of default is B, but it is passed by name (=>). Arguments passed by name are special arguments, and the evaluation (execution) of the argument is done in the called method, not in the caller. Java doesn't have this feature, so it must have been compiled into something special! Let's take a look at javap:

$ javap scala/Option.class | grep getOrElse
  public final <B> B getOrElse(scala.Function0<B>)

So it turns out that the name-passing argument => B is compiled intoscala.Function0 <B>(that is, the zero-argument function () => B). This means that passing null to the argument default of getOrElse [B](default:=>B) means that ** null is not B, but Function0 [ B] is **. Both B andFunction0 <B>are subclasses of ʻObject for Java, so they will compile. Since the "object that should tell you what to return in the case of None "is null, NPE will occur in the case of None`.

… So it looks like you should write an object in Java that corresponds to () => null: Function0 [B].

Failure 3: ʻOption.getOrElse (()-> null)`

Thanks to the "SAM (Single Abstract Method) conversion" introduced in Java8, Java lambda expressions are directly converted to scala.Function1. So you can write in Java:

final scala.Option<String> some = scala.Option.apply("foo");  // Some("foo")
System.out.println(some.getOrElse(() -> null));               // foo
final scala.Option<String> none = scala.Option.apply(null);   // None
System.out.println(none.getOrElse(() -> null));               // null

You can call getOrElse almost like Scala. Thank you. It seems like we've finally achieved what we expected, but unfortunately there are still pitfalls. In fact, ʻOption.getOrElse` is not * type-safe in * Java.

TooBad_CCE.java


import java.util.Date;

public class TooBad_CCE {
    public static void main(final String[] args) {
        final scala.Option<String> some = scala.Option.apply("foo");  // Some("foo")
        final String foo = some.getOrElse(() -> null);
        System.out.println(foo);                                      // foo
        final scala.Option<String> none = scala.Option.apply(null);   // None
        final String shouldBeNull = none.getOrElse(() -> null);
        System.out.println(shouldBeNull);                             // null
        //Up to this point is OK

        //The content of Option is Date, but the recipient is String
        final scala.Option<Date> now = scala.Option.apply(new Date());
        final String nowstr = now.getOrElse(() -> null);              // !! throw ClassCastException !!
    }
}

Since now is of type ʻOption , the type of now.getOrElse (...)should beDate, but as mentioned above, even if it is received by another type, it will compile. I will end up. I get a ClassCastException` at run time.

Why is this happening? Now let's take a closer look at Scala's API reference and javap output.

apidoc.scala


final def getOrElse[B >: A](default:=>B): B

javap.java


public final <B> B getOrElse(scala.Function0<B>)

There are differences in the constraints imposed on B. In Scala [B>: A], that is, B must be a superclass of ʻA, but in Java it's just , that is, B is ʻA. You can choose an irrelevant type. In the TooBad_CCE.java example, String is selected for B from the return type, but this is not a problem for the Java compiler. Therefore, it compiles, and the type is actually different, so an exception is thrown at runtime.

For the time being, it is possible to generate a compile error by explicitly specifying the generics argument.

final scala.Option<Date> now = scala.Option.apply(new Date());
final String nowstr_ct = now.<Date>getOrElse(() -> null);    // compilation error
final String nowstr_rt = now.<String>getOrElse(() -> null);  // !! throw ClassCastException !!

However, it is a lot of work, and if you mistakenly specify the recipient type instead of the ʻOption` object type, you will still get a run-time error, which will cause careless mistakes.

It turns out that getOrElse works as expected, but loses type safety. It causes bugs that are difficult to find, so I would like to avoid it if possible.

Correct answer: ʻOption.fold (()-> null, some-> some)`

ʻOption has a method called fold`. From [API Reference] api-option-fold:

final def fold[B](ifEmpty:=>B)(f:(A)=>B): B

If ʻOption is None, the evaluated value of ʻif Empty is returned, and ifSome (a),f (a)is returned. If you specify ʻidentity for f, it behaves the same as getOrElse. The point is that both ʻA and B appear in the second parameter section, so if you give a function with the type ʻA => A, you can get B without explicitly specifying the type parameter. It can be fixed to ʻA.

This method compiles into just a two-argument method. From javap:

$ javap scala/Option.class | grep fold
  public final <B> B fold(scala.Function0<B>, scala.Function1<A, B>);

So, it seems good to specify a function that returns null as the first argument and a function that returns the argument as it is as the second argument. let's do it.

WorksAsIntended.java


import java.util.Date;

public class WorksAsIntended {
    public static void main(final String[] args) {
        final scala.Option<Date> now = scala.Option.apply(new Date());
        // final String nowstr = now.fold(() -> null, some -> some);  // compilation error
        final Date nowdate  = now.fold(() -> null, some -> some);  // ok, now
        final scala.Option<Date> none = scala.Option.apply(null);
        final Date nulldate = none.fold(() -> null, some -> some); // ok, null
        System.out.println(nowdate);   //Current time
        System.out.println(nulldate);  // null
    }
}

(You can use any name instead of some, but I tried this name to make it clear that it handles the case of Some.)

It's a bit long, but I was able to achieve a more type-safe way "using the instance method of scala.Option ".

Digression: When type inference fails

If the Java compiler cannot do type inference, such as when passing it as an argument to an overloaded method, you need to explicitly specify the generics argument.

However, even in that case, if you specify the wrong generics argument, a compile error will occur, so type safety is maintained unlike the case of ʻOption.getOrElse (()-> null)`.

ExplicitGenParam.java


import java.util.Date;

public class ExplicitGenParam {
    public static void main(final String[] args) {
        final scala.Option<Date> now = scala.Option.apply(new Date());
        println(now.fold(() -> null, some -> some));          // compilation error, ambiguous
        println(now.<Date>fold(() -> null, some -> some));    // OK,Current time
        println(now.<String>fold(() -> null, some -> some));  // compilation error, incompatible types
    }

    public static void println(final Date x) {
        System.out.println(x);
    }

    public static void println(final String x) {
        System.out.println(x);
    }
}

Conclusion

How to change Scala's ʻOption [A] to ʻA or null in Java

option.fold(() -> null, some -> some)

Is recommended.

Let's use this to rewrite Program.java in the" Background "section for FileName.scala.

Program.java


public class Program {
    public static void main(final String[] args) {
        printExtension("foo.txt");
        printExtension("hosts");
    }

    public static void printExtension(final String filename) {
        final String ext = FileName.getExtension(filename).fold(() -> null, some -> some); //This has changed
        if (ext != null) {
            System.out.println("\"" + filename + "\"The extension of\"" + ext + "\"is.");
        } else {
            System.out.println("\"" + filename + "\"Has no extension.");
        }
    }
}

//Execution result:
// "foo.txt"The extension of"txt"is.
// "hosts"Has no extension.

I'm happy.

Lesson

I have learned some lessons from this matter, so I will summarize them.

--ʻOption.orNull is virtually unusable from Java. --The name-passing argument (=> B) is compiled into scala.Function0 , so when passing null, pass the lambda expression ()-> null. --Type safety may not be maintained when calling a method with Scala upper and lower bound type parameters ([B>: A]`) from Java.

However, if you write the entire program in Scala, all the above work will be unnecessary, so everyone, please consider Scala when developing a new program.

Recommended Posts