Ceylonはほとんど理想の言語

Ceylonはほとんど理想の言語


先日、新言語Ceylonがリリースされました。まだ少ししか触れてないですが、ざっと見たところとても良さそうだったのでCeylonの何が素晴らしいのか書いてみたいと思います。

本エントリーではCeylonがどんな言語かを紹介することは目的としません(Ceylonの概要については公式のQuick introductionを読むのが早いです)。プログラミング言語はどのようにあるべきかを考え、それと照らし合わせてCeylonが素晴らしい理由を述べます。

なお、プログラミング言語の良し悪しについては色々な論点があると思いますが、本エントリーでは言語仕様についてのみ言及します。現実的な道具として良いかどうかはまた別問題なので悪しからず。

※C#との違いについてのコメントを多くいただいたため追記しました。

Ceylonはなぜ素晴らしいか

かねてから僕はオレオレ言語を作りたいと思いどのような言語が理想的かを考えていたのですが、Ceylonがリリースされて言語仕様をチェックしてみたところ、僕が考えていた仕様をCeylonはほとんど満たしていたのです。そういうわけでCeylonの素晴らしさを伝えたいと思いこのエントリーを書くことにしました。

それでは、言語はどうあるべきかを一つずつ考えながらCeylonが素晴らしい理由を見ていきましょう。

静的型付けである

個人的には、言語は静的型付けであることが望ましいと思っています。

多くのスクリプト言語に見られるように動的型付き言語は素早いコーディングを可能としますし、それに慣れてしまうといちいち型を書くのが面倒だというのもわかります。しかし、エディタの補完機能やリファクタリング機能などを考えると静的型付けの方が圧倒的に有利ですし、それらの恩恵を考えると一概に動的型付けの方が素早くコーディングできるとは言えないのではないかと思います。

動的型付き言語では、関数(やメソッド)の引数にどんな型の値が渡されるか実行時までわかりません。想定外の値が渡される危険性が常につきまといますし、そのような場合でも適切に動作するように関数を実装するのは非常に困難です。もし、なんらかのミスで不適切な型の値が渡されてしまった場合、その場で問題が露見するとは限らないので原因の特定が困難になります。特定の条件下でだけそのような問題が発生する場合には潜在バグにもなります。テストを書く際にもありとあらゆる型の値を入れてみるわけではないので、想定される型についてのみテストを書くことになりそのようなバグを検出することができません。

よって、静的型付けであることを前提としながらできるだけその煩わしさを軽減する方向で考えるのが良いのではないかと思います。

Ceylonは静的型付き言語です。

型推論がある

静的型付けの煩わしさを軽減する強力な武器が型推論です。モダンな静的型付き言語であれば型推論はほしいところです。

Ceylonではローカル変数とローカルメソッドの戻り値について型推論が利用できます。特に、

List<List<Integer>> a = [[2, 3], [5, 7], [11, 13]];

のようにジェネリクスが使われている場合に

value a = [[2, 3], [5, 7], [11, 13]];

のように書き換えられるのはうれしいところです。

トップレベルの関数やメソッドについては型推論が使えませんが、動的型付けであってもドキュメントに型を明記しないといけないはずなので労力は変わらないように思います。また、ソースの可読性の観点ではそれらの型は明記されているのが望ましいとも考えられます。


デフォルトで変数にnullを代入できない

せっかく静的片付けでタイプセーフになっていてもnullを認めると台無しです。例えば、Javaではありとあらゆる場所でNullPointerExceptionが起こり得るのでそれに関係した潜在バグを生みがちですし、enum型変数にnullが代入できるのは非常に使い勝手が悪いです。

プログラムを書いていると、nullが入ることを想定していない引数を定義することが多々あります。想定外の挙動を防ぐためには、関数の冒頭で引数に渡された値のチェックをしなければなりません。せっかくタイプセーフになっているのにこれでは安全性を保つために大量の検査コードをが必要になってしまいます。また、それらの検査は実行時に行われるので潜在バグの原因にもなります。

理想的な言語では、少なくとも変数や引数にnullを許容するかしないかを指定できるべきです。そして、多くの変数にはnullを代入出来る必要がありません。そう考えると、安全側に寄せてデフォルトでは変数や引数にnullを渡せないようにするのが望ましいと考えられます。

