Emulating C# LINQ in Java using Code Reflection

原文はこちら。
The original article was written by Paul Sandoz (Architect, Java at Oracle).
https://openjdk.org/projects/babylon/articles/linq

この記事では、C# の統合言語クエリ (LINQ) の機能をCode Reflectionを使ってJavaでエミュレートする方法を説明します。具体的には、LINQクエリ(C#式)からSQLステートメントへの変換を可能にするLINQの機能です。

統合言語クエリ / Language Integrated Query (LINQ)
https://learn.microsoft.com/dotnet/csharp/linq/

Code Reflectionは、OpenJDK Project Babylonの下で研究開発されているJavaプラットフォームの機能です。

Project Babylon
https://openjdk.org/projects/babylon/

Code Reflectionの概念とAPIを順次紹介していきます。説明は網羅的でも詳細でもなく、Code Reflectionとその機能を直感的に理解できるようにデザインされています。

C# LINQ

C#のLINQのガイドには以下のような記述があります。

Language-Integrated Query (LINQ) is the name for a set of technologies based on the integration of query capabilities directly into the C# language. Traditionally, queries against data are expressed as simple strings without type checking at compile time or IntelliSense support. Furthermore, you have to learn a different query language for each type of data source: SQL databases, XML documents, various Web services, and so on. With LINQ, a query is a first-class language construct, just like classes, methods, and events.
(統合言語クエリ (LINQ) は、C# 言語への直接的なクエリ機能の統合に基づくテクノロジのセットの名前です。 これまでは、データに対するクエリは、コンパイル時の型チェックや IntelliSense のサポートがない単純な文字列として表現されてきました。 さらに、SQL データベース、XML ドキュメント、さまざまな Web サービスなど、各種データ ソースの異なるクエリ言語を学習する必要があります。 LINQ では、クエリは、クラス、メソッド、イベントと同様に、ファースト クラスの言語コンストラクトです。)

統合言語クエリ / Language Integrated Query (LINQ) https://learn.microsoft.com/dotnet/csharp/linq/

以下はクエリ構文を使うデータベースへのLINQクエリの簡単な例です。

DB db = ...;

// Query for customers in London.
IQueryable<String> custQuery =
    from cust in db.Customers
    where cust.City == "London"
    select cust.ContactName;

このクエリ構文は、LINQクエリの読み書きを簡単にするためのsyntax sugarですが、実際にはメソッド呼び出しのショートカットに過ぎません。

DBクラスには、CustomersのようなSQLテーブルをモデル化するクラスが含まれています。CityContactNameといったプロパティでテーブルの行をモデル化しています。

同じLINQクエリをメソッド構文を使って書くことができます。

DB db = ...;

// Query for customers in London.
IQueryable<String> custQuery =
    db.Customers
    .Where(cust => cust.City == "London")
    .Select(cust => cust.ContactName);

LINQクエリ(IQuerableのインスタンス)にはC#のプロパティExpressionがあり、その値はLINQクエリの記号表現です。

IQueryable.Expression Property
https://learn.microsoft.com/dotnet/api/system.linq.iqueryable.expression?view=net-8.0#system-linq-iqueryable-expression

C#では、このようなシンボリック表現は式ツリー(expression trees)と呼ばれます。

Expression Trees
https://learn.microsoft.com/dotnet/csharp/advanced-topics/expression-trees

上記のコードでは、クエリcustQueryの式ツリーは、WhereメソッドとSelectメソッドに渡されたラムダ式の式ツリーと、これらのメソッドへの呼び出し式の式ツリーで構成されています。

LINQクエリのExpressionプロパティである式ツリーには、嬉しい特徴があります。式ツリーを評価すると、新しいLINQクエリが生成され、その式ツリーは評価されたものと等しくなります。つまり、数学が好きな人のために、Qをクエリ、Q.eをクエリの式、Eを(式からクエリへの)評価関数とすると、

E(Q. e) = Q

となります。どんなに喜ばしいことでも、あまり実用的ではありません。このような式ツリーは、記号的に処理され、変換されるように設計されています。例えば、SQLのような異なるプログラミング・ドメインに変換することで、クエリの式ツリーはSQL文に変換されます。

私たちはJavaにおけるこの側面に焦点を当て、Code Reflectionの機能を使用して生成されるJavaコードの記号的表現であるコード・モデルを等価な式プロパティとするクエリを作成します。

Emulating LINQ-like query expressions in Java

エミュレーション目的で、同じクエリを Java で以下のように表現できます。

// A record modeling a table with three columns, one for each component
record Customer(String contactName, String phone, String city) {
}

QueryProvider qp = new linq.TestQueryProvider();

// Find all customers based in London, and return their names
QueryResult<Stream<String>> results = qp.newQuery(Customer.class)
        .where(c -> c.city.equals("London"))
        .select(c -> c.contactName)
        .elements();

Customerというrecordクラスを使用してSQLテーブルをモデル化し、そのrecordクラスのコンポーネントがそのテーブルの行に対応します。Javaコードが、メソッド構文を使用したC# LINQクエリと非常によく似ていることがわかります。

以下のセクションでは、Code Reflectionを使用した実装方法について説明します。

概念実証の実装が、Babylonリポジトリにテスト目的で利用可能です。この実装は、まだ完全ではありません。

https://github.com/openjdk/babylon/tree/code-reflection/test/jdk/java/lang/reflect/code/linq

fluent queryを個々の文に展開し、型を確認できるようにしてみましょう。

Queryable<Customer> allCustomers = qp.query(Customer.class);
Queryable<Customer> londonCustomers = allCustomers.where(c -> c.city.equals("London"));
Queryable<String> londonCustomerNames = londonCustomers.select(c -> c.contactName);
QueryResult<Stream<String>> results = londonCustomerNames.elements();

最初の 3 つのメソッド呼び出しはQueryableのインスタンスを生成し、最後の呼び出しはQueryResultのインスタンスを生成します。各インスタンスはクエリのシンボリック表現(コードモデル)を持ちます。これについては後で説明します。

まず、Queryable<Customer>のインスタンスを返す新しいクエリを作成します。そこから、whereメソッドの呼び出しで、ロンドン市内にいる顧客をフィルタリングし、customercityコンポーネントが文字列 “London”と等しい場合にtrueを返すラムダ式を受け入れます。次に、customercontactNameコンポーネントを返すラムダ式を受け付けるselectメソッドの呼び出しで、customercontactNameにマッピングします。最後に、elementsメソッドの呼び出しでクエリ結果を生成します。これは、結果をどのように消費するかを示すもので、この場合はcustomercontactNameのストリームです。

whereメソッドとselectメソッドのシグネチャは以下のとおりです。

default Queryable<T> where(QuotablePredicate<T> f) { /* ... */ }

default <R> Queryable<R> select(QuotableFunction<T, R> f) { /* ... */ }

QuotablePredicateQuotableFunctionは、PredicateFunctionCode Reflectionインターフェースjava.lang.reflect.code.Quotableを拡張した関数型インターフェースです。

以下は、Quotableの宣言です。

@FunctionalInterface
public interface QuotablePredicate<T> extends Quotable, Predicate<T> {
}

ラムダ式がquotableな関数型インターフェースをターゲットとする場合、ラムダ式のコードモデルはソース・コンパイラが作成し、quotableなインスタンスを使って実行時にアクセスできるようになります。シリアライズ可能なラムダ式(Serializableを使用)を取得する方法と同様に、コードモデルを取得できるquotableなラムダ式を取得することができます。

Obtaining a code model

まず、quotableなラムダ式のコードモデルを取得することに焦点を当てましょう。クエリのコードモデルの構成および作成方法は後の節で説明します。

ある顧客がロンドン市内にいるかどうかをチェックするQuotablePredicateインスタンスのコードモデルは、以下のようにして得られます。

QuotablePredicate<Customer> qp = c -> c.city.equals("London");
Quoted q = qp.quoted();
Op op = q.op();
CoreOps.LambdaOp qpcm = (CoreOps.LambdaOp) op;

Quotable::quotedメソッドを呼び出してQuotedのインスタンスを取得し、そこからQuoted::opを呼び出してコード・モデルを取得します。この場合、ラムダ式は値を捕捉しませんが、もし捕捉していれば、quotedインスタンスからそれらの値を取得できます。

qpのコード・モデルは、ラムダ式をモデル化するラムダ式オペレーションに対応するCoreOps.LambdaOpのインスタンスとして表されます。Quoteはラムダ式のQuoteだけに限定されない可能性があるため、まず、すべての操作のトップレベルの型であるjava.lang.reflect.code.Opのインスタンスとしてコードモデルを取得し、そこからダウンキャストします。

Explaining the code model

コードモデルは、オペレーション(演算、operation)、ボディ(本体、body)、ブロック(block)を含むツリーです。オペレーションには0個以上のボディが含まれます。ボディには、1個以上のブロックが含まれます。ブロックには、1個以上の一連のオペレーションが含まれます。ブロックでは、0個以上のブロック・パラメーター(値)を宣言できます。オペレーションは、オペレーションの結果(値)を宣言します。オペレーションは、オペランドとして値を使用できますが、それは当該値の宣言後に限ります。

この単純なツリー構造を使用すると、多くのJava言語コンストラクトをモデル化するオペレーション(operation)を定義でき、それゆえ、多くのJavaプログラムをモデル化するコード・モデルを作成できます。これは最初は意外に見えるかもしれません。算術演算のような従来の意味でのオペレーションという用語に馴染みがある読者が多いかもしれません。しかし、上述の構造を考えれば、このような従来の意味に限定する必要はありません。

  • オペレーションセマンティクスが関数を宣言するオペレーション(CoreOps.FuncOpのインスタンス)
  • Javaラムダ式をモデル化するオペレーション(CoreOps.LambdaOpのインスタンス)
  • またはJavaのtry文をモデル化するオペレーション(ExtendedOps.JavaTryOpのインスタンス)

を自由に定義できます。

qpのコードモデルはどのように見えるでしょうか?メモリ内の形式(CoreOps.LambdaOpのインスタンス)をテキスト形式にシリアライズできます。

System.out.println(qpcm.toText());

以下のようなテキストが表示されます。

lambda (%0 : linq.TestLinq$Customer)boolean -> {
    %1 : Var<linq.TestLinq$Customer> = var %0 @"c";
    %2 : linq.TestLinq$Customer = var.load %1;
    %3 : java.lang.String = field.load %2 @"linq.TestLinq$Customer::city()java.lang.String";
    %4 : java.lang.String = constant @"London";
    %5 : boolean = invoke %3 %4 @"java.lang.String::equals(java.lang.Object)boolean";
    return %5;
};

テキスト形式から、コードモデルのルートがラムダ式 (lambda) のオペレーションであることがわかります。ラムダ式のオペレーションは、他のすべてのオペレーションと同様に演算結果を持ちますが、ツリーのルートであるため、それを提示する必要はありません。

ラムダのような式は、ラムダ式のオペレーションの単一のボディと、エントリブロックと呼ばれるボディの最初で唯一のブロックの融合を表します。そして、エントリーブロックには一連のオペレーションがあります。各オペレーションには、対応するクラスのインスタンスがインメモリ形式で存在し、これらはすべて抽象クラスjava.lang.reflect.code.Opを拡張したものです。

エントリ・ブロックには、qpのパラメータをモデル化するlinq.TestLinq$Customer型で記述された%0c に対応)というブロック・パラメータが1個あります。このパラメータは、別のオペレーションのオペランドとして使用します。多くのオペレーションは、演算結果、例えばフィールド・ロード操作の結果である%3を生成し、それは後続のオペレーションのオペランドなどとして使われます。returnオペレーションにも他のオペレーションと同様に結果がありますが、その結果は意味のある使い方はできないので、提示しません。

