java:proxy
(java:proxy "fully.qualified.Interface"... callable)
rontolisp の callable を背後に持つ、指定インターフェース (1 つ以上) のホストインスタンスを生成します。各インターフェースメソッドは (callable "method-name" arg...) として callable に振り分けられます。つまり callable の第 1 引数には呼び出されたメソッドの名前(文字列)が渡され、残りの引数がそのメソッドの実引数になります。callable の戻り値はメソッドの戻り型へマーシャリングされます (void メソッドは無視します。返した関数はプロキシにしないので、インターフェースが期待される戻り値には java:proxy か java:reify のオブジェクトを返してください)。これにより rontolisp のラムダが Java のリスナーやコンパレータになります。単一メソッド (SAM) インターフェースではメソッド名は常に同じなので、慣習的に無視します (下記の例の method 引数)。JVM 専用の java 連携パッケージの一部であり、インタプリタと JVM クラスへのコンパイルの両方で利用できます (WASM バックエンドでは利用できません)。Java 連携ガイドを参照してください。
java.util.function.Supplier をラムダで実装し、その get メソッドを呼ぶとラムダが実行されて 42 を返します。
関数型インターフェース
java.util.function 系を含め、任意のインターフェースで動作します。ラムダの第 1 引数はメソッド名を受け取り、残りの引数がメソッド引数を受け取ります。インターフェースの単一抽象メソッド (SAM) のアリティに合わせてください。
| インターフェース | SAM | ラムダの形 |
|---|---|---|
Supplier | get() | (lambda (method) ...) |
Function | apply(x) | (lambda (method x) ...) |
Consumer | accept(x) | (lambda (method x) ...) |
Predicate | test(x) | (lambda (method x) ...) |
BiFunction | apply(a, b) | (lambda (method a b) ...) |
BinaryOperator | apply(a, b) | (lambda (method a b) ...) |
Comparator | compare(a, b) | (lambda (method a b) ...) |
JDK 側が SAM メソッドを呼び出す場合でもプロキシは動作します。たとえば HashMap.merge は、渡された BiFunction を呼び出して古い値と新しい値を合成します。
デフォルトメソッド
動的プロキシは BiFunction.andThen や Predicate.and といったデフォルトメソッドも含め、すべてのメソッド呼び出しを callable に転送します。これらを呼ぶと、インターフェース本来のデフォルト実装を実行する代わりに (callable "andThen" ...) としてラムダに振り分けられるため、(f.andThen g) のようなコンビネータは利用できません。代わりに単一抽象メソッド (apply/test/accept/get/compare) を呼んでください。
メソッドごとに別の関数で実装し、デフォルトメソッドの本体を保つには java:reify を使ってください。
Java の false
Java の false は nil として callable に渡ります。callable の後ろを :java-false で終えると、代わりに |false| を渡します (ガイドの Java の false を受け取る)。
:octets で終えると、callable に byte[] (引数、またはその要素) をそのオクテットを持つ (unsigned-byte 8) のベクタとして渡します。このベクタは Java の配列そのものなので、callable が格納した値は Java から読めます (ガイドのバイト列を受け取る)。:through と関数で終えると、Java は callable をその関数経由で呼び、渡す引数の先頭にはメソッド名が来ます (ガイドの関数を別の関数経由で呼ばせる)。
複数のインターフェース
callable より前の名前はすべて、1 つのオブジェクトが実装するインターフェースです。Java はそのオブジェクトをどのインターフェースとしても保持できます。2 つのインターフェースが宣言する同じメソッド名は、Java がどちらのインターフェース経由で呼んでも、その 1 つの名前で callable に届きます。
各名前はインターフェースで、1 回だけ指定します (java:proxy names interface I twice)。オブジェクトの toString は #<java-proxy I J> です。(java:call p "accept" 1) のようなオブジェクト自体への呼び出しは実行時にそのクラスで解決され、いずれかのインターフェースが期待される箇所に渡す呼び出しは実行前に解決されます。
コンパイル済みプログラムでの扱い
インターフェース名がリテラル文字列で、そのインターフェースがコンパイル時に見える java:proxy は、コンパイル時に生成するクラス (プログラムの隣の Prog$Proxy0.class) になります。インターフェースが期待される箇所に渡した関数も同じです。java.lang.reflect.Proxy を使わないので、そのプログラムは --java-static でコンパイルでき、GraalVM ネイティブイメージにも設定なしでビルドできます。表示は #<java Prog$Proxy0> で、インタプリタでは java.lang.reflect.Proxy のクラス名になります。実行時にインターフェース名を与える java:proxy はリフレクションブリッジを通ります。