Ceylonはこれを実に見事な方法で解決しています。なんと、Ceylonでは決してNullPointerExceptionが発生することがありません。

Ceylonがnullをどのように扱うかを理解するためには、まずCeylonのUnion typeについての理解が必要です。Union typeがどういうものかは、次のコードを見るのが早いです。

Integer|String a = "ABC";
Integer|String b = 0;
List<Integer|String> values = [0, 1, "ABC", "DEF"];

このように、Ceylonでは|を使って複数の型の和集合となる型を宣言できます(&を使って積集合となるIntersection typeも宣言できます)。しかも、3行目の例のようにジェネリクスの型パラメータにすらUnion typeやIntersection typeを使えます。Integer|String型の変数に対してはIntegerとStringの両クラスが共通に持つメソッドしかコールすることができません。上記のコードの変数aに対してString固有のメソッド(ここでは部分文字列を返すsegmentメソッド)をコールするためには次のようにします。

Integer|String a = "ABC";

if (is String a) {
    print(a.segment(1, 2)); // "BC"
}

if (is String a) {…}とすることで、コンパイラはブロック内で変数aをString型として扱います。これはJavaで

Object a = "ABC";

if (a instanceof String) {
	String s = (String)a;
    print(s.substring(1, 2)); // "BC"
}

とするのとは決定的に違います(コンパイル時に型のチェックが行われるかという点で)。Javaでは、もしaをString以外にキャストしてしまうと実行時までエラーが検出されませんが、Ceylonではブロック内でaをString型以外として扱おうとするとコンパイル時エラーとなります。

さて、これがnullとどう関係するのでしょうか。

Ceylonでnull代入可能な変数や引数を宣言するには次のように型に?を付けます。

String? a = "ABC";

if (exists a) {
    print(a.segment(1, 2)); // "BC"
}

変数宣言に?を付けることでその変数はnullを代入可能となる代わりに、if (exists a)でチェックをしなければStringクラスのsegmentメソッドを呼ぶことすらできなくなります。このような挙動はどのように実現されているのでしょうか。

CeylonのnullはNullクラスの唯一のインスタンスで、X?はX|Nullを表すシンタックスシュガーです。そのため上記のコードは次のような意味になります。

String|Null a = "ABC";

if (is String a) {
    print(a.segment(1, 2)); // "BC"
}

aはString|Null型なのでnullを代入できるようになります。しかし、Nullクラスは一つもメソッドを持たないため、NullクラスとStringクラスが共通に持つメソッドは存在しません。そのため、CeylonではX|Nullという型の変数(nullが入っているかもしれない変数)に対するメソッドコールは必ずコンパイルエラーとなり、実行時にNullPointerExceptionが発生することはありません。

しかし、このままではaに対してStringクラスのメソッドをコールすることができません。そのため、isまたはexistsで検査し、そのブロックの中でメソッドを呼ぶ必要があります。そのようなチェックを毎回行わないといけないのは煩わしいように感じられるかもしれませんが、そもそもnull代入可能でなければならない変数はプログラム中で多くなく、デフォルトでnull代入が不可能であればその手間は気にするほどではありません。その代わりにNullPointerExceptionによる潜在バグを排除できるのであればメリットは十分です。

このように、Ceylonは特別なnullという値を作るのではなく言語仕様の中で表現し、同時にnullセーフを実現しており非常にエレガントです。

existsがあることでnullが特別扱いされているように見えるかもしれませんが、X型の変数xに対するexists xis X&Object xのような意味になります。Nullクラスの親クラスはAnythingクラスなのですが、AnythingクラスのサブクラスはObjectクラスとNullクラスの二つに限定されており、Nullクラスを継承することもできないため、is X&Object xで検査することでxがXのインスタンスかつXがNullでないことを保証できます。なお、Ceylonのドキュメントには”A condition of form exists x means is Object x, and is satisfied if the reference x refers to a non-null value. Within the associated block, x will have the non-optional type X&Object, where X is the previous type of x.”とだけ書かれており、実際にexistsがisのシンタックスシュガーになっているかはわかりません。ただ、次のコードが通るのでexistsはやはりifっぽい挙動をしています。

Anything a = 123;

if (exists a) {
    print(a.string);
}

ジェネリクスがある

