Scala でリフレクションしてみる

f:id:ts0818:20191020142630j:plain

 大勢の人が当たり前に知っている事実だったはずなのに、全部デタラメだったという経験はあるだろうか?

 例えば、映画「スター・ウォーズ」シリーズに登場するC-3POは銀色(本当は金色)という思い込みや、白雪姫の王妃のセリフは「鏡よ、鏡(Mirror, mirror on the wall)」であるといった思い込みだ(英語では本来「魔法の鏡よ / magic mirror on the wall」)。

 これをマンデラ効果と呼ぶそうだ。

マンデラ効果とは何か?不特定多数が「偽の記憶」を真実だと思い込んでしまう集団的な誤解 (2019年8月26日) - エキサイトニュース

はい~、「鏡よ、鏡」って思ってたので、マンデラ効果の影響化にいたという...どうもボクです。

美しさを求める問いかけを投げかける対象先と言えば、そう!鏡 ですよね。

smoo.jp

beautytech.jp

「Bueaty」+「Technology」=「BueatyTech」っていう分野もあるそうですね。

『心は自分を映し出す鏡!』ということで、今回は、Scala のリフレクション について学んでいこうかと。

そして、

www.bcnretail.com

⇧  プログラミングの世界にも、新たな天才が!

やっぱり、才能ある人間はいるんですね。

片や常に泥沼にハマり続ける日々を送るわたくしのような人間もおると(涙)。

というわけで、やる気が一気に落ち込んだところで、レッツトライ~。

何回も躓いているので、もし参考にしていただける場合は、一旦、最後まで通してお読みいただいたほうが良いかと~。

 

リフレクションって?

 Wikipediaさ~ん!

情報工学においてリフレクション (reflection) とは、プログラムの実行過程でプログラム自身の構造を読み取ったり書き換えたりする技術のことを指す。

リフレクション (情報工学) - Wikipedia

一般に、リフレクションとはオブジェクトがそれ自身の構造や計算上の意味を取得することを可能にするものである。リフレクションによるプログラミングパラダイムリフレクティブプログラミング (reflective programming) という。

リフレクション (情報工学) - Wikipedia

通常、プログラムのソースコードコンパイルされると、プログラムの構造などの情報は低レベルコード(アセンブリ言語など)に変換される過程で失われてしまう。リフレクションをサポートする場合、そのような情報は生成されるコードの中にメタデータとして保存される。

リフレクション (情報工学) - Wikipedia

LISPForthなど実行時とコンパイル時の区別のない言語では、コードの解釈とリフレクションとの間に違いはない。

リフレクション (情報工学) - Wikipedia

⇧  う~ん、「プログラムの構造などの情報」っていうのが何を指しているんだい?な情弱な私ですが、まとめると、『リフレクションは、「プログラムの構造などの情報を、メタデータとして保持し、それを利用する」』ってことですかね。 

Wikipedia さんで、Java についての例を抜粋させていただくと、

■リフレクション使わない

// リフレクションなし
Foo foo = new Foo();
foo.hello();

リフレクション (情報工学) - Wikipedia

■リフレクション使う 

// リフレクション
Class cl = Class.forName("Foo");
Method method = cl.getMethod("hello");
method.invoke(cl.newInstance());

リフレクション (情報工学) - Wikipedia

っていう違いがあるんだけども、

どちらのコードでもFooクラスインスタンスを作成し、そのインスタンスhello()メソッドを呼んでいる。

リフレクション (情報工学) - Wikipedia 

前者の例では、クラス名やメソッド名がハードコーディングされているので実行時に他のクラスに変更するのは不可能である。リフレクションを用いた後者の例では、それらを実行時に容易に変更することができる。

リフレクション (情報工学) - Wikipedia

しかしその一方で、後者は読みにくく、またコンパイル時チェックの恩恵も得られない。つまり、もしFooクラスが存在しなかったとしたら前者のコードではコンパイル時にエラーとなるが、後者のコードでは実行するまでエラーが発生しない。

