大勢の人が当たり前に知っている事実だったはずなのに、全部デタラメだったという経験はあるだろうか?
例えば、映画「スター・ウォーズ」シリーズに登場するC-3POは銀色(本当は金色)という思い込みや、白雪姫の王妃のセリフは「鏡よ、鏡(Mirror, mirror on the wall)」であるといった思い込みだ(英語では本来「魔法の鏡よ / magic mirror on the wall」)。
これをマンデラ効果と呼ぶそうだ。
マンデラ効果とは何か?不特定多数が「偽の記憶」を真実だと思い込んでしまう集団的な誤解 (2019年8月26日) - エキサイトニュース
はい~、「鏡よ、鏡」って思ってたので、マンデラ効果の影響化にいたという...どうもボクです。
美しさを求める問いかけを投げかける対象先と言えば、そう!鏡 ですよね。
「Bueaty」+「Technology」=「BueatyTech」っていう分野もあるそうですね。
『心は自分を映し出す鏡!』ということで、今回は、Scala のリフレクション について学んでいこうかと。
そして、
⇧ プログラミングの世界にも、新たな天才が!
やっぱり、才能ある人間はいるんですね。
片や常に泥沼にハマり続ける日々を送るわたくしのような人間もおると(涙)。
というわけで、やる気が一気に落ち込んだところで、レッツトライ~。
何回も躓いているので、もし参考にしていただける場合は、一旦、最後まで通してお読みいただいたほうが良いかと~。
リフレクションって?
Wikipediaさ~ん!
日本語では自己言及と呼ばれる。通常リフレクションというと動的(実行時)リフレクションのことを指すが、静的(コンパイル時)リフレクションをサポートするプログラミング言語もある。
リフレクションはSmalltalk、Java、.NET Frameworkのような仮想機械やインタプリタ上で実行されることを想定した言語でサポートされることが多く、C言語のような機械語として出力されることを想定した言語でサポートされることは少ない。
一般に、リフレクションとはオブジェクトがそれ自身の構造や計算上の意味を取得することを可能にするものである。リフレクションによるプログラミングパラダイムをリフレクティブプログラミング (reflective programming) という。
通常、プログラムのソースコードがコンパイルされると、プログラムの構造などの情報は低レベルコード(アセンブリ言語など)に変換される過程で失われてしまう。リフレクションをサポートする場合、そのような情報は生成されるコードの中にメタデータとして保存される。
⇧ う~ん、「プログラムの構造などの情報」っていうのが何を指しているんだい?な情弱な私ですが、まとめると、『リフレクションは、「プログラムの構造などの情報を、メタデータとして保持し、それを利用する」』ってことですかね。
Wikipedia さんで、Java についての例を抜粋させていただくと、
■リフレクション使わない
// リフレクションなし Foo foo = new Foo(); foo.hello();
■リフレクション使う
// リフレクション Class cl = Class.forName("Foo"); Method method = cl.getMethod("hello"); method.invoke(cl.newInstance());
っていう違いがあるんだけども、
前者の例では、クラス名やメソッド名がハードコーディングされているので実行時に他のクラスに変更するのは不可能である。リフレクションを用いた後者の例では、それらを実行時に容易に変更することができる。
しかしその一方で、後者は読みにくく、またコンパイル時チェックの恩恵も得られない。つまり、もしFoo
クラスが存在しなかったとしたら前者のコードではコンパイル時にエラーとなるが、後者のコードでは実行するまでエラーが発生しない。
⇧ つまり、リフレクションを使うことで何ができるかと言うと、『クラス名やメソッド名を操作できる』ってことですかね。
使い道としては、単体テストなんかには向いてますかね。
ちなみに、Java 9 から、非推奨になったリストの影響で、リフレクションについても上手く動かない問題が出てくるようですかね。
そして、Javaの話が続いてしまいますが、リフレクションにも影響してくるのが、Javaだとジェネリックス、イレイジャってものらしく、
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 MIZER、FANATIC◇CRISIS、SHAZNAと共に「ヴィジュアル四天王」と呼ばれていた。
⇧ あ...「Janne Da Arc」は「ヴィジュアル四天王」じゃないんだ...
はい...すみませんでした...
そんじゃあ、本題。
Scalaにおけるリフレクションってどんなん?
リフレクション (reflection) とは、プログラムが実行時において自身をインスペクトしたり、変更したりできる能力のことだ。
それはオブジェクト指向、関数型、論理プログラミングなど様々なプログラミングのパラダイムに渡って長い歴史を持つ。
それぞれのパラダイムが、時として顕著に異なる方向性に向けて現在のリフレクションを進化させてきた。
LISP/Scheme のような関数型の言語が動的なインタープリタを可能とすることに比重を置いてきたのに対し、Java のようなオブジェクト指向言語は実行時におけるクラスメンバのインスペクションや呼び出しを実現するための実行時リフレクションに主な比重を置いてきた。
複数の言語やパラダイムに渡る主要なリフレクションの用例を以下に 3つ挙げる:
Scala 2.10 までは Scala は独自のリフレクション機能を持っていなかった。 代わりに、Java リフレクションを使って (1) の実行時リフレクションのうちの非常に限定的な一部の機能のみを使うことができた。
しかし、存在型、高カインド型、パス依存型、抽象型など多くの Scala 独自の型の情報はそのままの Java リフレクションのもとでは実行時に復元不可能だった。 これらの Scala 独自の型に加え、Java リフレクションはコンパイル時にジェネリックである Java 型の実行時型情報も復元できない。
⇧ Scala には、リフレクションって無かったらしく、Javaのリフレクションを使ってたんだけど、いろいろ不足があったようで、
Scala 2.10 は、Scala 独自型とジェネリック型に対する Java の実行時リフレクションの欠点に対処するためだけではなく、 汎用リフレクション機能を持ったより強力なツールボックスを追加するために新しいリフレクションのライブラリを導入する。 Scala 型とジェネリックスに対する完全な実行時リフレクション (1) の他に、 Scala 2.10 はマクロ という形でコンパイル時リフレクション機能 (2) と、 Scala の式を抽象構文木へとレイファイ (reify) する機能 (3) も提供する。
⇧ そんじゃあ、作っちゃえ、ということで、ここに爆誕。
Scala 2.10 から独自のリフレクションAPIを生み出してしまったのだと。
Scalaでリフレクションを可能にするAPIたち
まぁ、Scala 2.10より、Scalaの標準APIでリフレクションが可能になったということで、リフレクションするのにAPIはいくつ必要なのかと。
Javaではリフレクションに、以下のようなクラスを使います。
リフレクション対象の各要素がそのままクラスになっていて、
クラスに関する情報の参照やクラスに対する操作はClassを使い、
メソッドをコントロールしたければMethodを、と素直でわかりやすいです。
それに対し、Scalaでは以下のようなクラスを使います。
- scala.reflect.runtime.universe.Type
- scala.reflect.runtime.universe.Symbol
- scala.reflect.runtime.universe.Mirror
Typeは型情報の参照に、Mirrorは対象の操作に、それぞれ使います。
SymbolはTypeやMirrorと相互変換できたりする、リフレクション関連クラスを統合するモノです。
⇧ 上記サイトによりますと、メインで使っていくAPIの数は、Java、Scala 共に3つのようですが、Scala のリフレクションは難易度が高そうです(涙)。
Javaのリフレクションは、Classの情報を取得した後に、その他の情報を取得するって方法が一般的かと思われますが、Scala の場合は、
の2つの方法でClassの情報を取得できるらしい。
Scala でリフレクションしてみる
その前に、Scala のプログラミングを動かすために何が必要なのか。
- JDK(Java Development Kit) 8 以上
- sbt、または、Scala
⇧ 最低限、上記が必要。sbt は、Scalaのビルドツールなので、sbt をインストールしておけば、Scalaも、sbt に同梱されてくる?らしい。
なので、「JDK 8 以上」「sbt」の2つがインストールされていれば、Scala を始めることができるかと。
使用できるバージョンとかは、
⇧ 決まってるので要注意。
インストールしてない場合は、
⇧ 上記サイト様を参考にインストールしてみてください。
インストールし終わったら、Javaの確認。
sbt は、Scalaのプロジェクトを作成するまでは、コマンドでバージョン確認ができないので、インストールされてるかは、エクスプローラーから確認するしかないかと...
おそらく、バージョンは「0.11.3」ってことかと。
とりあえず、Scala プロジェクト用のディレクトリを作成で。
んで、
⇧ Scala プロジェクト用のディレクトリに「build.sbt」ってファイルを作成しておく必要があると。作成します。
あとは、「*.scala」ファイルを作成して、compile とかすれば、CUI上でScalaのプログラミングを実行はできるんですが、せっかくなんで、GUI上でやりたいと。
って選択肢になるかと。実質、IDEだと、現状、IntelliJ IDEA の一択かと。ソースコードエディタは他にもScala対応してるかもですが、分からないっす...
今回は、Visual Studio Code を使っていこうかと。インストールしてない方はインストールしちゃいましょう。
んで、ソースコードエディタ使う場合に、拡張機能としてインストールするものとして、最近だと、「Metals」ってのがイケてるらしい。
Metals = Meta (from Scalameta) + LS (from Language Server)
GitHub - scalameta/metals: Scala language server with rich IDE features 🚀
⇧ 上記サイト様によりますと、まだ、発展途上の感は否めないらしい、というか発展してもらわないと困る問題はあるとのこと。
ちなみに、公式の説明だと、
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.
⇧ 『「BSP(Build Server Protocol)」って仕様を満たしてるビルドツールなら「Metals」で動くよ』ってことらしい。
「Metals」は、「Scala language server with rich IDE features」ってあるように、IDEみたいな機能をソースコードエディタでも使えるようにしてくれるってことみたい。
そしたらば、 ここからは、Visual Studio Code 起動で。「ファイル(F)」>「フォルダーを開く...」で、
作成しておいた、Scalaプロジェクト用のディレクトリを選択
のアイコンをクリックで。
「Scala(Metals)」って拡張機能をインストールするんですが、
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.
⇧ 拡張機能のバッティングが起こるらしいので、「Scala Language Server」「Scala(sbt)」って拡張機能がインストールされてしまっている場合は無効化しておきましょう。
んで、インストールされると。
⇧ はい、エラー。sbt のバージョンが足りないって...どこにも、sbt の要件とか書いとらんやんけ~!
致し方ないので、sbt のバージョンアップで。chocolatey でインストールしてる場合は、chocolatey でアップデートできるらしい。
更新されてるかと。
Visual Studio Code 再起動してみたら、
⇧ まさかの、「sbt 1.2.8」ジャストのバージョン指定。
⇧ multiple versions でのインストールを試みるが、
「sbt 1.3.3」がアンインストールされるという...
まぁ、今回は、使わないから良いけどさ...
Visual Studio Code 再起動で。
エラーは出なくなった。
のアイコンが追加されてるのでクリックで、「BUILD」>「import build」をクリックで。
駄目でした...
どうやら、Scalaのプロジェクト用のディレクトリで、「sbt」ってコマンドを実行しておく必要があったらしい。
そしたら、一旦、「exit」で。
で、Visual Studio Code を再起動すると、「Import build」ってダイアログに表示されるんで、クリックで。
整ったらしい。Scalaのバージョンがギリギリらしい。
まぁ、残念ながら、Java、Scalaの対応表はあるんだけども、欲しいのは、Java、Scala、sbt の対応表でしょうに...
とりあえず、拡張子が「.scala」のファイルを作成していきますか。
そしたらば、ソースコードを記述していきましょう。
とりあえず、「Hello World!」を表示する内容を。
object TestReflection { def main(args: Array[String]):Unit = { println("Hello World!") } }
そしたら、「表示(V)」>「ターミナル」で。
ターミナルで「sbt」シェルを開く。
そしたら、「run」で。
で、エラー。
デフォルトでは、sbtはsrc / main / scalaディレクトリの下のソースを管理します。そこに、実行するAppオブジェクトがあります。
sbtでは、runはsrc / main / scala配下のすべてのソースからアプリケーションを検索します。
⇧ どうやら、ディレクトリ構成が駄目らしい...知らんがな
以下のようなディレクトリ構成に修正します。
再び、ターミナルで実行します。
今度は動きました~!
では、本題のリフレクションで。
Scalaのリフレクションを使用するには、「scala-reflect.jar」というJARファイルが必要です。
Scalaのオフィシャルサイトで配布されているディストリビューションには、libディレクトリに含まれているのでScalaの各種コマンド(scala/scalac/fsc)で使う分には何も用意する必要がありません。
ただ、sbtを使っている場合は、依存関係の追加が必要です。
⇧ どうやら、「build.sbt」に、自分の使ってるScalaのバージョンに合致する「scala-reflect.jar」を使うよって明示しないと駄目みたい。
ターミナルで、「sbt」のシェル内にいる状態で、「about」とコマンドすれば、Scalaのバージョンが表示できます。
そんじゃ、「build.sbt」に追記で。
⇧ 記述の仕方は、上記を参考。
それぞれのバージョンは、ご自身の環境のものに合わせてください。
version := "1.2.8" libraryDependencies += "org.scala-lang" % "scala-reflect" % "2.12.7"
そしたらば、「Metals」のダイアログが出るんで「Import changes」をクリック。
んで、ソースコードを修正します。
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)) } }
んで、実行してみる。
はい、エラー。
「object runtime is not a member of package reflect」って言われてもね...
どうやら、「build.sbt」の変更を反映させるには、「Metals」のほうの「build import」の再実行とは別に、「sbt」シェルも再起動しなきゃならんかったみたい...知らんがな。
ということで、一旦、「exit」し、再度「sbt」シェルを起動。
そしたらば、「run」で、「TestReflection.scala」を実施で。
リフレクションを実施できました~!
Scala のプログラミングの実行は結局、CUIで実施してしまってますが...
「scala-reflect」パッケージも依存性の追加で導入できてますね。(sbt を使ってる場合)
まぁ、モヤモヤ感が半端ないですが、無事、目的は達成できたということで。
ちなみに、main メソッドは省略できるみたいです。
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 はできたみたい...
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
次回は、テスト実行にトライですかね。
2019年10月23日(水)追記:↑ ここまで
今回はこのへんで。