ども nobkzです。
Haxeの多相関数の型推論について
Haxeでは型推論があり、匿名型があり、構造的部分型付けがあり、いろいろ便利なのですが
ここでは、Haxeの型システム上で起りうる問題と解決を提示していきます。
パラメトリック多相関数の型推論について
id関数の望ましくない、コンパイルエラー
Haxeではパラメトリック多相の型推論は失敗します。たとえば関数を見ていただきたい。
var id = function(x) return x;
これは関数プログラミングしていれば良く出てくる、引数をそのまま返す関数ですよね。
では、この関数の型はなんでしょうか?
というより、idの 引数の型 はどうなるのでしょうか?
Haxeには$typeで、型を調べることができます
var x = 1;
$type(x) // Int
では、id関数を試してみましょう。
$type(function(x) return x) // x : Unknown<0> -> Unknown<0>
Unknown<0> -> Unknown<0>という型付けされました。つまり、この引数の型については、コンパイラはシラネって言っているのです。
なので、コンパイラに 型の情報 を与えてあげると、Unknownでなくなります。
$type(function(x:Int) return x) // x : Int -> Int
ここで、問題なのが、もし、引数をUnknownにしたままにすると、 呼び出し時に、適用する引数の型の情報で推論してしまう ということです。
つまり、以下のコードではInt -> Intと推論されてしまいます。
function id(x) return x;
id(1)
$type(id) // x : Int -> int
「なにが問題か?」と思われる方も多いのでしょうが、 本来idのような関数の引数の型はどのような引数でも取りうるべき なのです。
つまり、以下の様なコードはHaxeでは型チェックでコンパイルエラーを起しますが、本来起きるべきでない型のエラーだと思います。
function id(x) return x;
id(1);
id("string"); // コンパイルエラー (String should be Int)
型パラメータを使用する、id関数のエラーを回避する
さて、この問題の解決は簡単でして、型パラメータを適用すれば良いのです。
function id<A>(x : A) return x;
id(10);
id("string");
すると、Aという型パラメータを持ち、A -> Aという型の関数となります。
Haxeのアドホック多相について
以下のような、引数の型によって振る舞いが変わるadd関数を定義してみましょうという話です。
package ;
class AdhocTest{
public static function main(){
var s = new Sample();
var i : Int = s.add(1,2);
trace(i); // -> 3
var a : Array<String> = s.add(["a","b"], ["c","d","e"]);
trace(a); // -> [a,b,c,d,e]
}
}
オーバーロードできない
Haxeには異なる振る舞いを関数は違う名前で書くべきという設計思想があり、 Haxeにはメソッドのオーバーロードすることができません。以下のコードはエラーとなります。
class Sample{
public function new (){}
public function add(x:Int, y:Int) return x + y;
public function add<T>(x:Array<T>, y:Array<T>)
return x.concat(y); // Duplicate class field declaration : add
}
このように、addの 二重の宣言 となりエラーを出します。
enumとabstractで解決する
さて、このような、エラーを解決するにはどうすれば良いでしょうか?
ひとつは、 関数名を変更すること です。これは誰もが思いつくでしょう。
class Sample{
public function new (){}
public function plus(x:Int, y:Int) return x + y;
public function connect(x:Array<Int>, y:Array<Int>)
return x.concat(y); // Duplicate class field declaration : add
}
しかし、最近僕は、HaxeでDSLを作ったとき、 関数名を変更できないad-hocな多相関数を実現しないといけない特殊な状況 がありましてその解決のためにenumと、abstractを使った方法で解決しました。
enumで複数の型をまとめる
結局のところ、多相関数は、複数の型を適用することが問題なので、 複数の型を一つの型にまとめて しまえば良いのです。
複数の型をまとめるには、enum が便利です。
enum ADD<T>{
Plus(i:Int);
Concat(a:Array<T>);
}
switchパターンマッチを利用して、型によって異なる振舞いを実装する
さて、複数の型をまとめたら、その型を利用して、関数を実装しましょう。そのときに、まとめた型から、内包されている型を取り出さないといけません。そして、取り出した型によって、ふるまいを変更しないといけません。
そのときは、パターンマッチが便利です。パターンマッチを使用して、実装してみましょう。
class Sample{
public function new(){};
public function add<T>(x:Add<T>, y:Add<T>){
return switch([x,y]){
case [Plus(x),Plus(y)] : Plus(x + y);
case [Concat(x), Concat(y)] : Concat(x.concat(y));
case _ : throw "type error!!!";
}
}
}
abstractで、まとめた型を見えなくする
さて、ここで、複数の型をenumでまとめましたが、これでは、オーバーロードというより、新しい型を作って、関数を実装し直したというだけです。
さて、それではどうすれば良いでしょうか? abstractを使いって暗黙の型変換を実装して、型を見えなくすれば良いのです 。
さて、ご存知だと思いますが、 HaxeのabstractはC#やJavaのそれとは全く異なります。 詳しくは、ググってください。
さて、AbstractAdd型をつくり、Add -> AbstractAdd -> Int or Arrayと暗黙の型変換する型をつくりましょう。
abstract AbstractAdd<T>(Add<T>){
inline function new(a) this = a;
@:from inline public static function fromInt(x:Int)
return new AbstractAdd(Plus(x));
@:from inline public static function fromArray<T>(a:Array<T>)
return new AbstractAdd(Concat(a));
@:from inline public static function fromAdd(a:Add<T>)
return new AbstractAdd(a);
@:to inline public function toArray<T>() : Array<T>
return switch(this){
case Concat(a) : a;
case _ : throw "type error!";
}
@:to inline public function toInt() : Int
return switch(this){
case Plus(i) : i;
case _ : throw "type error!";
}
@:to inline public function toAdd<T>() : Add<T> return this;
}
さて、add関数もAbstractAdd型の引数にして起き、また、返り値をなんとかしましょう。
class Sample{
public function new(){};
public function add<T>(x:AbstractAdd<T>, y:AbstractAdd<T>){
return switch([x,y]){
case [Plus(x),Plus(y)] : x + y;
case [Concat(x), Concat(y)] : x.concat(y);
case _ : throw "type error!!!";
}
}
}
アドホック多相関数(のようなもの)が実現できました!
さてこのようにenumとabstractを使えば、アドホック多相関数が実現できます。
(正確に言えばには、暗黙の型変換を行なう、AbstractAdd -> AbstractAdd -> AbstractAdd の関数を定義したので、見た目、疑似的なad-hocな多相な関数なのですが。)
package ;
class AdhocTest{
public static function main(){
var s = new Sample();
var i : Int = s.add(1,2);
trace(i); // -> 3
var a : Array<String> = s.add(["a","b"], ["c","d","e"]);
trace(a); // -> [a,b,c,d,e]
}
}
最後にソース全体を張りつけておきます。
package ;
enum Add<T>{
Plus(i:Int);
Concat(a:Array<T>);
}
abstract AbstractAdd<T>(Add<T>){
inline function new(a) this = a;
@:from inline public static function fromInt(x:Int)
return new AbstractAdd(Plus(x));
@:from inline public static function fromArray<T>(a:Array<T>)
return new AbstractAdd(Concat(a));
@:from inline public static function fromAdd(a:Add<T>)
return new AbstractAdd(a);
@:to inline public function toArray<T>() : Array<T>
return switch(this){
case Concat(a) : a;
case _ : throw "type error!";
};
@:to inline public function toInt() : Int
return switch(this){
case Plus(i) : i;
case _ : throw "type error!";
}
@:to inline public function toAdd<T>() : Add<T> return this;
}
class Sample{
public function new(){};
public function add<T>(x:AbstractAdd<T>, y:AbstractAdd<T>) : AbstractAdd<T>{
return switch([x,y]){
case [Plus(x),Plus(y)] : x + y;
case [Concat(x), Concat(y)] : x.concat(y);
case _ : throw "type error!!!";
}
}
}
package ;
class AdhocTest{
public static function main(){
var s = new Sample();
var i : Int = s.add(1,2);
trace(i);
var a : Array<String> = s.add(["a","b"], ["c","d","e"]);
trace(a);
}
}