「Rubyのような表現力に、Haskellのような型チェック」が宣伝文句のプログラミング言語 merd で遊んでみよう、という企画です。 型付けの強い言語は型推論やパターンマッチと言った強力な機能と引き替えに、 言語としての記述力にそこかしこで制限が加わってしまいがちです。 例えばCのprintfのような機能。例えばad-hocなoverloading。 果たしてこの制限は不可避なのでしょうか?(反語)
まだまだα版とのことで、この記事の内容はすぐ古くなる可能性が大きいです。
とりあえず ver 0.1.6 に基づいて書いていきます。
ご意見・間違いの指摘など大歓迎。
merd's home page の一番下の方からソースがダウンロードできます。pure な OCaml で書かれているので、OCamlが動かせる環境ならどこでも動くはず。 ただしビルドするには先にOCamlのインストールが必要です。 merdのビルド自体はmakeが入っていればmake一発。
…と思ったらWindowsでうまく動かなかったので、微調整しました。 merd Windows用(コンパイル済み) を置いときますので、興味のある方は是非どうぞ。
あとは、環境変数 MERD_LIB に、配布書庫内の lib ディレクトリを指定しておくと動きます。unix系ならsetenvコマンドなどで。 Win9xならautoexec.batにset MERD_LIB=c:\Develop\merd-0.1.6\lib みたいな一行を追加。WinNT系ならシステムのプロパティの詳細設定の環境変数で。
println("Hello, World")
実行結果は…
>> merd test.me Test needs Pervasives Starting package Test Starting evaluation Hello, World
あるいは
"Hello, World".println
でも同じ。OO風に書けるように、ということで
x.f
は f(x)
のシンタックスシュガーだそうな。
今日の所はそんなに特徴的な点はないので気楽に~(^^;
add(x,y) =
x + y
println( to_string( add(1, 2) ) )
関数定義は 「関数名( 仮引数リスト ) = 内容
」で。
関数呼び出しは、「関数名( 引数リスト )
」とまあお馴染みです。
x = 1
x.add(2).to_string.println
Hello, World の時にちょっと書いたように、関数呼び出しは
「第一引数.関数名(残りの引数リスト)
」と、
オブジェクト指向風にも記述できます。ただ、「1.add(...)
」
という書き方は 1.
が浮動小数点数としてパーズされてしまうためか、
どうも無理っぽいです。
変数の定義は、「x = 1
」 のように初期化文を置くことで実現します。
変数の有効範囲は、その定義のあったスコープ(関数定義内の文なら、関数の内部。
()
でくくった部分式の中なら、その部分式の終わりまで。etc)
に限られます。
Python や Haskell のように、改行や字下げが意味を持っています。例えば、
add(x,y) = x
+ y
は二つの引数を足す関数 add の定義になっていますが、
add(x,y) = x
+ y
と + y
の字下げをやめると、add は第一引数を返す関数で、
その後ろに別の + y
という式が続いている、と見なされます。
グローバル変数 y
が存在しなければ多分コンパイルエラー。
if_equal_then_print(x,y) =
if x == y then
x.to_string.println
else
"???".println
if_equal_then_print(123, 456)
if_equal_then_print("abc", "abc")
??? abc
if による条件分岐。まあ違和感は特に感じないと思います。 else 部分を書かずに if - then で終わるのもOk。 上の例でお気づきのように、to_string や == などは、 整数型や文字列型など、色んな型に対して多重に定義されています。
インデントでブロックを表現するので、
f() =
"aaa".println
"bbb".println
"ccc".println
"ddd".println
f()
こんな感じで字下げして並べておけば上から順に実行されます。 関数の戻り値は最後の式の値になるそうな。
merd という言語の特徴として真っ先に取り上げられるのが、 今から紹介する文法規則です。
(1+2*3).to_string.println
7.
まぁ普通ですね。足し算より掛け算の方が優先なので、結果は 1+6 で 7。
(1 + 2*3).to_string.println
7.
私はC++等で普段プログラムを書くとき、"あとで見るときにわかりやすいように" という理由で、先に計算される側をくっつけて書くことが結構あります。 足し算と掛け算ならこんなことしなくてもわかりますが、シフト演算子とか || とか、日常生活ではまず使わない計算式を使うときには特に。
((1+2)*3).to_string.println
9.
演算の優先度を手動で指定するには、かっこで括ります。 これなら 3*3 で 9 が答えになります。
(1+2 * 3).to_string.println
9.
merd の場合、くっつけて書いてあればそれだけで演算が優先されます。 「+の前後には空白を入れずに*の前後には空白をいれる」 なんて書き方をしてるってことは、プログラマはほぼ間違いなく、 1+2 を先に計算して、次にそれ*3、したいのでしょう。 それなら、と処理系が適切に解釈してくれるわけ。 merd のドキュメントはこの動作のことを "Horizontal Layout" と呼んでいました。
「あなたに見えるそのままにパーズされます」というのがこの言語の謳い文句。
上で紹介した Horizontal Layout と、インデントを構文解析に使う、
という2点からなりたっています。例えば 1+2 * 3
とあれば、コード上でそう見えているその通りに、1+2 が先にくっつきます、と。
f() =
"abcde".println
123
g() =
"fghij".println
456
h(x,y) =
x.to_string.println
h( f(), g() )
f とか g とかは、文字列を表示してから整数を返す関数です。 h は、整数を二つ受け取って、最初の方だけを表示する関数です。
abcde fghij 123.
関数呼び出し時には、「まず引数を全て評価して、その値で関数を呼び出す」 という作業が行われます。まず f() と g() を呼んで、その値で h() を呼んでいる、 と。しかしよく見てみると、h() の中では引数 y は一度も使っていません。 ということは、yに対応する引数の計算はわざわざしなくても良いのでは? と考えてみることもできます。そこで、上にあげた「値で関数を呼び出す」 方式の他に、「本当にその引数が必要になるまで計算を遅らせる」、 Lazy Evaluation と言う方式が考えられます。
merd で Lazy Evaluation をさせるには…
h(Lazy(x),Lazy(y)) =
x.to_string.println
hh(Lazy(x),Lazy(y)) =
(x+y).to_string.println
h( f(), g() )
hh( f(), g() )
abcde 123. abcde fghij 579.
引数を Lazy() と指定しておくことで実現します。簡単簡単。
ところでこのLazyEvaluationって、一体何の役に立つのでしょう? 使わないかもしれない部分の評価を遅らせておくことで、 速度があがるかもしれません。でも、Lazy を実現するために色々頑張るコストも相当大きそうです。 正解は、"Lazyがないと書けない機能"が存在するから、です。
C言語などに、真偽値と真偽値のANDをとったりORをとったりする演算子があります。 これらは short-circuit と呼ばれる、左側のオペランドを見て真偽値が決まったら、 もう片方は全く評価しない、という動作をします。Lazy的です。
// C言語。short-circuitに依存したコードの例
if( ptr==NULL || *ptr=='\0' ) { ... }
この特殊な動作のために、演算子を自分で定義できる C++ 言語であっても、 && や || と完全同じ動作をする演算子を、 ユーザー側で定義することができなくなっています。 ユーザー定義の演算子は関数としてしか定義が記述できないので、 両方の引数をまず評価してから、関数本体が実行されてしまうので。
さて、merd ではこの二つの演算子はライブラリ関数として定義されています。 ( merd/lib/Pervasives/standard_types.me )
a && Lazy(b) =
if a then b else False
a || Lazy(b) =
if a then True else b
さらになんと、if-then-else 以外の制御構造は、 merd では全てライブラリ関数として実現されています。
while Lazy(c) do Lazy(body) :=
if c then
body
while c do body
while-do はこんな感じの再帰関数。
Lazy(a) if b :=
if b then a
Perlなどでお馴染みの後置のifが欲しくなったら、ささっと定義できました。
loop Lazy(a) :=
a; loop a
無限ループ~。
ブロックやクロージャを作る特殊な構文を使ったりしなくても、 呼び出し側では普通の演算子/関数を呼んでいるかのような自然さで、 新しい制御文を幾らでも導入できてしまうわけです。 この柔軟さはなかなか美しいなぁ、と私などは感心することしきり。 なお値呼び(正格) と Lazy(非正格) を切り替えられる言語としては、 他に Clean や Haskell などがあります。
引数 -> 中身
で名無しの関数ができます。
(x -> x+x)(5).to_string.println
10.
詳しくはおいおい述べますが、引数が変数である必要はありません。 上の例は引数として数をとる例でしたが、今度は次の例は、 引数として 5 だけを取る関数です。なお、 今は無名関数の例で書いてますが、普通の関数定義でも 5 のみを取る関数、 は定義できます。
(5 -> 10)(5).to_string.println
10.
2 とか渡すと怒られます。5 じゃないので、実行前に型エラー。
(5 -> 10)(2).to_string.println
can't ensure 5. > 2., file test.me, line 2, characters 1-2, file test.me, line 2, characters 10-11
無名関数を複数個縦に並べると、並べた関数を上から見ていって、 引数と型が一致した物を適用、というどうさになります。
(5 -> 10
2 -> 8)(2).to_string.println
8.
というわけで、この機能はあたかもC/C++のswitch文のように使えます。
int_to_str(n) =
(
0 -> "zero"
1 -> "one"
2 -> "two"
_ -> "takusan"
)(n)
int_to_str(1).println
one
n の値が 0 だったら "zero"、1 だったら...
と文字列に変換する無名関数を定義して、そこに引数 n を渡しています。
ただ分岐がしたいだけなのに回りくどい…と思われるかも知れませんが、
回りくどいのは考え方だけで、見た目はフツーなので普通に気にせず使いましょう。^^;
_
は特別な記号ではなく、ただの変数名で、
他で使用されていないので任意の型の値を受け入れる、
default: になっているわけですね。
C++な人はboost::bindをご想像下さい。
sub(x,y) =
x - y
f = sub(10,)
f(3).to_string.println
7.
関数の引数を全部渡さずに、,
だけ書いて空欄にしておくと、
関数の部分適用になります。この例だと、f は f(x) = 10 - x
という関数。
sub(x,y) =
x - y
f = sub(,10)
f(3).to_string.println
-7.
第二引数を部分適用すると f(x) = x - 10
ですから、
結果は -7 、と。ML系言語のCurry化による方法と違い、
第一引数以外も自然な記法で部分適用できています。
apply_twice(f, x) = f(f(x))
sub(x, y) = x - y
apply_twice( sub(,10), 22 ).to_string.println
2.
「関数を2回適用する」という関数 apply_twice など、 関数を受け取って処理する関数、いわゆる高階関数に渡すときに便利という例でした。
この関数引数の「穴」は、関数定義の際にも空けられます。
int_to_str(defaultstr, ) =
0 -> "zero"
1 -> "one"
2 -> "two"
_ -> defaultstr
int_to_str("hoge",5).println
hoge
f(x,) は、「fの第一引数を x に固定したような関数」を返すのでした。
( add(10,)だったら、y->10+y
を。)ということは、
f(x,) = 定義
は、「fの第一引数を x に固定したような関数
を返す関数」の定義となるのが自然かと思われます。この例では、
defaultstrを受け取った時に、(0なら"zero",1なら"one"...)
という無名関数を返す関数として記述しています。
日本語でいうと多重定義。名前や記号は同じなのに、 使われる型によって違う実体を指すことを言います。 ってややこしく書いてますが、要は
("abc" + "def").println
(1 + 2).to_string.println
abcdef 3
例えば + という記号はオーバーロードされていて、 Stringという型に対して使うと文字列結合になって、 Numberという型に対して使うと数の足し算になります、と。 どの型に適用されているかは処理系側が自動で判定して適切な物を選んでくれます。 JavaやC++プログラマの方にもなじみ深いのではないかと。
しかしC++の場合、例えば関数ポインタとして渡すなど引数を指定しない時は、 「実際はどの実体が使われるのか」がコンパイラにわからないため、 結局キャストなどで明示的に型指定が必要でした。merd だと、
[1, 2, "hoge"].map( x->x+x )
2., 4., "hoehoge"
関数のオーバーロード状態を保ったまま渡して、 渡された関数の中で複数の型に対して適用することができます。
[1, 2, "hoge", y->y].map( x->x+x )
can't ensure x_ > "hoge" | 1.....2. | (y -> y), file test.me, line 1, characters 26-27, file test.me, line 1, characters 1-19
もちろん型チェックをすべて外せばこのような処理は簡単に実現されますが、 そうするとプログラムの安全性に不安が残ります。この言語の場合、 「関数y->yなどというものは足し算できないので実行前にはねる」 というような型安全性を保ったままオーバーロードが実現されています。
上のように関数の引数として渡せると言うことは、 単純な「名前が同じに宣言された関数を、多重に扱う」という処理ではなく、 「複数の実体がオーバーロードされている値」 という特殊な値を言語的に扱っている、ということを意味しています。
つまり、「この値とこの値を多重に重ねたい!」と思ったら、 その場でコードとして書くことができます。例を見てみましょう。 |&|演算子を使います。
x = 1 |&| "one"
x.println
(x + 3).to_string.println
"one" 4.
使われている場所に応じて数の 1 だったり文字列 "one" として扱われる値、x を定義しています。
僕は大嘘をついてしまった。
Pragma::prefix(-16,if then else)
if a then Lazy(b) else Lazy(c) :=
a.
True -> b
False -> c
merd では if~then~else もちゃんとライブラリで定義されていました。
一行目が、if then else が複数引数の前置演算子であることと、
その優先度の宣言。以下の行は、merd では A.B
が常に B(A)
と等しいことを思い出すと要するに、
(True -> b
False -> c)(a)
Trueならb,Falseならcを返す関数に a を渡しているということになります。
関数呼び出し式の中で、引数に穴を空けておくと、
埋めた引数をbindした関数を返してくれる機能を紹介しました。
これを if then else
に適用してみましょう。
(if then 1 else 2)(False)
2
条件式部分に何も書かず関数を作ってみました。Falseに適用すると無事 2 が返っています。
merd は「型付けの強い」言語です。つまり、 型エラーは全てコンパイル時に検出します。 …と、しかし、ここまで私は一度も変数や関数の型を宣言していませんでした。 merd では、変数の使われ方に基づいて、自動で「型推論」が行われます。
bool2str(b) =
if b then "true" else "false"
と関数を宣言したら、”if の条件部に置いてあるんだから、そりゃ変数 b の型は Bool だろう” とか ”返値になる値は"true"か"false"なんだから、 bool2str の関数としての型は Bool→String に決まり!” といった感じによきに計らってくれるのが型推論。
bool2str(0)
can't ensure Pervasives::Builtin::False | Pervasives::Builtin::True > 0., file test.me, line 4, characters 9-10
実際、型の違う値を渡そうとすると、上のように ”引数 0 が False|True の一種であることを保証できません” っというエラーが実行前に出てきます。 (その後実行が始まって再度実行時例外で停止したりしますが。) ここまではまぁ普通。
上の例で、"Bool" といった型の名前っぽいものは登場せずに、"False | True" という Boolの値の名前っぽいものが登場していたのにお気づきでしょうか? merdの型推論では、こんな風に、関数の定義を見て「取りうる値の範囲」 まで考慮した型チェックが行われています。
f(x) =
(1 -> "abc"
2 -> "def"
3 -> "ghi")(x)
この関数 f は例えばC言語で考えれば、char* f(int) という型になるでしょう。 整数を取って文字列を返す関数。対して merd の --give-types オプションで型情報を表示させてみると、こうなります。
>>> merd --give-types test.me - Test::Vars ----------------------------------------------- f !! 1.....3. -> "abc" | "def" | "ghi"
a !! b
は、a が b という型を持つ、という表現です。
つまり、f は、1~3 を取って "abc"か"def"か"ghi" を返す関数。
f(4) のような呼び出しをコンパイル時のエラーにできます。
switch の default を作って例外を投げたり、enum で定数に名前を付けたり、
subtypeキーワードで型の範囲を制限したり、といった処理は一切必要ありません。
この型システムがどんな風に優れているのか、もうちょっと見てみましょう。
1..3 なら取りうる値は全部数ですし、 "abc"|"def"|"ghi" なら全部文字列でした。 さて、こんな関数を考えてみましょう。
f(x) =
(1 -> "one"
"one" -> "ichi")(x)
f(1) # ok
f("one") # ok
# f(2) # コンパイルエラー
# f("ichi") # コンパイルエラー
>>> merd --give-types test.me - Test::Vars ----------------------------------------------- f !! 1. | "one" -> "ichi" | "one"
定義を見ると当たり前なんですけど、 1|"one" を取って、 "ichi"|"one" を返す関数、という型が付きました。 もはや 1|"one" は Int の部分型でもStringの部分型でもないため、 単なるenumやsubtype of XXX方式では表現できないことができています。
関数 f が上のような定義なら、値1 もしくは 値"one" を引数として受け取っている限りでは、絶対に型安全性は侵害されません。 ところが、大抵の「型づけの強い」言語では、この様な関数 f を、 ”Intを取るのかStringのかはっきりせい”と型エラーにしてしまいます。 逆に変数に型の無い言語では、1や"one"以外の引数も通してしまい、 実行時に何だかエラー、という悲しいことになります。 merdの型システムは、この欠点をどっちも解消しよう、という Soft-Typing 的なものになっているわけです。
def: a "type" is a "set of values"
merd/docs/TYPING
整数のリスト、とか文字列のリスト、だけでなく、 色んな要素が入りうるリスト、にも自然に型がつきます。
lst = [1, "234", [5,6], (x -> x)]
# lst !! List(1. | "234" | List(5.....6.) | (x -> x))
次に、任意のリストに対し、その全要素に対して関数を適用する、
という次のような関数を書いてみます。merd では、[] が空リスト、
先頭:残りのリスト
という形でリストを構築します。
例えば (1:2:3:4:[]) == [1,2,3,4]
。
で、前にやった「関数引数の穴」と「無名関数」の組み合わせで、
こんな風にかけます。
list_apply(fn,) =
[] -> []
hd:tl -> fn(hd) : list_apply(fn,tl)
# list_apply !! (h -> f), List(h) -> List(f)
次のように書くとコンパイルエラーになります。
list_apply( (x->x+1), lst )
lstの要素の中には、1 を足したりできないヤツが混じってる可能性がありますので。
list_apply( (x->x+x), lst )
これもだめ。x->x は自分とも足し算できません。
list_apply( (x->x,x), lst )
自分と自分のpairを作る、という演算ならどれに適用しても大丈夫なので、 こうならば無事コンパイルが通ります。
v = (x->x*x) |&| (x->x.capitalize)
# v !! (String -> String)
# |&| (-2147483648.....2147483647. -> -2147483648.....2147483647.)
縦棒 A|B|C
は A か B か C のどれか一つ、
という言ってみれば型どうしの or でしたが、
複数の実体をオーバーロードした場合は、A と B と C
を併せ持つ、いわば型の and が必要になります。
この場合は前回書いたように、A|&|B
のように表記されるそうな。
ここで overload と heterogeneous list を組み合わせる例を書こうと思ったのですが、 コンパイルが通らない…(T_T)
ff = (1->2 ;; 2->5 ;; 3->8)
gg =
[] -> []
h:t -> ff(h) : gg(t)
# ff !! 1.....3. -> 2.|5.|8.
# gg !! List(Subval |&| (h where h !< 1.....3.)) -> List(2.|5.|8.)
ff は、縦に書かずに ;; で区切って横に無名関数を並べたものです。
見たとおりの関数で、見たとおりの型がついています。注目すべきは、
gg の 「h where h !< 1.....3.
」この部分。
!<
というのは、「hが1..3の部分型(subtype)である」
と読んで、次のような意味になります。
D !< B
とは、「型 B の値が入るはずのところに型 D
の値が入っていても必ず安全に実行できる」こと。例えばオブジェクト指向の、
B が基底クラス、D が派生クラス、というのがその例。
この場合は、1..3 の部分型とは 1|2|3、1|2、2|3、1|3、1、2、3
の7種類のことですね。全体としては、gg は、
List(1..3) や List(1..2) や List(2..3) や…型の値を受け取って、
List(2|5|8) 型の値を返す関数、と。
…で、そんな当たり前のことが何故嬉しいのかというと。
C++で考えてみます。BaseがDerivedクラスの基底とすると、
Derived* !< Base*
という関係が成り立ちます。すると当然、
const std::list<Derived*>*
!< const std::list<Base*>*
も成り立つはずです。Base*のリストのつもりでDerived*のリストを操作しても、
問題はないはずなので。ところが、C++では、後者の部分型関係を
利用することができません。const std::list<Derived*>
と
const std::list<Base*>
が派生関係にないため、
その2者の間の変換をしようとするとエラーになってしまうからです。
boost::shared_ptr などでもこの部分型関係を取り戻すために、
templateを使ったトリックがわざわざ導入されています。
merd では、上でみたように、Listどうしのsubtype関係も自動で推論されるため、
プログラマは何も気にしなくてOK!
(※ 2004/10/13追記。上の段落で、
std::list
を const std::list
に変更。const
をつけないと、listの要素書き換えの時に型安全性が保たれないので、
二種類のリスト型は部分型関係になりません。水島さん指摘ありがとうございました。)
今までのところオーバーロードは、例えば
1..3 |&| String |&| List(5..8)
複数の型の&として扱われる、という話をしてきました。
ここで、オーバーロードの代表格、+演算子の型を見てみましょう。
NumberとStringとListに使える演算子なので、
Number|&|String|&|List
とかが出てくるのでしょうか? 否。
f(x) = x + x
# f !! x -> (x !< &Strict |&| &Addable) ; x
&Strict や &Addable、という見慣れぬものが出てきました。
f は、 &Strict と &Addable の部分型であるようなxを取ってxを返す関数、
と読めます。つまり、+
が具体的にどの型に対して定義されているかとかは関係なく、
&Addable
という抽象的なものの部分型でありさえすればいい、
という型が付いています。
このAddableというのは実は、型の種類を表す「型クラス」という代物です。 ライブラリのソースを見ると、
Addable = class
::+ := o,o -> o
::+= := Inout(o),o -> ()
times := o, Uint -> o
こんなのがありました。 「+と+=とtimes、という関数が適用できる型」という型の種類に Addable という名前を付けています。例えば 型String は 型クラスAddable のインスタンス、という言い方ができますね。
で、こう定義されていると、+ という関数の引数に対する型推論が働く際に、 実際のオーバーロードを探すのではなく、型クラスAddableで型が推論されます。 さて、この動作は何が嬉しいのでしょう?
型クラスは、適用できる関数の一覧を宣言する、 という点でC++の'concept'やJavaのinterface、に近い物があります。 interface、ということは、要するにこれで多態が実現されます。
型クラスなしの従来のオーバーロードで f(x) = x + x
をコンパイルする場合は、f は x に来る可能性のある型を
IntかStringかListか、と限定していました。これでは、
このfをコンパイルした後に作成した+のある新しい型にfを適用することができません。
ここはやはり、fは、「足し算ができる任意の型」を受け取って返す、
ということにしたい。それが Addable というわけ。
サンプルの猛烈に少ない解説で申し訳ないですが、おおよそそんな感じです。
実際にmerdで書かれたコードということで、付属のライブラリを覗いてみます。
まずは最も基本的な lib/Pervasives/Standard_classes.me
から。
名前の通り、「型クラス」を色々定義しています。面白そうな部分をピックアップ。
Default_val = class
*** := o
? := o -> Bool
::&&& := o,o -> o
::||| := o,o -> o
::&&&= := Inout(o), o -> ()
::|||= := Inout(o), o -> ()
o という部分に、Default_val 型クラスのインスタンス型が入ります。 つまり、*** という名前のo型の値と、&&& や ||| という 2項演算子などが定義されていれば、型oは、Default_val 型クラスのインスタンスになります。 感じとしては、*** がその型の"デフォルト値"。? がデフォルト値ならFalse、 そうでなければTrueを返す演算、などなど。
::? =
*** -> False
_ -> True
a &&& b := if a.? then b else ***
a ||| b := if a.? then a else b
a &&&= b := a = a &&& b
a |||= b := a = a ||| b
演算子達は上のようなgenericな定義が与えられているので、 事実上、*** が定義されていればこれらの演算が全て使えることになります。 例えばIntのデフォルト値は 0 なので、
read_from_buf(buf, bytes) =
bytes |||= buf.available_bytes
buf.read_impl(bytes)
読み込みバイト数として 0 が指定されたら、 今読めるだけ全部バッファから読み込むー、みたいなのが綺麗に書けます。 …しかし、&&& や ||| の第二引数は Lazy としておきべきじゃないんだろうか…。
「自分のもつ全要素に関数を適用する処理が存在する」というのが Mapable List(String) 型のリスト全体に対して String->Number 型の関数を適用して List(Number) 型のリストを返す、みたいな処理がありますから、 List は Mapable 型クラスのインスタンスとなれます。
Mapable(a) = class
map := o(a), a->b -> o(b)
map_withenv := o(a), env, a,env->b,env -> o(b),env
ここでは String などのように具体的な型でなく、List (of a)
のように、他の型 a を取ってはじめて List(a) という具体的な型を返す、
List という型構築子に関するクラスを考えています。こういう場合は
Mapable = class ...
ではなく Mapable(a) = class ...
という形で記述することに注意。C++でいうと、クラステンプレートに関する concept、
みたいな概念ですね。
[1,2,3,4,5].map( to_string )
# ["1", "2", "3", "4", "5"]
Enumerable(a) = class
each := O(a), a->() -> ()
find := O(a), a->Bool -> a
take_while:= O(a), a->Bool -> List(a)
size := O(a) -> Uint
empty? := O(a) -> Bool
to_list := O(a) -> List(a)
foldl := O(a), b, (b,a -> b) -> b
foldr := O(a), b, (a,b -> b) -> b
all? := O(a), a->Bool -> Bool
exists? := O(a), a->Bool -> Bool
collect := O(a), a->b -> (b !< Addable |&| Default_val) ; b
collect_withenv := O(a), env, a,env->b,env ->
(b !< Addable |&| Default_val) ; b,env
よくあるヤツ。()
ってのはunit型と言って、
CやC++のvoidみたいなもんです。何も意味のある値は返しません、と。
例えば List に関するEnumerableの実装は
each(,f) =
*** -> ()
e:l -> f(e) ; l.each(f)
find(,f) =
*** -> raise(Not_found)
e:l -> if f(e) then e else l.find(f)
とかなんとか。
先に説明しましたが、if / while / loop などの制御構造は全てライブラリ関数として実現されています。
n_times(, Lazy(a)) =
0 -> ()
n -> a ; (n-1).n_times(a)
こんなのとかも勿論あり。
なんだか当初の予定と反して型システムの話をあんまりやりませんでしたが、 この辺でこの記事は終了です。
感想としては、SoftTypingや関数の[穴]、Horizontal Layoutなど、 てきとーに書いたソースの適当さを、 処理系側でしっかりチェックしてからよきに計らって実行してくれる素敵な言語だなぁ、 といった感じ。「てきとーなの禁止!!」という厳格な言語でもなく、 とりあえず実行してしまってバグるルーズな言語でもない、 両者のいいとこ取りと言えるのではないでしょうか。かなり好きになりました。
短所といえばまだまだ開発途上で、バグが結構多いところでしょうか。 インタプリタ自体が平気で落ちますし。^^; 紹介はあるけれどまだ実装されていないっぽい Association Variable Name & Type などの楽しそうな機能と共に、 今後に期待ですね。
ではでは~。