ScalaMatsuri 2018でImplicitの発表を見てようやく理解できたのでJava使いから見たImplicitの話を書きます。なお私はScalaは素人同然なので記事は修正・削除される可能性が大いにあります。
Interface実装しまくるとメソッドが増えすぎる問題
javaではそのオブジェクトをどう扱えるかをInterfaceで宣言し、それのメソッドを実装します。
例えばあるクラスXのインスタンスx1とx2が比較可能である場合は、XにComparableインタフェースを実装します。
そしてこのComparableなオブジェクトはソート可能になるのでCollection.sort()などに渡すことができます。
public class X implements Comparable<X> {
public int i;
public X(int i) {
this.i = i;
}
@Override
public int compareTo( X x) {
return this.i - x.i;
}
@Test
public void testSort(){
List<X> list = new ArrayList<X>();
list.add(new X(3));
list.add(new X(1));
list.add(new X(0));
list.add(new X(2));
Collections.sort(list);
}
}
Interfaceはどう扱えるかの宣言なので、Aのように扱えるとかBのようにも扱えるとかいくらでも実装することができます。
しかしむやみに実装すると、Interfaceのメソッドを大量に実装することにもなります。
public interface A {
void a1();
void a2();
}
public class X implements Comparable<X>, A, B, C {
public int i;
public X(int i) {
this.i = i;
}
@Override
public int compareTo( X x) {
return this.i - x.i;
}
@Override
public void a1() {
}
@Override
public void a2() {
}
@Override
public void b1() {
}
@Override
public void b2() {
}
@Override
public void c1() {
}
@Override
public void c2() {
}
}
これはよろしくありません。単にメソッドが多いだけで見通しが悪くなりますし、クラス当たりのメソッド数が規約で制限されていたらレビュアーに怒られるかもしれません。それに実のところこれらのメソッドはあまり関連していないので同じ場所に書くメリットはあんまりありません。
そもそもソートするためにComparableを実装したということはソートの必要が無ければ実装しなかった、つまりそのオブジェクトがなんであるかの本質とは割と関係ないということです。
クラスは「それは何であるか」であり、Interface(型)は「それを何として扱えるか」であり宣言は同じところに書くことで見やすくなりますが実装のほうは混ぜるメリットはあまりありません。
クラスを分割する
そこでクラスを分割することを考えます。Comparableなどのインタフェースを満足させるためのメソッドは元オブジェクトのラッパとなるクラスを作れば済むことが多いです。
このComparableをImplementしたXを仮にComparableXクラスとし、XからtoComparableX()できるようにします。
public class ComparableX implements Comparable<ComparableX> {
public X x;
public ComparableX(X x) {
this.x = x;
}
@Override
public int compareTo(ComparableX comparableX) {
return this.x.i - comparableX.x.i;
}
}
public class X {
public int i;
public X(int i) {
this.i = i;
}
public ComparableX toComparableX() {
return new ComparableX(this);
}
public A toA() {
return new XinA(this);// class XinA implements Aというラッパを作ったとする。
}
public B toB() {
return new XinB(this);
}
public C toC() {
return new XinC(this);
}
}
ソートしたいときはこのComparableXのリストを作ってソートし、中身のXを取り出せばいいわけですね。
同様にほかのインタフェースを持ったクラスも定義することができます。
これで見通しは良くなりましたね。
実際にはtoA()とかtoB()を書くよりはラッパ側にコンストラクタやファクトリを書くべきでしょう。
class A {
public A(X x){}
public static A getInstance(X x){}
}
変換コードと言うのは変換先が増えるものです。しかし変換先が増えるのはXの問題ではなく変換先を要求する他のクラス・コードの問題です。AだのBだのと変換先が増えるたびにXを変更するのは不合理と言えます。
分割するとそのまま扱えなくなる
ですが、メソッドやコンストラクタ、ファクトリを挟むことによりXをそのまま扱うことができないという問題が発生してしまいます。
A xA= x.toA()
aFnc(xA)
aFnc(new A(x))
aFnc(A.getInstance(x))
//aFnc(X)と直接書けたらいいのに…
また、もし同じ型のオブジェクトを返すメソッドが複数存在した場合にどちらを使うべきか判断できなくなるかもしれません。
class X{
//どちらを使うべきなんだ!?
public A toWonderfulA(){}
public A toGratestA(){}
}
逆に名前がぶつかる可能性もありますし正直言うともとになるインタフェースごとに分けて書きたいくらいです。
そこでJavaでは実現できない妄想として、同じクラスをImplementしているインタフェースごとに分割して書ける言語というものを考えてみましょう。
public class X {
public int i;
public X(int i) {
this.i = i;
}
}
public class X implements Comparable<X> {
public int compareTo(X x) {
return this.i - x.i;
}
}
public class X implements A {
public void a1() {
}
public void a2() {
}
}
とても見通しが良いかとおもいます。
もちろん、このコードはコンパイルできません。同じクラスであることを利用してコンパイラが気を利かせてマージしてくれたっていいと思うんですけどね。
Scalaならできます
ですがScalaは似たようなコードを書くことができます。そう、Implicitです。
case class X(i: Int) {
}
//Interfaceみたいなもの
trait A[T] {
def a1():Int
def a2():Int
}
object X {
// Implicitで「Xに別の型が要求されたとき」のコードを書くことができる。これは単にXから取り出せる値を返している。
implicit def xToInt(x:X): Int = x.i
// class X { toOrderingX(){return new Ordering()}}と同じ.Xが直接バインドされないのでcomparableというよりcomparator。sortedでこのOrdering.compare()にリストの要素Xを二つ渡して比較することでソートする
implicit val toOrderingX: Ordering[X] = new Ordering[X] {
override def compare(x: X, y: X): Int = x.i - y.i
}
// 普通のコードからはclass X implimentes Ordering、またはOrderingコンパレータがあるXのように見える
val xList = List(new X(3), new X(1), new X(2), new X(0))
val sortedList = xList.sorted
// class X { toA(){return new A(this)}}と同じ。直接引数xをバインドすることもできるけど型クラスっぽくないからかサンプルコードとして見たことが無い。
implicit def xToA(x:X): A[X] = new A[X] {
def a1()=x.i
def a2(){}
}
// class X implimentes A に見える…けど単にnew A(new X(1))として動いると考えるほうが簡単かな?
aFnc(new X(1))
}
Implicitは暗黙の型変換と呼ばれますがJava屋から見れば後付けインタフェースみたいなものです。
クラスとインタフェースは同じように型として扱えますが本来はちょっと違うものです。
このように、クラスがどういうものかと外から扱うときにどう扱えばいいかを分離することができるのでよりシンプルなモデルを書くには有効でしょう。
implicitはクラスと型の間に存在するものである
あるクラスのメソッドはそのクラスのインスタンスを操作対象にしたコードブロックです。
なら同じインスタンスを対象にするコードはそのクラスにまとめて書いたほうが管理しやすいのではないか?というのがオブジェクト指向の基本的な考えの一つです。
//関数はどこに置くこともできるので自由だが管理しにくい
def f(x : X) : Y = ...
def g(x : X) : Y = ...
//メソッドはクラス・インスタンスと結びついたコードブロック。Xがthisとして渡されてるように動く
class X {
def f() = ...
def g() = ...
}
ではImplicitはというと、ある「クラスが別の型を要求されている関係」に結びついたコードブロックです。
先ほどJavaで妄想として書いたコードが実際に動くようなもんですね。
//「XがComparableを要求されたとき」に結びついて欲しい(コンパイル通らない
public class X implements Comparable<X> {
public int compareTo(X x) {
return this.i - x.i;
}
}
//「XがA型を要求されたとき」に結びついて欲しい(コンパイル通らない
public class X implements A {
public void a1() {
}
public void a2() {
}
}
//Implicitは「XがA型を要求されたとき」に結びつく(コンパイル通る
implicit def xToA(x:X): A[X] = new A[X] {
def a1()=...
def a2(){}
}
DDDとの関連
ドメインモデルはドメイン以外の何にも依存しないべきです。これは他のレイヤに依存しないようにするというのが基本的な実装ですが、逆に他のレイヤから依存されるコードをできるだけ避けるというのも必要かと思います。
例えばソートを想定しないドメインモデルが有ったとして、画面に表示するときにソートできると便利だからと言う理由でComparableを実装するべきでしょうか?便利と言うだけでモデルにコードを書き足していくとモデルはその純粋さを失います。Implicitを使うことでよりドメインに絞ったコードが書けるかもしれません。
また、ドメインモデルはできるだけ統一したいところですが別のコンテキストではまた別のモデルとして設計されることがあります。
例えば販売に関連するあるオブジェクトがあるとします。このオブジェクトは売るときは商品であるが店舗まで運搬するときは単なる貨物であり、買った側は売る側の都合とは関係なく使いたいように使うでしょう。つまりコンテキストによってそのオブジェクトは異なるという事です。
書籍などではコンバータを書いてドメインモデルを変換するように書いてありますが、コンバータというのはその立ち位置が不安定で同じようなものが複数作成されることなどは珍しくありませんし、「コンバータはモデルの一部か?」などの不毛な論争が起きることもあります。
Implicitを使うことでコンバータをコンパイラに認識させることができ、管理できる可能性があります。(現状ではあまり勧められる使い方ではなさそうですが。)
Implicitの問題点
どこに書いてあるのかどこに書くべきなのかわかりにくい
メソッドはクラスと深く結びついた関数で少なくともXと物理的に同じ場所にあります。インタフェースもほとんどの言語ではクラスの横で宣言するので宣言は同じ場所にあります。
Implicitはそれを破壊します。単にクラスの外側で宣言するのでクラスと同じ場所に書いてあるとは限らないというだけの話ですが。XがA型を要求されたときのコードはXと同じ場所にあるべきか?Aと同じ場所にあるべきか?XもAも既存クラスで同じ場所に書けないときはどこに書くべきか?これは難しく深刻な問題です。
というのもクラスは「それは何であるか」の記述ですが、「それは何であるか」について書く伝統・指針はありますが「それがアレとして認識されてるときに何であるか」を書く指針は無いからです。
関係は自明でなければいけないが自明であるか判断がつかない
例えばIntがFloatとして振舞うことを要求された場合、IntをFloatにできるのは自明でしょう。一方、FloatをIntにできるかは自明ではありません。
多くの言語は端数を切り捨てていますが四捨五入など他の丸め方もあります。
あるデータを別の型のデータに変換するときに普通は便利かどうかで考える一方で自明であるかを考えることはあまりないので想定外の変換をしてしまうことは珍しくありません。
暗黙の型変換という名前が悪い
Implicitを直訳すると暗黙になるのでしょうがないのですが
やりたいことは型変換をコンパイラに知らせること、つまり宣言なので宣言的型変換とかそういう名前にするべきでした。型変換というのもちょっと疑問です。「クラスと要求されてる型のギャップを埋める何か」は型変換というんでしょうかね?とはいえ私はScalaにも英語にも詳しくないのでこれは単なる言いがかりでしょう。
感想
Implicitはコンセプトは正しくコードに自由度を与えられますが、悪用もしやすく正しく書くための指針が足りない状態という印象です。