リフレクション (情報工学) - Wikipedia

⇧  つまり、リフレクションを使うことで何ができるかと言うと、『クラス名やメソッド名を操作できる』ってことですかね。

使い道としては、単体テストなんかには向いてますかね。 

ちなみに、Java 9 から、非推奨になったリストの影響で、リフレクションについても上手く動かない問題が出てくるようですかね。

docs.oracle.com

nulab.com

 

そして、Javaの話が続いてしまいますが、リフレクションにも影響してくるのが、Javaだとジェネリックス、イレイジャってものらしく、 

nagise.hatenablog.jp

nagise.hatenablog.jp

Java VM 上で動くJavaより後の世代の言語に Scala が挙げられる。Scalaもまたイレイジャ方式のメソッド解決をする言語である。

Javaと同様にイレイジャが同一となるメソッドのオーバーロードができないという制約を持つ。

しかし、TypeTag / ClassTagを用いて実行時に型変数の型を得ることができる。つまりイレイジャ方式であっても、実行時に型変数の型を動的に得られるようにすることは可能である

Javaが実行時に型変数の型を動的に得られないのは、実行時のイレイジャに対して、別途、型変数の型を渡す機構を作らなかったからと言えよう。現に、別途作ったScalaはやれているのだから。

贖罪のイレイジャ - プログラマーの脳みそ

⇧  Scala は、流石、better Java と呼ばれるだけあって、いろんな手が打てるようにしてくれていますね。

上記サイト様は、Java について詳しいので、Javaのことについて知りたいって方は必見です、わたしは「Effective Java」読んでて分からない概念が、上記サイト様で解決できました~。 

Javaにとっての「イレイジャ」の仕様は、後方互換性を維持するという点に重きを置いていたことで、ジェネリックスについても複雑な部分が出てきてしまったという話らしいです、悲劇...。

 

Scalaにおけるリフレクションって?

たいへん長らく脱線しまくりLa'cryma Christi~ですが...

La'cryma Christiラクリマ・クリスティー)は、日本ヴィジュアル系ロックバンド1991年に結成され、2007年に解散したが、2009年に再結成した。かつて、MALICE MIZERFANATIC◇CRISISSHAZNAと共に「ヴィジュアル四天王」と呼ばれていた

La'cryma Christi - Wikipedia

⇧  あ...「Janne Da Arc」は「ヴィジュアル四天王」じゃないんだ...

はい...すみませんでした...

 

そんじゃあ、本題。

Scalaにおけるリフレクションってどんなん?

docs.scala-lang.org

リフレクション (reflection) とは、プログラムが実行時において自身をインスペクトしたり、変更したりできる能力のことだ。

それはオブジェクト指向、関数型、論理プログラミングなど様々なプログラミングのパラダイムに渡って長い歴史を持つ。

それぞれのパラダイムが、時として顕著に異なる方向性に向けて現在のリフレクションを進化させてきた。

LISP/Scheme のような関数型の言語が動的なインタープリタを可能とすることに比重を置いてきたのに対し、Java のようなオブジェクト指向言語は実行時におけるクラスメンバのインスペクションや呼び出しを実現するための実行時リフレクションに主な比重を置いてきた。

概要 | Reflection | Scala Documentation

複数の言語やパラダイムに渡る主要なリフレクションの用例を以下に 3つ挙げる:

  1. 実行時リフレクション。実行時にランタイム型 (runtime type) やそのメンバをインスペクトしたり呼び出す能力。
  2. コンパイル時リフレクションコンパイル時に抽象構文木にアクセスしたり、それを操作する能力。
  3. レイフィケーション (reification)。(1) の場合は実行時に、(2) の場合はコンパイル時に抽象構文木を生成すること。

概要 | Reflection | Scala Documentation

Scala 2.10 までは Scala は独自のリフレクション機能を持っていなかった。 代わりに、Java リフレクションを使って (1) の実行時リフレクションのうちの非常に限定的な一部の機能のみを使うことができた。