コードモデルには静的単一代入(SSA、Static Single-Assignment)という性質があります。例えば、値 %3 は決して変更できません。変数宣言は、値(ボックス)を保持する値を生成する操作としてモデル化され、アクセス操作はそのボックスにロードまたはストアします。

(読者の中には、これがMLIRに非常に似ていると思う人もいるかもしれませんが、それは意図的なものです)

MLIR – Multi-Level IR Compiler Framework
https://mlir.llvm.org/

ラムダ式、変数(メソッドのパラメータやローカル変数)、変数へのアクセス、フィールドへのアクセス、メソッドの呼び出し(例えば、String::equalsメソッドへのアクセス)といったJava言語の構成要素が、オペレーションによってどのようにモデル化されているかがわかります。

フィールドのロードおよび呼び出しのオペレーションは、反射的オペレーション(reflective operation)と呼ばれます。これらは、メソッドハンドルの構築や、バイトコード命令とその定数プールエントリの生成に使用できる記述子(@ を先頭に持つオペレーション属性)を宣言します。

Code models of queries

それぞれのクエリ可能なインスタンスとクエリ結果のインスタンスについて、そのコードモデルを取得し、テキスト形式を出力できます。

最初のクエリ可能なインスタンスの例です。

Queryable<Customer> allCustomers = qp.query(Customer.class);
System.out.println(allCustomers.expression().toText());
func @"query" (%0 : linq.Queryable<linq.TestLinq$Customer>)linq.Queryable<linq.TestLinq$Customer> -> {
    return %0;
};