動的型付き言語ではまったく気にする必要がないのに静的型付き言語では無視できない問題があります。それは、配列やリスト、マップなどのコンテナクラスの型をどうするかということです。

Java 1.4まではListやSetの要素はすべてObject型で、要素を取り出すときにはそれをダウンキャストして使うようになっていました。これでは、せっかく静的型付けなのにコンテナクラスを使う度にタイプセーフでなくなってしまいます。また、キャストを書くのも煩わしいです。

Objective-Cはさらに大胆です。id型という明示的なキャストなしにダウンキャストできる型を導入することで、コンテナクラスでのタイプセーフを諦めてしまいました。Java 1.4と比べると明示的なキャストが必要ない分マシですが、根本的な解決にはなっていません。

JavaではJava 5からジェネリクスが導入されることでこの問題が解決されました。

Ceylonにもジェネリクスがあります。しかも、Javaと違って次のようなこともできます。

型パラメータを指定した上での型判定

List<Integer>|List<String> a = [2, 3, 5, 7, 11, 13];

if (is List<Integer> a) {
    print("a is List<Integer>"); // printed
}
if (is List<String> a) {
    print("a is List<String>"); // not printed
}

型パラメータによる型判定

void isT<T>(T t, String string) {
    if (is T string) {
        print("``string`` is T");
    } else {
        print("``string`` is not T");
    }
}

isT("T", "ABC"); // ABC is T
isT(1, "ABC"); // ABC is not T


プリミティブ型が存在しない

ジェネリクスがあっても、プリミティブ型が存在すると面倒です。プリミティブ型の値はコンテナクラスに入れられない等の問題を引き起こします。Javaではintに対応したIntegerなどのラッパークラスと、int・Integer間の変換を暗黙的に行うオートボクシング/アンボクシングによってこれを解決していますが、そもそもプリミティブ型がなければそんなことを考える必要もありません。一方で、プリミティブ型がなく整数の和ですらメソッドをコールするような言語ではパフォーマンスが不安です。

理想的には、言語仕様上はIntegerクラスのインスタンスのふりをしているけれども、実行時にはintとして扱われることが望ましいです。

Ceylonにはプリミティブ型は存在しません。前述のようにnullでさえNullクラスのインスタンスです。また、CeylonのIntegerやFloatはクラスですが、CeylonのプログラムをJVM上で動作させるときにはIntegerはlongに、Floatはdoubleにマッピングされます。

ただし、IntegerやFloatをコンテナクラスに入れたときにJVM上でどういう挙動になっているのかまでは調べられていません。もしかするとLongクラスやDoubleクラスでラップされているかもしれません。ただ、IntegerやFloatはfinalでありサブクラスが存在しないのでポリモーフィズムを考える必要がありません。裏側ではlongやdoubleのための基本的なコンテナクラスが用意されており、ボクシングによるパフォーマンスの低下が回避されているかもしれません。

フィールドは完全にprivate & プロパティがある

これまでの条件で、静的型付けの純粋オブジェクト指向言語を実現できるようになりました。そんな言語でも、メソッドを介さずにフィールドにアクセスできてしまってはすべてがぶち壊しです。

フィールドがすべて隠蔽されていてgetter/setterを介してそれらにアクセスしていれば、サブクラスでその挙動をオーバーライドすることができますが、親クラスでフィールドが公開されているとサブクラスでそれをオーバーライドすることはできません。

一方で、JavaのようにgetXXX()とかsetXXX(x)というメソッドを作ったりコールしたりするのは面倒です。ソースも長くなって見づらいし、EclipseのGenerate getters and settersがあっても、リネームしたときなんか本当に面倒です。Objective-CやC#のようにプロパティが欲しいところです。

Ceylonにはフィールドがありません。次のコードのnameのように、フィールドのように見えるものでもサブクラスでオーバーライドできます(Ceylonではオーバーライドのことをrefineと呼ぶようです。推測ですが、オーバーライドでは上書き感が強すぎるので、あくまで親クラスの仕様に沿って動作をrefineするという意味合いなのではないでしょうか。)。Ceylonではこれをプロパティと呼ばず、ポリモーフィズムの働く属性ということでPolymorphic attributesと呼ぶようです。

class A(shared default String name) {
}

class B(String firstName, String lastName)
	extends A(firstName) {
    shared actual String name {
        return super.name + " " + lastName;
    }
}