しかし、存在型、高カインド型、パス依存型、抽象型など多くの Scala 独自の型の情報はそのままの Java リフレクションのもとでは実行時に復元不可能だった。 これらの Scala 独自の型に加え、Java リフレクションはコンパイル時にジェネリックである Java 型の実行時型情報も復元できない。

この制約は Scalaジェネリック型の実行時リフレクションも受け継いでいる。

概要 | Reflection | Scala Documentation

Scala には、リフレクションって無かったらしく、Javaのリフレクションを使ってたんだけど、いろいろ不足があったようで、

Scala 2.10 は、Scala 独自型とジェネリック型に対する Java の実行時リフレクションの欠点に対処するためだけではなく、 汎用リフレクション機能を持ったより強力なツールボックスを追加するために新しいリフレクションのライブラリを導入する。 Scala 型とジェネリックスに対する完全な実行時リフレクション (1) の他に、 Scala 2.10 はマクロ という形でコンパイル時リフレクション機能 (2) と、 Scala の式を抽象構文木へとレイファイ (reify) する機能 (3) も提供する。

概要 | Reflection | Scala Documentation

⇧  そんじゃあ、作っちゃえ、ということで、ここに爆誕

Scala 2.10 から独自のリフレクションAPIを生み出してしまったのだと。

 

Scalaでリフレクションを可能にするAPIたち

まぁ、Scala 2.10より、Scalaの標準APIでリフレクションが可能になったということで、リフレクションするのにAPIはいくつ必要なのかと。

www.casleyconsulting.co.jp

Javaではリフレクションに、以下のようなクラスを使います。

  • java.lang.Class
  • java.lang.reflect.Method
  • java.lang.reflect.Field

リフレクション対象の各要素がそのままクラスになっていて、
クラスに関する情報の参照やクラスに対する操作はClassを使い、
メソッドをコントロールしたければMethodを、と素直でわかりやすいです。

それに対し、Scalaでは以下のようなクラスを使います。

Typeは型情報の参照に、Mirrorは対象の操作に、それぞれ使います。
SymbolはTypeやMirrorと相互変換できたりする、リフレクション関連クラスを統合するモノです。

Scalaでリフレクション | キャスレーコンサルティング株式会社

Javaがリフレクション要素ごとにクラス化しているのに対し、
Scalaでは要素に対する作用(参照や操作)をクラス化しているイメージです。

Scalaでリフレクション | キャスレーコンサルティング株式会社

⇧  上記サイトによりますと、メインで使っていくAPIの数は、JavaScala 共に3つのようですが、Scala のリフレクションは難易度が高そうです(涙)。

Javaのリフレクションは、Classの情報を取得した後に、その他の情報を取得するって方法が一般的かと思われますが、Scala の場合は、

の2つの方法でClassの情報を取得できるらしい。 

 

Scala でリフレクションしてみる

その前に、Scala のプログラミングを動かすために何が必要なのか。

⇧  最低限、上記が必要。sbt は、Scalaのビルドツールなので、sbt をインストールしておけば、Scalaも、sbt に同梱されてくる?らしい。

なので、「JDK 8 以上」「sbt」の2つがインストールされていれば、Scala を始めることができるかと。

使用できるバージョンとかは、

docs.scala-lang.org

⇧  決まってるので要注意。

インストールしてない場合は、

www.sejuku.net

⇧  上記サイト様を参考にインストールしてみてください。

 

インストールし終わったら、Javaの確認。

f:id:ts0818:20191022161422p:plain

sbt は、Scalaのプロジェクトを作成するまでは、コマンドでバージョン確認ができないので、インストールされてるかは、エクスプローラーから確認するしかないかと...

f:id:ts0818:20191022163209p:plain

おそらく、バージョンは「0.11.3」ってことかと。

f:id:ts0818:20191022164255p:plain

とりあえず、Scala プロジェクト用のディレクトリを作成で。

f:id:ts0818:20191022162417p:plain

んで、