このクエリ可能インスタンスは、ルートが関数宣言操作であるコードモデルを持ちます。関数宣言オペレーションは、以前に説明したラムダ式のオペレーションと同様の構造です。このコードモデルは、パラメータ%0を返すアイデンティティ関数を表します。パラメータの型表現は、Customer型をカプセル化するlinq.Queryable<linq.TestLinq$Customer>です。そのため、どのテーブルをクエリしているかがわかります。

Queryable<Customer> londonCustomers = allCustomers.where(c -> c.city.equals("London"));
System.out.println(londonCustomers.expression().toText());

2番目のクエリ可能インスタンスの例です。

func @"query" (%0 : linq.Queryable<linq.TestLinq$Customer>)linq.Queryable<linq.TestLinq$Customer> -> {
    %1 : linq.QuotablePredicate<linq.TestLinq$Customer> = lambda (%2 : linq.TestLinq$Customer)boolean -> {
        %3 : Var<linq.TestLinq$Customer> = var %2 @"c";
        %4 : linq.TestLinq$Customer = var.load %3;
        %5 : java.lang.String = field.load %4 @"linq.TestLinq$Customer::city()java.lang.String";
        %6 : java.lang.String = constant @"London";
        %7 : boolean = invoke %5 %6 @"java.lang.String::equals(java.lang.Object)boolean";
        return %7;
    };
    %8 : linq.Queryable<linq.TestLinq$Customer> = invoke %0 %1 @"linq.Queryable::where(linq.QuotablePredicate)linq.Queryable";
    return %8;
};