A a = A("NAME");
B b = B("FIRST", "LAST");

List<A> l = [a, b];
for (e in  l) {
    print(e.name);
}


デフォルトで変数に再代入できない

多くの変数は一度値を代入すると二度と書き換えられることがありません。できるだけ安全側に寄せようとすると、書き換える必要のない変数はすべて再代入不可であるべきですが、JavaのfinalにしてもCのconstにしてもいちいちつけるのは面倒です。デフォルトは再代入不可であり、必要に応じて再代入可となるようにすべきだと思います。

Ceylonでは次のコードのようにデフォルトで変数に再代入不可、variableキーワードを付けることで再代入可となります。その結果副次的に、クラスの属性も再代入不可となり、明示的にvariableを付けない限りクラスがイミュータブルになるという点も素晴らしいです。

Integer a = 2;
// a = 3; // Error

variable Integer b = 5;
b = 7;

状態を持つことはプログラムを複雑化し、またテストを難しくします。極力イミュータブルであるような設計を心がけ、ミュータブルにするときは明示的にvariableキーワードを付与しなければならないことで、あえてミュータブルを導入するということに意識的でいられます。

優れたリテラルがある

特にコンテナクラスのリテラルについては、Javaは壊滅的です。できるだけ煩わしさを軽減するという方向性で考えると使い勝手の良いリテラルは必須です。

Ceylonではスクリプト言語のように各種リテラルが用意されているのですが、それらはすべてコンストラクタ等のシンタックスシュガーになっています。タプルすらも普通のクラスで実現されていることには驚きました。次のコードのaの宣言とbの宣言はまったく同じことをしています。

[Integer, Float, String] a = [2, 3.0, "5"];

Tuple<Integer|Float|String, Integer,
	Tuple<Float|String, Float, Tuple<String, String>>>
	b = Tuple(2, Tuple(3.0, Tuple("5", [])));

言語設計はシンプルに保ちながらもシンタックスシュガーで使い勝手が良い言語に仕上げている点に好感が持てます。

その他

その他にもCeylonの素晴らしいところは色々あります。

Ceylonではクラス名は大文字から、メソッド名や変数名は小文字(かアンダースコア)から始めなければなりません。命名に自由度を残しても、クラス名を小文字から始めるなど文化に沿わない命名は害悪でしかありません。暗黙的に守らなければならないルールを作るくらいなら、明示的なルールである方が望ましいです。

Declarative syntax for treelike structuresとして説明されている次の構文も、まだちゃんと追えてませんがおもしろそうです。

Html html = Html {
	doctype = html5;
	Head { title = "Ceylon: home page"; };
	Body {
		H2 ( "Welcome to Ceylon ``language.version``!" ),
		P ( "Now get your code on : )" )
	};
};

そして、これは言語仕様自体の話ではないですがCeylonには「なんでこんな言語仕様になってるんだろう?」と思ったときに答えてくれるFAQがあります。例えば、Ceylonではprotectedに当たるアクセス修飾子がないですが、なぜそprotectedをなくしたのかという理由がFAQに書かれています。このFAQを眺めていると、Ceylonが強い理念を持って設計された言語であることがわかります。CeylonのFAQはこちらです。

Ceylonの残念な点

ここまでCeylonの素晴らしいところを見てきましたが、こうなってればもっと良かったのにと思う点について書いてみます。

多くのオブジェクト指向言語ではクラスが二つの役目を担っているように見えます。一つはもちろんカプセル化や継承、ポリモーフィズムによって抽象化を実現することです。もう一つは、名前空間として機能することです。

名前空間として機能するとは、例えば次のようなことです。Javaで文字列の先頭と末尾から空白文字を取り除くにはStringクラスのtrimメソッドを使いますが、trimという一般的な名前のメソッドであってもs.trim()のようにインスタンスを指定すれば他のtrimメソッドと名前衝突することはありません。trimメソッドはインスタンス(に紐付いたクラス)に属しているのだから当たり前だと思うかもしれませんが、trimメソッドの機能を考えると必ずしもStringクラスのインスタンスメソッドである必要はありません。引数にStringをとる関数(やクラスメソッド)で良いはずです。ただ、s.trim()のように書けるとStringUtils.trim(s)などとした場合と比べて名前衝突を避けながらも簡潔に書けるからインスタンスメソッドとして定義されているに過ぎません。