www.scala-sbt.org

⇧  Scala プロジェクト用のディレクトリに「build.sbt」ってファイルを作成しておく必要があると。作成します。

f:id:ts0818:20191022165255p:plain

f:id:ts0818:20191022170352p:plain

あとは、「*.scala」ファイルを作成して、compile とかすれば、CUI上でScalaのプログラミングを実行はできるんですが、せっかくなんで、GUI上でやりたいと。

ScalaGUI上で開発するには、

って選択肢になるかと。実質、IDEだと、現状、IntelliJ IDEA の一択かと。ソースコードエディタは他にもScala対応してるかもですが、分からないっす...

今回は、Visual Studio Code を使っていこうかと。インストールしてない方はインストールしちゃいましょう。

んで、ソースコードエディタ使う場合に、拡張機能としてインストールするものとして、最近だと、「Metals」ってのがイケてるらしい。

github.com

Metals = Meta (from Scalameta) + LS (from Language Server)

GitHub - scalameta/metals: Scala language server with rich IDE features 🚀

takezoe.hatenablog.com

tarao.hatenablog.com

⇧  上記サイト様によりますと、まだ、発展途上の感は否めないらしい、というか発展してもらわないと困る問題はあるとのこと。

ちなみに、公式の説明だと、

scalameta.org

Metals uses the Build Server Protocol (BSP) to communicate with build tools. Any build tool that implements the protocol should work with Metals.

There are two options for integrating Metals with a new build tool:

  • via Bloop: emit Bloop JSON configuration files and use the Bloop build server. The benefit of this approach is that it is simple to implement but has the downside that compilation happens outside of your build tool.
  • via custom build server: add Build Server Protocol support to your build tool. The benefit of this approach is that Metals integrates directly with your build tool, reproducing the same build environment as your current workflow. The downside of this approach is that it most likely requires more effort compared to emitting Bloop JSON files.

Integrating a new build tool · Metals

⇧  『「BSP(Build Server Protocol)」って仕様を満たしてるビルドツールなら「Metals」で動くよ』ってことらしい。

「Metals」は、「Scala language server with rich IDE features」ってあるように、IDEみたいな機能をソースコードエディタでも使えるようにしてくれるってことみたい。  


そしたらば、 ここからは、Visual Studio Code 起動で。「ファイル(F)」>「フォルダーを開く...」で、

f:id:ts0818:20191022170600p:plain

作成しておいた、Scalaプロジェクト用のディレクトリを選択

f:id:ts0818:20191022170833p:plain

f:id:ts0818:20191022171147p:plain のアイコンをクリックで。f:id:ts0818:20191022171116p:plain

Scala(Metals)」って拡張機能をインストールするんですが、

scalameta.org

Make sure to disable the extensions Scala Language Server and Scala (sbt) if they are installed. The Dotty Language Server does not need to be disabled because the Metals and Dotty extensions don't conflict with each other.

Visual Studio Code · Metals

⇧  拡張機能のバッティングが起こるらしいので、「Scala Language Server」「Scala(sbt)」って拡張機能がインストールされてしまっている場合は無効化しておきましょう。

f:id:ts0818:20191022171452p:plain

んで、インストールされると。

f:id:ts0818:20191022175905p:plain

f:id:ts0818:20191022180012p:plain

⇧  はい、エラー。sbt のバージョンが足りないって...どこにも、sbt の要件とか書いとらんやんけ~!

致し方ないので、sbt のバージョンアップで。chocolatey でインストールしてる場合は、chocolatey でアップデートできるらしい。

f:id:ts0818:20191022180716p:plain

f:id:ts0818:20191022180846p:plain

更新されてるかと。

f:id:ts0818:20191022181222p:plain

Visual Studio Code 再起動してみたら、

f:id:ts0818:20191022181512p:plain

⇧  まさかの、「sbt 1.2.8」ジャストのバージョン指定。

chocolatey.org

⇧ multiple versions でのインストールを試みるが、

f:id:ts0818:20191022182211p:plain