2番目のクエリ可能インスタンスのコードモデルには、結果がwhereメソッドの呼び出しに渡され、その結果が返されるラムダ式が含まれています。

3番目のクエリ可能インスタンスの例です。

Queryable<String> londonCustomerNames = londonCustomers.select(c -> c.contactName);
System.out.println(londonCustomerNames.expression().toText());
func @"query" (%0 : linq.Queryable<linq.TestLinq$Customer>)linq.Queryable<java.lang.String> -> {
    %1 : linq.QuotablePredicate<linq.TestLinq$Customer> = lambda (%2 : linq.TestLinq$Customer)boolean -> {
        %3 : Var<linq.TestLinq$Customer> = var %2 @"c";
        %4 : linq.TestLinq$Customer = var.load %3;
        %5 : java.lang.String = field.load %4 @"linq.TestLinq$Customer::city()java.lang.String";
        %6 : java.lang.String = constant @"London";
        %7 : boolean = invoke %5 %6 @"java.lang.String::equals(java.lang.Object)boolean";
        return %7;
    };
    %8 : linq.Queryable<linq.TestLinq$Customer> = invoke %0 %1 @"linq.Queryable::where(linq.QuotablePredicate)linq.Queryable";
    %9 : linq.QuotableFunction<linq.TestLinq$Customer, java.lang.String> = lambda (%10 : linq.TestLinq$Customer)java.lang.String -> {
        %11 : Var<linq.TestLinq$Customer> = var %10 @"c";
        %12 : linq.TestLinq$Customer = var.load %11;
        %13 : java.lang.String = field.load %12 @"linq.TestLinq$Customer::contactName()java.lang.String";
        return %13;
    };
    %14 : linq.Queryable<java.lang.String> = invoke %8 %9 @"linq.Queryable::select(linq.QuotableFunction)linq.Queryable";
    return %14;
};

