この記事はQiitaで公開されていました
GoとLimboは、一部のファンから、似ていると言われますので、そんなに似ているのか、実際に比較してみました。ついでに、この2つの言語は共通してAlefが先祖になりますので、Alefも含めます。
いちおうこの記事は、Go Advent Calendar(その1)の17日目です。
概要
簡単なレシートプリントの機能をGoで書いて、それをAlef、Limboでも実装してみます。実用性に関しては、気にしてはいけません。
- 商品はチャネル経由で取り出す
- 未登録会員、無料会員、有料会員の3つがある
- 未登録の場合は宛名がない
- 無料会員、有料会員は宛名が先頭に追加される
- 有料会員の場合のみ、1000円以上なら10%引きになる
Goの場合
まずはGoで実装します。
$GOPATH/src/goadvent/membership/m.go
会員・商品関連の処理をmembershipパッケージに実装しました。会員情報だけではないので、このパッケージ名は良くないですね。
package membership import "fmt" type Product struct { ID int Name string Price int } type Query struct { ProductID int Result chan *Product } func Serve(products []*Product, request <-chan *Query) { for q := range request { p := lookup(products, q.ProductID) q.Result <- p } } func lookup(products []*Product, id int) *Product { for _, p := range products { if p.ID == id { return p } } return nil } type Printer interface { Print(p *Product) } func Print(request chan<- *Query, id int, d Printer) { q := Query{ ProductID: id, Result: make(chan *Product, 1), } request <- &q p := <-q.Result d.Print(p) } type Guest struct { } func (*Guest) Print(p *Product) { fmt.Println("Product", p.Name) fmt.Println("Price", p.Price) } type Free struct { Email string } func (u *Free) Print(p *Product) { fmt.Println("User", u.Email) fmt.Println("Product", p.Name) fmt.Println("Price", p.Price) } type Premium struct { Email string } func (u *Premium) Print(p *Product) { fmt.Println("User", u.Email) fmt.Println("Product", p.Name) fmt.Println("Price", u.Discount(p.Price)) } func (*Premium) Discount(price int) int { if price >= 1000 { price -= price / 10 } return price }
print文を何度も書いている部分は、text/templateを使ったほうがよかったり、本当はパッケージ名.型名
でひとつの名前にするべきなのにmembership.Product
という意味の分からない名前になっていたりしますが、忘れましょう。
$GOPATH/src/goadvent/main.go
membershipパッケージを叩くだけのドライバです。 なんの面白みもないコードですね。
package main import "membership" func main() { products := []*membership.Product{ &membership.Product{ID: 1, Name: "sample1", Price: 100}, &membership.Product{ID: 2, Name: "sample2", Price: 1000}, } request := make(chan *membership.Query) go membership.Serve(products, request) membership.Print(request, 1, &membership.Guest{}) membership.Print(request, 2, &membership.Free{Email: "a@example.com"}) membership.Print(request, 2, &membership.Premium{Email: "b@example.com"}) }
コンパイル方法
$ go build .
実行結果
これを実行すると、次の結果が得られます。
Product sample1 Price 100 User a@example.com Product sample2 Price 1000 User b@example.com Product sample2 Price 900
Limbo
では次に、Limboで同じ処理をする実装をしましょう。
LimboはInfernoというOSにおける(ほぼ)唯一の言語で、Disという仮想マシン上で動作します。この言語はCのように、ヘッダファイルと実装で分かれています。ヘッダは習慣的に.mで、実装は.bをファイル名に持ちます。
/module/membership.m
まずはヘッダから。
Membership: module { PATH: con "/dis/lib/membership.dis"; Product: adt { id: int; name: string; price: int; }; Query: adt { productid: int; result: chan of ref Product; }; Member: adt { pick { Guest => Free => email: string; Premium => email: string; } print: fn(u: self ref Member, p: ref Product); discount: fn(u: self ref Member, price: int): int; }; init: fn(); serve: fn(products: array of ref Product, request: chan of ref Query); print: fn(request: chan of ref Query, id: int, d: ref Member); };
構造
Limboは大きな枠組みとしてモジュールがあり、その中に、型であったりとか、関数・メソッドであったりとかが定義されます。モジュールは、Goにおいてはパッケージが近いかもしれません。
スタイル
Limboは名前の先頭が大文字か小文字かを区別しません。習慣的には、型やモジュール名は大文字、それ以外はすべて小文字です。
また、Goと同じく型は後に置きますけれど、名前と型の区切りに:が入ります。array of ref Product
のように、そのまま読めるものになっています。
pickについては後述します。
/appl/lib/membership.b
Membershipモジュールの実装です。
必ずimplement モジュール名;
で開始します。
implement Membership; include "sys.m"; sys: Sys; include "bufchan.m"; bufchan: Bufchan; include "membership.m"; init() { sys = load Sys Sys->PATH; bufchan = load Bufchan Bufchan->PATH; }
この処理はGoのimport
と似ていますが、import
と異なり、モジュールのロードは自分で行わなければいけません。init()という関数も特殊なものではありません。(勝手に呼ばれたりしません)
Bufchanについては後述します。
serve(products: array of ref Product, request: chan of ref Query) { while((q:=<-request) != nil){ p := lookup(products, q.productid); q.result <- = p; } } lookup(products: array of ref Product, id: int): ref Product { for(i := 0; i < len products; i++) if(products[i].id == id) return products[i]; return nil; } print(request: chan of ref Query, id: int, d: ref Member) { sq := chan of ref Product; c := bufchan->bufchan(sq, 1); q := ref Query(id, c); request <-= q; p := <-q.result; d.print(p); } Member.print(u: self ref Member, p: ref Product) { pick t := u { Guest => sys->print("Product %s\n", p.name); sys->print("Price %d\n", p.price); Free => sys->print("User %s\n", t.email); sys->print("Product %s\n", p.name); sys->print("Price %d\n", p.price); Premium => sys->print("User %s\n", t.email); sys->print("Product %s\n", p.name); sys->print("Price %d\n", t.discount(p.price)); } } Member.discount(u: self ref Member, price: int): int { case tagof u { tagof Member.Guest or tagof Member.Free => raise "not implement"; tagof Member.Premium => if(price >= 1000) price -= price / 10; return price; } raise "unknown membership"; }
pickとtagof
pickを使うと、ひとつの型(adt)について、追加のメンバー変数をもたせることができるようになります。
C言語で、
struct Node { int type; union { Sym *sym; vlong vconst; } u; };
と実装していたところは、pickを使うと自然に記述できますし、実際そういうところに使うようです。
tagofも似たような用途です。
raise文
Limboは例外をサポートしています。あまり例外をハンドルすることはありませんが、取り扱う場合は以下のように文字列マッチングを行います。
{ # 例外が発生する場所 } exception e { "error:*" => # error:*でマッチした場合の処理 "*" => # それ以外すべて }
Bufchanについて
Limboのチャネルは、バッファリングを行いませんし、サポートしません。そのため、自分でバッファリングを行うチャネルを実装しなければいけません。
/module/bufchan.m
Bufchan: module { PATH: con "/dis/lib/bufchan.dis"; bufchan: fn[T](c: chan of T, size: int): chan of T; };
ジェネリクス
Limboはジェネリクスが使えます。
/appl/lib/bufchan.b
Bufchanモジュールの実装です。このコードは、The Limbo Programming Languageに紹介されているものを少し変更しているだけですので雰囲気だけ察してください。
implement Bufchan; include "bufchan.m"; xfer[T](oldchan, newchan: chan of T, size: int) { temp := array[size] of T; fp := 0; n := 0; dummy := chan of T; sendch, recvch: chan of T; s: T; for(;;){ sendch = recvch = dummy; if(n > 0) sendch = newchan; if(n < size) recvch = oldchan; alt{ s = <-recvch => temp[(fp+n)%size] = s; n++; sendch <- = temp[fp] => temp[fp++] = nil; n--; if(fp>=size) fp -= size; } } } bufchan[T](oldchan: chan of T, size: int): chan of T { newchan := chan of T; spawn xfer(oldchan, newchan, size); return newchan; }
/appl/cmd/sample.b
最後にMembershipモジュールを使うコマンドを実装します。
implement Sample; include "sys.m"; sys: Sys; include "draw.m"; draw: Draw; include "membership.m"; membership: Membership; Member: import membership; Sample: module { init: fn(ctxt: ref Draw->Context, argv: list of string); }; init(nil: ref Draw->Context, nil: list of string) { membership = load Membership Membership->PATH; membership->init(); products := array[] of { ref membership->Product(1, "sample1", 100), ref membership->Product(2, "sample2", 1000), }; request := chan of ref Membership->Query; spawn membership->serve(products, request); membership->print(request, 1, ref Member.Guest); membership->print(request, 2, ref Member.Free("a@example.com")); membership->print(request, 2, ref Member.Premium("b@example.com")); }
その他の特徴
Limboには、言語仕様にタプル型とリスト型が存在します。タプルはほぼGoの多値を返す関数と同じように使いますが、ただの値なので、チャネルをそのまま通せることが便利ですね。
c := chan of (int, string); c <-= (0, "test");
また、Limboのチャネルは、配列としてまとめて扱えます。Goではreflect.Select()
として提供されています。
a := array[2] of chan of int; a[0] = chan of int; a[1] = chan of int; # 送信可能なチャネルどれかに送信 a <-= 3; # 受信可能なチャネルどれかから受信 (i, n) := <-a;
Alef
最後にAlefで実装します。
Alefは、Plan 9用に設計された、Cの後継を目指した言語です。Cにチャネル(と便利な機能)を加えたもの、が適切な表現かなと思います。
残念ながらAlefは、Plan 9 3rd edition(2000年)から無くなってしまいましたが、有志が現在でも実行可能にしたソースがありますので、それを使わせてもらいました。
membership.h
Alefは、Cと同じくヘッダファイルを使います。宣言の並びも、記号もほとんどCのままですね。
aggr Product { int id; byte *name; int price; }; aggr Query { int productid; chan(Product*) result; }; enum { M_GUEST, M_FREE, M_PREMIUM, }; adt Guest { int dummy; /* メンバーが1つ以上必要なので... */ }; adt Free { extern byte *email; }; adt Premium { extern byte *email; int discount(int); }; adt Member { int type; union { Guest; Free; Premium; }; void guestinit(*Member); void freeinit(*Member, byte*); void premiuminit(*Member, byte*); void print(*Member, Product*); }; void memberserve(Product **products, int n, chan(Query*) request); void memberprint(chan(Query*) request, int id, Member *d);
aggrとadt
aggrは、Cの構造体と同じような、メソッドを持たない型を作成します。adtはそれに加えて、メソッドやアクセス制御の機構を持たせることができます。
アクセス制御
adtのメンバー変数は、特に指定しなければプライベート、メソッドは、指定しなければパブリックとして扱われます。逆にしたい場合はintern
またはextern
キーワードを加えます。
型の埋め込み
Alefは(実はLimboもですが)型の埋め込みをサポートしています。上記の例では、Member型のunionが埋め込まれていますし、unionのメンバー変数もそれぞれ埋め込まれています。
メソッド
メソッドの最初の引数で、変わった型の書き方をしています。
void guestinit(*Member) { ... }
これは、Goでは以下の記述に相当します。
func (*Member) GuestInit() { ... }
また、
void guestinit(.Member) { ... }
これは以下に相当します。
func (Member) GuestInit() { ... }
membership.l
Alefの実装ファイルは習慣的に、ファイル名に.lを使います。名前空間の概念がありませんので、関数名がかぶらないように工夫が必要です。
#include <alef.h> #include "membership.h" Product * lookup(Product **products, int n, int id) { int i; for(i = 0; i < n; i++) if(products[i]->id == id) return products[i]; return nil; } void memberserve(Product **products, int n, chan(Query*) request) { Query *q; Product *p; while((q=<-request) != nil){ p = lookup(products, n, q->productid); q->result <-= p; } } void memberprint(chan(Query*) request, int id, Member *d) { chan(Product*)[1] c; Query *q; Product *p; alloc c, q; q->productid = id; alloc q->result; request <-= q; p = <-q->result; d->print(p); } void Member.guestinit(Member *u) { u->type = M_GUEST; } void Member.freeinit(Member *u, byte *email) { u->type = M_FREE; u->Free.email = email; } void Member.premiuminit(Member *u, byte *email) { u->type = M_PREMIUM; u->Premium.email = email; } void Member.print(Member *u, Product *p) { switch(u->type){ case M_GUEST: print("Product %s\n", p->name); print("Price %d\n", p->price); break; case M_FREE: print("User %s\n", u->Free.email); print("Product %s\n", p->name); print("Price %d\n", p->price); break; case M_PREMIUM: print("User %s\n", u->Premium.email); print("Product %s\n", p->name); print("Price %d\n", u->Premium.discount(p->price)); break; } } int Premium.discount(int price) { if(price >= 1000) price -= price / 10; return price; }
まだCの名残が強いですね。それに、宣言を先にしておかなければいけないのが、現在ではしんどいなと思います。
チャネルの作成
Alefはバッファリング付きチャネルをサポートしています。
chan(Product*)[1] c;
alloc c;
これは、以下と同等です。
c := make(chan *Product, 1)
sample.l
#include <alef.h> #include "membership.h" void main(void) { chan(Query*) request; Member g, f, m; Product *products[2]; alloc request, products[0::2]; *products[0] = (1, "sample1", 100); *products[1] = (2, "sample2", 1000); proc memberserve(products, 2, request); g.guestinit(); memberprint(request, 1, &g); f.freeinit("a@example.com"); memberprint(request, 2, &f); m.premiuminit("b@example.com"); memberprint(request, 2, &m); }
イテレータ
alloc products[0::2];
これは、以下と同じです。
for(i = 0; i < 2; i++) alloc products[i];
procとtask
GoやLimboは、並行処理をさせるための命令はひとつ(go, spawn)しかありませんが、Alefはprocとtaskの2つが存在しています。
procは、他のprocと同時に実行されるようにスケジュールされます。taskはそれが呼ばれたprocの中で動作し、procの中において常に実行されているのは1つだけになるようなスケジューリングが行われます。
コンパイル
コンパイルするときは、アーキテクチャごとにコンパイラを使い分けます。昔のGoみたいですね。
8al membership.l 8al sample.l 8l sample.8 membership.8
その他の特徴
Alefは他にも、面白い実験的な機能が用意されていました。
この例では使いませんでしたが、Limboのジェネリクスと同じ構文でテンプレートが使えます。(型が作られるたびにコードが生成されるのでバイナリは大きくなりがち)
また、
par{ func1(); func2(); }
とすると、parの中に記述した処理それぞれを並列に実行し、すべてが完了するのを待ってからブロックを抜ける処理がかけます。
エラー処理に関しては、Alefはエラー処理に特化したdeferのような機能を持っています。
fin = open(file1, OREAD); rescue { close(fin); return; } fout = open(file2, OWRITE); rescue { close(fout); raise; } if(write(fout, buf, n) != n) raise;
エラーが発生した場合、下から順番に、rescueブロックに書いた内容が実行されます。
まとめ
この3つの言語はどれも、チャネルがあり、並行処理のための命令があり、すべてシステム記述用の言語なので似ていると思っていましたが、比較してみるとまったく別の言語としか思えませんでした。
作られた時代が古いのもあるので単純な比較はできませんけれど、Goはとても書きやすく、いい言語だということを再認識しました。