Scala 2.12 の SAM type を受け取るメソッドを JRuby から呼び出せそうなのでやってみた

ヌーラボでScalaを書くRubyistの谷本です。Scala 2.12から SAM conversion と呼ばれる機能が追加されました。 これにより、Scalaで SAM type を受け取るメソッドを、ラムダ式を受け取る方式で呼び出せるようになりました。

仕事ではScalaを使っているので、 JRuby は使っていませんが、Rubyistとしては、 JRuby と他のJVM言語間の相互呼び出しという話題は気になって仕方ありません。そこで、 JRuby から簡単に SAM conversion を利用できそうだったのでこのエントリでは、その呼び出し手順についてご紹介します。

SAM type を受け取るメソッドをラムダ式を受け取る方式で呼び出せるとはどういうこと?

SAMとはSingle-Abstract-Methodの略で、SAM typeとはAbstractメソッドが一つしかない型のことを言います。

例えば、以下のようなScalaコードがあった場合、Nulaberは SAM Type にあたり、Greeting#helloはSAM typeを受け取るメソッドになります。

// Greting.scala
trait Nulaber {
  def name(): String  // Abstractメソッドが一つだけ存在する
  def groupName: String = "Nulaber"
  def nameWithDecoration: String = s"[[${name()}]]"
}

class Greeting {
  def hello(c: Nulaber): String = s"Hello ${c.nameWithDecoration}!! You are a ${c.groupName}!"
}

Greeting#helloメソッドを Scala 2.11 で呼び出すには、以下のように、無名クラスでAbstractメソッドを定義してインスタンスを作ってhelloメソッドに渡す必要があります。

scala> val result = new Greeting().hello(new Nulaber{
     |   def name() = "tanimoto"
     | })
result: String = Hello [[tanimoto]]!! You are a Nulaber!

Scala 2.11 では少し煩わしさのあるコードですが、 Scala 2.12 では、以下のようにこれをラムダ式を渡す形式で呼び出せるようになりました。これにより、new Nulaberdef nameといったロジックの本質的ではない部分を省略して書くことができ、コード全体がすっきりするので、ロジックに集中できます。

scala> val result = new Greeting().hello {() => "tanimoto"}
result: String = Hello [[tanimoto]]!! You are a Nulaber!

Java 8にはすでに同様の構文が存在し、FunctionalInterfaceアノテーションとともに知られているので、Java 8エンジニアの方には馴染みがあると思います。

JRuby から呼び出す

JRubyとは、JVM上に実装されたRubyのインタプリタです。JVM上で実装されているため、Javaのライブラリが使えたりします。

なぜこのJRubyから簡単に呼び出せそうだと予測したかというと、JRubyのProc呼び出しとJava 8のSAM conversionには互換性があるからです。JRubyのWikiではClosure Conversionとして紹介されている機能です(リンク先のSwingを使った例がわかりやすいです) 。さらに、Scala 2.12のリリースノートにはScalaのラムダ式をjava.lang.Runnableに変換する例が載っているので、Java 8とScala間でSAM conversionが使えるなら、JRubyからでも使えるのではないかと予想しました。JRuby 9.1.12.0を使って実際に呼び出せるか検証します。

JRubyからScalaで作られたclassファイルを呼び出すにはコンパイルに使われたScalaと同じバージョンのscala-library.jarをクラスパスに通して実行する必要があります。 以下のmain.rbを

#main.rb
java_import 'Greeting'
puts Greeting.new.hello {"tanimoto"}

コンパイルされたclassファイルがあるディレクトリで以下のように実行します。

$ jruby -J-cp /Users/tanimoto/.m2/repository/org/scala-lang/scala-library/2.12.3/scala-library-2.12.3.jar ../../../main.rb 
Hello [[tanimoto]]!! You are a Nulaber!

特に問題なく呼び出すことができました。

なぜ、 JRuby から呼び出せるのか?

では、なぜJRubyから呼び出せるのかを少し調べてみようと思います。classファイルの中身を調べるためにjavapコマンドを実行して見ましょう。javapコマンドは、classファイルを逆アセンブルするコマンドでオプションなしで実行するとpublicとprotectedなフィールドとメソッドが表示できます。Scala 2.11とScala 2.12で生成されるNulaberトレイトに関するclassファイルに対して実行した結果が以下です。

# Scala 2.11
$ javap Nulaber\$class.class 
Compiled from "Greeting.scala"
public abstract class Nulaber$class {
  public static java.lang.String groupName(Nulaber);
  public static java.lang.String nameWithDecoration(Nulaber);
  public static void $init$(Nulaber);
}
$ javap Nulaber.class 
Compiled from "Greeting.scala"
public abstract class Nulaber$class {
  public static java.lang.String groupName(Nulaber);
  public static java.lang.String nameWithDecoration(Nulaber);
  public static void $init$(Nulaber);
}

# Scala 2.12
$ javap Nulaber.class 
Compiled from "Greeting.scala"
public interface Nulaber {
  public abstract java.lang.String name();
  public static java.lang.String groupName$(Nulaber);
  public java.lang.String groupName();
  public static java.lang.String nameWithDecoration$(Nulaber);
  public java.lang.String nameWithDecoration();
  public static void $init$(Nulaber);
}

Scala 2.11から生成されたclassファイルには SAM type にあたるものは存在しませんが、Scala 2.12から生成されるclassファイルではNulaberが SAM type になっているます。おそらく、これが原因で JRuby からも呼び出せるようになったのでしょう。

まとめ

検証した結果、JRubyから呼び出した場合でもScala 2.12のSAM conversionは正しく動くようです。これで、JRubyからScalaのコードをProcを使って呼び出すことができず悩んでいた方はScala 2.12に移行する後押しになったでしょう。

ちなみに、このエントリを書くにあたって色々試したら、以下のように型パラメータを使ったコードに変更すると、Hello [[tanimoto]]!! You are a Nulaber!ではなく、Hello tanimoto!! You are a tanimoto!となり、期待しない振る舞いをしていました。使用する際はお気をつけください。

trait Group {
  def nameWithDecoration: String
  def groupName: String
}
trait Nulaber extends Group {
  def name(): String
  def groupName: String = "Nulaber"
  def nameWithDecoration: String = s"[[${name()}]]"
}

trait AbGreeting[T <: Group] {
  def hello(c: T): String = s"Hello ${c.nameWithDecoration}!! You are a ${c.groupName}!"
}
class Greeting extends AbGreeting[Nulaber]
$ jruby -J-cp /Users/tanimoto/.m2/repository/org/scala-lang/scala-library/2.12.3/scala-library-2.12.3.jar main.rb 
Hello tanimoto!! You are a tanimoto!

Java 8で同様のコードをジェネリクスを使って書いた場合も、同様に期待しない振る舞いをしているようなので、Scalaの方に問題があるのではなく、JRubyのバグか仕様だと思われます。時間がある時に詳しく検証して、JRubyコミュニティに報告が必要そうなら、GitHubにIssueを立てようと思います。

ヌーラボでは、仕事で使わないけれどOSSに興味津々なエンジニアを募集しています。JRubyについて話しましょう!

開発メンバー募集中

より良いチームワークを生み出す

チームの創造力を高めるコラボレーションツール

製品をみる