3番目のクエリ可能なインスタンスのコードモデルにはラムダ式が含まれています。このラムダ式の結果をselectメソッドの呼び出しに渡し、そのselectメソッドの結果を返します。

クエリ可能なコードモデルは、クエリを構築するためのステップを再定義するという、あるパターンが見えてきましたね。

クエリ結果は以下のようになります。

QueryResult<Stream<String>> results = londonCustomerNames.elements();
System.out.println(results.expression().toText());
func @"queryResult" (%0 : linq.Queryable<linq.TestLinq$Customer>)linq.QueryResult<java.util.stream.Stream<java.lang.String>> -> {
    %1 : linq.QuotablePredicate<linq.TestLinq$Customer> = lambda (%2 : linq.TestLinq$Customer)boolean -> {
        %3 : Var<linq.TestLinq$Customer> = var %2 @"c";
        %4 : linq.TestLinq$Customer = var.load %3;
        %5 : java.lang.String = field.load %4 @"linq.TestLinq$Customer::city()java.lang.String";
        %6 : java.lang.String = constant @"London";
        %7 : boolean = invoke %5 %6 @"java.lang.String::equals(java.lang.Object)boolean";
        return %7;
    };
    %8 : linq.Queryable<linq.TestLinq$Customer> = invoke %0 %1 @"linq.Queryable::where(linq.QuotablePredicate)linq.Queryable";
    %9 : linq.QuotableFunction<linq.TestLinq$Customer, java.lang.String> = lambda (%10 : linq.TestLinq$Customer)java.lang.String -> {
        %11 : Var<linq.TestLinq$Customer> = var %10 @"c";
        %12 : linq.TestLinq$Customer = var.load %11;
        %13 : java.lang.String = field.load %12 @"linq.TestLinq$Customer::contactName()java.lang.String";
        return %13;
    };
    %14 : linq.Queryable<java.lang.String> = invoke %8 %9 @"linq.Queryable::select(linq.QuotableFunction)linq.Queryable";
    %15 : linq.QueryResult<java.util.stream.Stream<java.lang.String>> = invoke %14 @"linq.Queryable::elements()linq.QueryResult";
    return %15;
};

最後に、クエリ結果のコードモデルには、結果が返されるelementsメソッドの呼び出しが含まれます。

前のコードモデルは、以下のメソッドのコードモデルと(プログラムの意味において)等価になります。

@CodeReflection
static QueryResult<Stream<String>> queryResult(Queryable<Customer> q) {
    return q.where(c -> c.city.equals("London"))
            .select(c -> c.contactName)
            .elements();
}

resultsのコードモデルを解釈し、最初のクエリ可能なインスタンスに渡せば、同じコードモデルを持つクエリ可能なインスタンスが自然に生成されます(あるいは、コードモデルをコンパイルして実行することもできます)。