f:id:ts0818:20191022182340p:plain

「sbt 1.3.3」がアンインストールされるという...

まぁ、今回は、使わないから良いけどさ...

Visual Studio Code 再起動で。

f:id:ts0818:20191022182801p:plain

エラーは出なくなった。

f:id:ts0818:20191022183209p:plain のアイコンが追加されてるのでクリックで、「BUILD」>「import build」をクリックで。

f:id:ts0818:20191022183130p:plain

駄目でした...

どうやら、Scalaのプロジェクト用のディレクトリで、「sbt」ってコマンドを実行しておく必要があったらしい。

f:id:ts0818:20191022185633p:plain

そしたら、一旦、「exit」で。

で、Visual Studio Code を再起動すると、「Import build」ってダイアログに表示されるんで、クリックで。

f:id:ts0818:20191022185751p:plain

f:id:ts0818:20191022185819p:plain

f:id:ts0818:20191022185923p:plain

整ったらしい。Scalaのバージョンがギリギリらしい。

f:id:ts0818:20191022190015p:plain

まぁ、残念ながら、JavaScalaの対応表はあるんだけども、欲しいのは、JavaScala、sbt の対応表でしょうに...

とりあえず、拡張子が「.scala」のファイルを作成していきますか。

f:id:ts0818:20191022195341p:plain

そしたらば、ソースコードを記述していきましょう。
とりあえず、「Hello World!」を表示する内容を。

object TestReflection {
    def main(args: Array[String]):Unit = {
        println("Hello World!")
    }
}

そしたら、「表示(V)」>「ターミナル」で。

f:id:ts0818:20191022201044p:plain

ターミナルで「sbt」シェルを開く。

f:id:ts0818:20191022201931p:plain

そしたら、「run」で。

f:id:ts0818:20191022202024p:plain

で、エラー。

f:id:ts0818:20191022202840p:plain

codeday.me

デフォルトでは、sbtはsrc / main / scalaディレクトリの下のソースを管理します。そこに、実行するAppオブジェクトがあります。

sbtでは、runはsrc / main / scala配下のすべてのソースからアプリケーションを検索します。

Scalaプロジェクトをビルド、コンパイル、実行する方法 - コードログ

⇧  どうやら、ディレクトリ構成が駄目らしい...知らんがな

以下のようなディレクトリ構成に修正します。

f:id:ts0818:20191022203435p:plain

再び、ターミナルで実行します。

f:id:ts0818:20191022203527p:plain

今度は動きました~!

では、本題のリフレクションで。

stackoverflow.com

kazuhira-r.hatenablog.com

Scalaのリフレクションを使用するには、「scala-reflect.jar」というJARファイルが必要です。

Scalaのオフィシャルサイトで配布されているディストリビューションには、libディレクトリに含まれているのでScalaの各種コマンド(scala/scalac/fsc)で使う分には何も用意する必要がありません。

ただ、sbtを使っている場合は、依存関係の追加が必要です。

ScalaのReflectionについて、まとめてみる - 導入編 - CLOVER🍀

⇧ どうやら、「build.sbt」に、自分の使ってるScalaのバージョンに合致する「scala-reflect.jar」を使うよって明示しないと駄目みたい。

 

ターミナルで、「sbt」のシェル内にいる状態で、「about」とコマンドすれば、Scalaのバージョンが表示できます。

f:id:ts0818:20191022205834p:plain

そんじゃ、「build.sbt」に追記で。

www.scala-sbt.org

 

⇧  記述の仕方は、上記を参考。

それぞれのバージョンは、ご自身の環境のものに合わせてください。

version := "1.2.8"
libraryDependencies += "org.scala-lang" % "scala-reflect" % "2.12.7"

そしたらば、「Metals」のダイアログが出るんで「Import changes」をクリック。

f:id:ts0818:20191022221235p:plain

んで、ソースコードを修正します。

import scala.reflect.runtime.universe._

