JVM系言語Xtendの拡張メソッドを試してみる

# TL; DR

- Xtendにはクラスにはクラスのメソッドを疑似的に追加する構文が用意されている
	- 「拡張プロバイダ」(extension provider)が提供するメソッド
	- クラス `A` にメソッドを追加したい場合
		- 拡張プロバイダのクラス `B` に、`A` を第一引数に取るメソッド `B#hoge(A)` を用意する
		- → `b.hoge(a)` を `a.hoge()` と書くことができる

```xtend
// 拡張プロバイダ
extension static UserRepository repo = new UserRepository()

// インスタンスメソッド User#save() は存在しないが、代わりに UserRepository#save(User) が呼ばれる
user.save
```

# はじめに

XtendはEclipse Foundationが開発しているプログラミング言語です。DSL開発フレームワーク [Xtext](https://eclipse.dev/Xtext/) の1機能として開発されています。
Javaへとトランスパイルされ、Javaよりも短い記述で実装することができます。

https://eclipse.dev/Xtext/xtend/

```java:JavaのHelloworld
public class Main {
    public static void main(String[] args) throws Exception {
        System.out.println("Hello World!");
    }
}
```

```xtend:XtendのHelloworld
class Main {
  def static void main(String[] args) {
    println("Hello World!")
  }
}
```

文法を調べてみると、クラスを疑似的に拡張する「メソッド拡張」の仕組みが面白かったので、本記事ではこの機能について紹介したいと思います。

# 検証バージョン

- Java: 21
- Xtext: 2.38.0

# Xtendの環境構築

（構文について知りたい方は「[拡張メソッドとは](#拡張メソッドとは)」まで読み飛ばしてください）

Eclipse Foundationが開発していることからもお察しの通り、Eclipse上のプラグインでトランスパイルを行います。

## プラグインのインストール

以下のリンクのインストール方法に従いXtextのプラグインをインストールします。

https://eclipse.dev/Xtext/download.html

SDK選択画面では `Xtend` と `Xtext` を選択します。

## 依存ライブラリの準備

今回はGradleで依存ライブラリをインストールしました。

インストールが完了したら新規プロジェクトを作成し、以下の `build.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同様、パッケージ名やクラス名とディレクトリ構成が対応しています。

```xtend:src/hello/HelloWorld.xtend
package hello

class HelloWorld {
  def static void main(String[] args) {
    println("Hello World!")
  }
}
```

保存してしばらくすると、自動的にJavaへのトランスパイルが始まります。生成物は `xtend-gen` 配下に出力されます。

```java:xtend-gen/hello/HelloWorld.java
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` で動作を確認してみましょう。

![run_xtend.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239887/6e176ce5-96c7-48a8-883e-3173ad470479.png)

![hello.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239887/20f552d6-fa7b-46cf-809f-b4e40d002331.png)

```
Hello World!
```

想定どおり実行できました。

# 拡張メソッドとは
それでは、本題の「拡張メソッド」についてです。

## 概要

拡張メソッドは疑似的にクラスにメソッドを追加する（=クラスを拡張する）機能です。「Xtend」の名前の由来にもなっています。

https://eclipse.dev/Xtext/xtend/documentation/202_xtend_classes_members.html

拡張といってもRubyのようにクラス自体を変更するわけではなく、あくまで構文上メソッドのように扱えるというものです。

クラス `A` にメソッド `hoge` を追加したい場合は、別クラス `Provider` に、`A` を第一引数に取る `hoge` メソッドを追加します。後は、 `Provider` で拡張メソッドとして使用する宣言をする（後述）と、以下の 糖衣構文が使えるようになります。

```xtend:これを
provider.hoge(a, arg1, arg2)
```

```xtend:こう書ける
a.hoge(arg1, arg2)
```

## スタティックメソッドで使用

まずはスタティックメソッドの拡張メソッドを使ってみます。基本型には組み込みのプロバイダが用意されているため、リテラルに対してスタティックメソッドが使えます。

例えば `String` には [`StringExtensions`](https://github.com/eclipse-xtext/xtext/blob/be22e41a20422505f5c57d0bd32db6932439266c/org.eclipse.xtext.xbase.lib/src/org/eclipse/xtext/xbase/lib/StringExtensions.java#L19) の拡張メソッドが使用可能です。

```xtend
"hello".toFirstUpper
```

トランスパイルするとこうなります。

```java:Java
StringExtensions.toFirstUpper("hello")
```

```:実行結果
Hello
```

組み込みのプロバイダ以外では `import` に `extension` を付けることで拡張メソッドを使用可能です。

```xtend
import static extension java.util.Collections.singletonList
```

## インスタンスメソッドで使用

続いてインスタンスメソッドの場合です。スタティックメソッドの場合と異なり、インスタンスメソッドを使うには拡張プロバイダのインスタンスが必要です。

フィールドに `extension` キーワードを指定することで、そのインスタンスの拡張メソッドを使用することができます。

```xtend
extension static UserRepository repo = new UserRepository()
```

## 実例

インスタンスメソッドの拡張メソッドについて、もう少し実用的な例を見てみましょう。
エンティティクラスの永続化を行う際、こんなことを思ったことはないでしょうか？

- DBの処理をエンティティクラスの振る舞いとして持たせるのは汚い
- 一方、エンティティクラスに保存のメソッドが生えていたほうが楽に扱える

拡張メソッドを使えば、どちらも叶えることができます。

まずはエンティティクラス `Person` を用意します。ここにはPersonの振る舞いだけ持たせています。

```xtend:src/hello/User.xtend
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` クラスです。

```xtend:src/hello/UserRepository.xtend
package hello

class UserRepository {
	def void save(User user) {
		// 実際はDB等に保存する
		println("user " + user + " is saved")
	}	
}
```

上記を利用するアプリケーションクラスを見てみます。
まずは、`Application` のスタティックフィールドに `UserRepository` のインスタンスを持たせて、拡張プロバイダとして利用します。

```xtend:src/hello/Application.xtend
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)` に変換されています。

```xtend-gen/hello/Application.java
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の拡張メソッドについての紹介でした。クラスを動的に拡張できる言語はいくつか見たことがありますが、別クラスのメソッドを借りてくるという仕組みは初めて見たので新鮮でした。
憶測ですが、トランスパイルが複雑にならない範囲でメソッドを後付けで増やせる方法としてこのような仕組みが採用されたのかもしれません。