QueryResult<Stream<String>> resultsIterpreted = (QueryResult<Stream<String>>) Interpreter.invoke(MethodHandles.lookup(),
        results.expression(), qp.query(Customer.class));
System.out.println(resultsIterpreted.expression().toText());

assert results.expression().toText().equals(resultsIterpreted.expression().toText());

これは明らかに実用的ではありません。コードモデルは変換されることを目的として設計されています。このコードモデルには、SQLクエリに変換するのに十分な記号情報が含まれています。

Building code models for queries

コードモデルは不変(immutable)です。コードモデルは、既存のコードモデルを作成するか、変換することによって作成できます。変換は入力コードモデルを受け取り、出力コードモデルを作成します。入力コードモデルで遭遇する各入力操作に対して、その操作を出力コードモデルのビルダーに追加するか(コピー)、追加しないか(削除)、新しい出力操作を追加するか(置換または追加)を選択します。

Queryableメソッドの実装であるwhereselectelementsは、先行するコードモデルを、追加のオペレーションを持つ新しいコードモデルに変換します。

以下はwhereメソッドの実装です。

@SuppressWarnings("unchecked")
default Queryable<T> where(QuotablePredicate<T> f) {
    CoreOps.LambdaOp l = (CoreOps.LambdaOp) f.quoted().op();
    return (Queryable<T>) insertQuery(elementType(), "where", l);
}

先に示したように、ラムダ式のコードモデルを取得し、要素の型、メソッドの名前とともに、その結果を返すメソッドinsertQueryに渡します。

insertQueryメソッドは変換を実行します。

private Queryable<?> insertQuery(JavaType elementType, String methodName, LambdaOp lambdaOp) {
    // Copy function expression, replacing return operation
    FuncOp queryExpression = expression();
    JavaType queryableType = type(Queryable.TYPE, elementType);
    FuncOp nextQueryExpression = func("query",
            methodType(queryableType, queryExpression.funcDescriptor().parameters()))
            .body(b -> b.inline(queryExpression, b.parameters(), (block, query) -> {
                Op.Result fi = block.op(lambdaOp);

                MethodDesc md = method(Queryable.TYPE, methodName,
                        methodType(Queryable.TYPE, ((JavaType) lambdaOp.functionalInterface()).rawType()));
                Op.Result queryable = block.op(invoke(queryableType, md, query, fi));

                block.op(_return(queryable));
            }));

    return provider().createQuery(elementType, nextQueryExpression);
}

まず、static factoryメソッドであるCoreOps.funcを使用して新しい関数宣言オペレーションを作成します。関数のパラメータと戻り値の型を記述するmethod descritionを作成した上で、bodyメソッドを呼び出して関数宣言オペレーションの本体を作成します。メソッドbodyの実装は、関数本体のエントリーブロックビルダーでラムダ式を呼び出し、そこからオペレーションを追加できます。

このケースでは、事前に入力されたコードモデル(以前に生成された関数宣言オペレーション)を、これから作成する出力コードモデルにインライン化します。インライン化は、入力関数宣言オペレーションの本体の内容をコピーし、入力のreturnオペレーションをインライン・メソッドに渡されたラムダ式によって作成されるものに置き換えて、queryExpressionを変換します。

入力のreturnオペレーションを、ラムダ式オペレーションのコピー、クエリ可能メソッドへの呼び出し、および呼び出し結果のreturnに置き換えます。ラムダ式オペレーションは、キャプチャしない限り自由にコピーできます。

インライン化しているので、入力関数のパラメータを出力関数のパラメータにマッピングする必要もあります(具体的には、どちらの場合も関数宣言オペレーションの本体のエントリブロックのブロックパラメータ)。なので、マッピングを行うインラインメソッドにも後者を渡します。

最後に、出力コード・モデルを持つQueryableの新しいインスタンスを作成します。

コメントを残す

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください