2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

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よりも短い記述で実装することができます。

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

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

検証バージョン

  • Java: 21
  • Xtext: 2.38.0

Xtendの環境構築

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

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

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

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

SDK選択画面では XtendXtext を選択します。

依存ライブラリの準備

今回は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同様、パッケージ名やクラス名とディレクトリ構成が対応しています。

src/hello/HelloWorld.xtend
package hello

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

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

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 asJava application で動作を確認してみましょう。

run_xtend.png

hello.png

Hello World!

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

拡張メソッドとは

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

概要

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

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

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

これを
provider.hoge(a, arg1, arg2)
こう書ける
a.hoge(arg1, arg2)

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

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

例えば String には StringExtensions の拡張メソッドが使用可能です。

"hello".toFirstUpper

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

Java
StringExtensions.toFirstUpper("hello")
実行結果
Hello

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

import static extension java.util.Collections.singletonList

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

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

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

extension static UserRepository repo = new UserRepository()

実例

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

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

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

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

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 クラスです。

src/hello/UserRepository.xtend
package hello

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

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

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

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?