TL; DR
- Xtendにはクラスにはクラスのメソッドを疑似的に追加する構文が用意されている
- 「拡張プロバイダ」(extension provider)が提供するメソッド
- クラス
A
にメソッドを追加したい場合- 拡張プロバイダのクラス
B
に、A
を第一引数に取るメソッドB#hoge(A)
を用意する - →
b.hoge(a)
をa.hoge()
と書くことができる
- 拡張プロバイダのクラス
// 拡張プロバイダ
extension static UserRepository repo = new UserRepository()
// インスタンスメソッド User#save() は存在しないが、代わりに UserRepository#save(User) が呼ばれる
user.save
はじめに
XtendはEclipse Foundationが開発しているプログラミング言語です。DSL開発フレームワーク Xtext の1機能として開発されています。
Javaへとトランスパイルされ、Javaよりも短い記述で実装することができます。
public class Main {
public static void main(String[] args) throws Exception {
System.out.println("Hello World!");
}
}
class Main {
def static void main(String[] args) {
println("Hello World!")
}
}
文法を調べてみると、クラスを疑似的に拡張する「メソッド拡張」の仕組みが面白かったので、本記事ではこの機能について紹介したいと思います。
検証バージョン
- Java: 21
- Xtext: 2.38.0
Xtendの環境構築
(構文について知りたい方は「拡張メソッドとは」まで読み飛ばしてください)
Eclipse Foundationが開発していることからもお察しの通り、Eclipse上のプラグインでトランスパイルを行います。
プラグインのインストール
以下のリンクのインストール方法に従いXtextのプラグインをインストールします。
SDK選択画面では Xtend
と Xtext
を選択します。
依存ライブラリの準備
今回はGradleで依存ライブラリをインストールしました。
インストールが完了したら新規プロジェクトを作成し、以下の build.gradle
を作成します。
plugins {
id "org.xtext.xtend" version "4.0.0"
}
repositories.mavenCentral()
dependencies {
implementation 'org.eclipse.xtend:org.eclipse.xtend.lib:2.38.0'
}
Eclipseにあまり慣れていないのでフォルダ配下で直接 gradle build
を実行しましたが、おそらくもっと良い方法があるはずです...
動かしてみる
それでは、実際にXtendで開発をしてみましょう。プロジェクトにXtendファイルを新規作成します。
Java同様、パッケージ名やクラス名とディレクトリ構成が対応しています。
package hello
class HelloWorld {
def static void main(String[] args) {
println("Hello World!")
}
}
保存してしばらくすると、自動的にJavaへのトランスパイルが始まります。生成物は xtend-gen
配下に出力されます。
package hello;
import org.eclipse.xtext.xbase.lib.InputOutput;
@SuppressWarnings("all")
public class HelloWorld {
public static void main(final String[] args) {
InputOutput.<String>println("Hello World!");
}
}
右クリック→Run as
→Java application
で動作を確認してみましょう。
Hello World!
想定どおり実行できました。
拡張メソッドとは
それでは、本題の「拡張メソッド」についてです。
概要
拡張メソッドは疑似的にクラスにメソッドを追加する(=クラスを拡張する)機能です。「Xtend」の名前の由来にもなっています。
拡張といってもRubyのようにクラス自体を変更するわけではなく、あくまで構文上メソッドのように扱えるというものです。
クラス A
にメソッド hoge
を追加したい場合は、別クラス Provider
に、A
を第一引数に取る hoge
メソッドを追加します。後は、 Provider
で拡張メソッドとして使用する宣言をする(後述)と、以下の 糖衣構文が使えるようになります。
provider.hoge(a, arg1, arg2)
a.hoge(arg1, arg2)
スタティックメソッドで使用
まずはスタティックメソッドの拡張メソッドを使ってみます。基本型には組み込みのプロバイダが用意されているため、リテラルに対してスタティックメソッドが使えます。
例えば String
には StringExtensions
の拡張メソッドが使用可能です。
"hello".toFirstUpper
トランスパイルするとこうなります。
StringExtensions.toFirstUpper("hello")
Hello
組み込みのプロバイダ以外では import
に extension
を付けることで拡張メソッドを使用可能です。
import static extension java.util.Collections.singletonList
インスタンスメソッドで使用
続いてインスタンスメソッドの場合です。スタティックメソッドの場合と異なり、インスタンスメソッドを使うには拡張プロバイダのインスタンスが必要です。
フィールドに extension
キーワードを指定することで、そのインスタンスの拡張メソッドを使用することができます。
extension static UserRepository repo = new UserRepository()
実例
インスタンスメソッドの拡張メソッドについて、もう少し実用的な例を見てみましょう。
エンティティクラスの永続化を行う際、こんなことを思ったことはないでしょうか?
- DBの処理をエンティティクラスの振る舞いとして持たせるのは汚い
- 一方、エンティティクラスに保存のメソッドが生えていたほうが楽に扱える
拡張メソッドを使えば、どちらも叶えることができます。
まずはエンティティクラス Person
を用意します。ここにはPersonの振る舞いだけ持たせています。
package hello
class User {
String name
int age
new(String name, int age) {
this.name = name
this.age = age
}
// NOTE: 最後に評価した式が暗黙的な戻り値になる
def boolean canDrink() {
age >= 20
}
override String toString() {
"{name: " + name + ", age: " + age + "}"
}
def sayHi() {
println("I'm " + name + ".")
}
}
続いて、 Person
を永続化する PersonRepository
クラスです。
package hello
class UserRepository {
def void save(User user) {
// 実際はDB等に保存する
println("user " + user + " is saved")
}
}
上記を利用するアプリケーションクラスを見てみます。
まずは、Application
のスタティックフィールドに UserRepository
のインスタンスを持たせて、拡張プロバイダとして利用します。
package hello
class Application {
extension static UserRepository repo = new UserRepository()
def run() {
val user = new User("Taro", 25)
// 無引数メソッドのカッコは省略可能
user.sayHi
println("able to drink: " + user.canDrink)
user.save
}
}
これにより、拡張メソッド save
が使用可能になり、他の User
のメソッドと同じように user.save
という形で呼び出すことができます。
余談ですが、上記のソースコードは以下のJavaへトランスパイルされました。user.save()
の部分は Application.repo.save(user)
に変換されています。
package hello;
import org.eclipse.xtext.xbase.lib.Extension;
import org.eclipse.xtext.xbase.lib.InputOutput;
@SuppressWarnings("all")
public class Application {
@Extension
private static UserRepository repo = new UserRepository();
public void run() {
final User user = new User("Taro", 25);
user.sayHi();
boolean _canDrink = user.canDrink();
String _plus = ("able to drink: " + Boolean.valueOf(_canDrink));
InputOutput.<String>println(_plus);
Application.repo.save(user);
}
}
おわりに
以上、Xtendの拡張メソッドについての紹介でした。クラスを動的に拡張できる言語はいくつか見たことがありますが、別クラスのメソッドを借りてくるという仕組みは初めて見たので新鮮でした。
憶測ですが、トランスパイルが複雑にならない範囲でメソッドを後付けで増やせる方法としてこのような仕組みが採用されたのかもしれません。