object TestReflection {
    def main(args: Array[String]):Unit = {
        // println("Hello World!")
        // ■ RuntimeMirror 編
        // ClassLoaderのMirrorを取得
        val clsLdMirr = runtimeMirror(getClass.getClassLoader)
        if(clsLdMirr == scala.reflect.runtime.currentMirror) {
            println(clsLdMirr)
        }
        // String型のInstanceMirrorを取得
        val strMirr = clsLdMirr.reflect("mojimoji")
        println(strMirr)
        // Symbol へ変換
        val strSym = strMirr.symbol
        println(strSym)
        // Symbol からTypeを取得
        val strTyp = strSym.typeSignature
        println(strTyp)
        // lengthメソッドを取得
        val lenSym = strTyp.member(TermName("length"))
        // lengthメソッドを実行可能なMirrorを取得
        val lenMirr = strMirr.reflectMethod(lenSym.asMethod)
        println(lenMirr())

        // ■ TypeTag 編
        val strTypTag = typeOf[String]
        // toUpperCase メソッドのsymbolを取得
        val uppCase = strTyp.member(TermName("toUpperCase"))
        // toUpperCase メソッドは、引数のなし、ありがある
        println(uppCase.alternatives(0).asMethod.paramLists)
        println(uppCase.alternatives(1).asMethod.paramLists)

        // 引数なしのtoUpperCaseメソッドの取得
        val uppCaseNoArg = uppCase.alternatives.find(_.asMethod.paramLists.flatten.isEmpty)

        // Stringインスタンスを取得
        val strMojiMirr = scala.reflect.runtime.currentMirror.reflect("mojimoji")
        // toUpperCase を実施
        println(strMojiMirr.reflectMethod(uppCaseNoArg.get.asMethod))

    }
}    

んで、実行してみる。

f:id:ts0818:20191022225731p:plain

はい、エラー。

「object runtime is not a member of package reflect」って言われてもね...

どうやら、「build.sbt」の変更を反映させるには、「Metals」のほうの「build import」の再実行とは別に、「sbt」シェルも再起動しなきゃならんかったみたい...知らんがな。

ということで、一旦、「exit」し、再度「sbt」シェルを起動。

f:id:ts0818:20191022230652p:plain

そしたらば、「run」で、「TestReflection.scala」を実施で。

f:id:ts0818:20191022230938p:plain

f:id:ts0818:20191022231032p:plain

リフレクションを実施できました~!
Scala のプログラミングの実行は結局、CUIで実施してしまってますが...

scala-reflect」パッケージも依存性の追加で導入できてますね。(sbt を使ってる場合)

f:id:ts0818:20191022233046p:plain

まぁ、モヤモヤ感が半端ないですが、無事、目的は達成できたということで。

ちなみに、main メソッドは省略できるみたいです。

qiita.com

 

2019年10月23日(水)追記:↓ ここから

「Metals」で、Testの実行がサポートされたようです!まだ実験的らしいですが。

The latest 0.7.6+133-1dfcd90c-SNAPSHOT release from the current master supports running tests and main functions from VS Code. This functionality is still experimental, early feedback is welcome before we release the next stable version. Please open separate issues if you encounter problems with the latest SNAPSHOT

Run Scala Unit Tests from Visual Studio Code · Issue #1001 · scalameta/metals · GitHub

ただ、sbt を使う場合は普通に、Unit Test はできたみたい...

scalameta.org

Contributing to Metals · Metals

To run the unit tests open an sbt shell and run unit/test

sbt
# (recommended) run specific test suite, great for edit/test/debug workflows.
> metals/testOnly -- tests.DefinitionSuite
# run unit tests, modestly fast but still a bit too slow for edit/test/debug workflows.
> unit/test
# run slow integration tests, takes several minutes.
> slow/test
# (not recommended) run all tests, slow. It's better to target individual projects.
> test

Contributing to Metals · Metals

次回は、テスト実行にトライですかね。

2019年10月23日(水)追記:↑ ここまで 

 

今回はこのへんで。