Stringクラスが本質的に持つべき情報はi番目の文字は何かということと、文字列の長さは何文字かということの二つだけのはずです。にも関わらず、その他の便利メソッドが本質的な情報を表すメソッドと区別されずインスタンスメソッドとしてごちゃ混ぜで存在してしまっています。そうすると、「なんでこのメソッドはあるのにあのメソッドはないんだ?」「このメソッドはこのクラスに必要なの?」と首をかしげたくなるケースが多々起こります。現に、JavaScriptのStringには途中までtrimメソッドが実装されていませんでしたし、Objective-CのNSStringクラスにはファイルパスを扱うためのstringByAppendingPathComponent:のようなメソッドまで用意されています。

そのような状況ではモジュールの提供者はどれだけのメソッドを足してもクラスを完成させることができませんし、利用者が自分でメソッドを足そうとしても元のクラス自体にはメソッドを足せません。クラスを継承してメソッドを足すという方法では元のクラスと共存できないので使えませんし(例えば、Stringクラスを継承してメソッドを足しても、”ABC”などのリテラルで作られるオブジェクトは結局元のStringクラスのインスタンスなので足されたメソッドと共存できません。)、JavaScriptやObjective-Cのようにクラスそのものを書き換えてしまうアプローチでは複数のモジュール間で衝突してしまう危険性があります。結局、StringUtilsなどという別クラスを作ってそこに便利メソッドを定義することになり、クラスに元から用意されている便利メソッドと一貫性がなくなってしまいます。

s.trim()の形で名前衝突を避けつつメソッドを呼べるようにするという目的の実現には、必ずしもインスタンスメソッドである必要はないはずです。例えば、第一引数にStringをとる関数はs.method()のようにコールできるという言語仕様を考えてみましょう。この言語では必要なモジュールをファイルごとにimportして使います。また、関数はオーバーロードでき、名称が同じでも引数の型が異なれば共存できます。すると、ファイルローカルでimportされた関数の名称と呼び出し元の値の型(上記であればsの型であるString)からコンパイラは一意にコールすべき関数を決定できます。もし、importした複数のモジュール間で一部の関数の名称(と引数の型)が衝突してしまっても、import時に別名を付けられる言語仕様であれば問題ありません。

そうすると、便利メソッドは異なるモジュール間でも一貫性を持って共存できますし(例えば、Stringクラスの基本的な便利メソッドを提供するモジュール、ファイルパスの操作に特化したモジュールなど、必要なものを組み合わせてimportし、Stringクラスを拡張するような感覚で利用できます。)、クラスはシンプルで完成された姿でいられるはずです。

Ceylonはそのようには設計されていないようです。モジュールのリファレンスを見ているとMix-inで様々なinterfaceを組み合わせてクラスの機能が実現されるように設計されているようですが、これまでに調べた限りでは後からクラスにメソッドを追加することはできなさそうです。また、Stringクラスを見てみてもずいぶんとゴテゴテとしたものになってしまっています

Quick introductionに”String is a List“とあるのを見たときは、もしかしてStringはシンプルにList<Character>になっており、その他のインスタンスメソッドは分離されているのかと胸が踊りましたが、単にListのinterfaceを満たしているというだけでした。残念です・・・。

まとめ

これまで見てきた通り、Ceylonはとても一つ一つの仕様について良く考えられた完成度の高い言語です。全般的に一貫してシンプルな言語設計ながら優れたシンタックスシュガーによって使い勝手の良い言語に仕上がっています。最後に残念な点について述べましたが、個人的にはこれまで見たどんな言語よりも良い言語なのではないかと思います。これからCeylonが世に広まることを期待してます!

Ceylonはここから簡単にブラウザ上で走らせられます。興味の出てきた方はぜひ一度試してみて下さい。

なお、まだCeylonについてはざっと見ただけで書いているので、おかしな点などあれば指摘していただけると助かります。本ブログはコメント欄を閉じてしまっているので、@koherまでにコメントを下さい。

(追記 2013/11/27 18:20)


C#との違いについて

「C#との違いがわからない」、「C#ライクなJava」というコメントを多くいただいたのですが、次の点でC#と異なると考えています。ただ、C#にはあまり詳しくないのでおかしな点があれば@koherまでご指摘いただけるとうれしいです。