Hello, polyglot!
きっかけ: るびま これを読んでpolyglotするかーって思った.
今回のコンセプトは
- 初めてなのでやさしくHello, world!
- 好きな言語を盛り込む
- 140文字以下
ソースコード
できあがったのがこれ.
;(#)={-"^<"v 'v+55p44p41< (define puts print) ;')#>:#,_@-}0;puts=putStrLn;main= (puts"Hello, world!")
言語紹介
Scheme
S式と呼ばれる単純な文法で記述する言語.括弧で括ると何かが起きる.行コメント ;
ネスト可能なブロックコメント #| ... |#
に加えて式を一つコメントアウトする式コメント #; expr
がある.
"
は文字列リテラルに使われるが, '
はquoteといって続く式は評価されずリテラルになる.'foo
はシンボルへ,'(foo bar baz)
はリストへと読まれる.
いろんな文字が識別子に使えるのも特徴.+ - < >
など.
Ruby
独特の複雑な文法で記述する,改行が意味を持つタイプの言語.あちこちで括弧を省略できる.#
で行コメントはできるが,複数行コメントはちょっと使い辛い.
"
と '
はどちらも文字列リテラルに使われる.似たようなものに /
で開始する正規表現リテラルがある他,%!...!
や %q!...!
(!
は任意の非英数字)等の多様なリテラルを持つ.
Haskell
インデントが意味を持つタイプの言語.トップレベルには(多分)宣言と定義しか書けないので,今回使用した言語の中では最も文法上の制約が強い.行コメントは --
ネスト可能なブロックコメントは {- ... -}
でできる.
静的型付き言語なので,型の整合性も取らなければならない.
Befunge
1文字1命令で,実行の向きを変えながら二次元格子状のコードを実行していく難解言語.コメントのための機能は無いが,意味のある命令以外は無視される.
スタック型.小さい整数を扱う事ができる.実行中のコードを読み書きできる.
Scheme着色版
;(#)={-"^<"v 'v+55p44p41< (define puts print) ;')#>:#,_@-}0;puts=putStrLn;main= (puts"Hello, world!")
Ruby着色版
;(#)={-"^<"v 'v+55p44p41< (define puts print) ;')#>:#,_@-}0;puts=putStrLn;main= (puts"Hello, world!")
Haskell着色版
;(#)={-"^<"v 'v+55p44p41< (define puts print) ;')#>:#,_@-}0;puts=putStrLn;main= (puts"Hello, world!")
Befunge着色版
;(#)={-"^<"v
'v+55p44p41<
(define puts print)
;')#>:#,_@-}0;puts=putStrLn;main=
(puts"Hello, world!")
解説
1行目
Schemeをコメントアウト.Haskellでは演算子 (#)
の定義の途中から4行目の途中までコメントアウト.これによってRubyも開き括弧だけしてコメントアウト.Befungeでは前半は無意味なコードで,後半にコード書き換え用の文字をスタックに積んで下へ.
2行目
Schemeでは quote
により離脱.同時にRubyは4行目の頭まで文字列リテラルにより離脱.Befungeではここを右から実行してきて,5行目の文字列を読むための命令を5行目に書き込む.
4行目
Schemeはコメントアウト.Rubyはここで文字列リテラルから復帰して括弧を閉じ,あとはコメント.Befungeではここ >:#,_
が出力部になっていて,スタックに積まれた文字列を出力し終了するようになっている.後半はHaskellがコメントから復帰し,(#)
の定義を終えて puts
を定義,main=
を置いておく.
没コード集
その1
;{->{55+48+v (define putStr display) '|#:\g5+9: <-1_' }=>0};alias putStr puts #>:#,_@|;-}main= (putStr"Hello, world!\n")
121打.(define putStr display)
がRubyでも文法上は妥当なことを利用し,ラムダ式 ->{ ... }
をコメント的に利用したコード.Rubyでは括弧を補うと (define(putStr(display())))
と解釈される.実行されなければどうということはない.1行目の 55+48+v
はBefungeのためのコードだが,2行目と同様にRubyでも文法OKなので書ける.
Schemeで |...|
が一つのシンボルになることを利用した式コメントが気持ちいい.
出力関数の名前はHaskellの putStr
に合わせている.
Befungeは文字列リテラル部分をg命令で読み取るようになっていて,長さが埋め込みなのがちょっと残念.
その2
eval'=eval'&&'p' {-0=>(alias putStr puts)} "> 55+48+v |#:\g6++49:<-1_ >:#,_@"#t(define putStr display)' ;#-}where _&&x=x main=(putStr"Hello, world!\n")
153打.eval
がSchemeでもRubyでも定義されていて,Befungeが下に脱出できるのを利用したコード.Befungeの変なコードなんか全部文字列にしちゃえば簡単じゃないかというコンセプト.しかし長くなりすぎた.似たような方法に format
を使う手がある.
その3
;{->{55+48+v (define puts print) '|#:\g4+7:<-1_' }=>':#,_@|;'}#-}puts=putStrLn;main= (puts"Hello, world!")
108打.その1の改良版.出力関数の名前はRubyの puts
に合わせている.
その4
;{->{"2^<"^ (define puts print) '|0/5p42p4<' }=>':#,_@|1;'}#-}puts=putStrLn;main= (puts"Hello, world!")
104打.その3の改良版.Befungeのアプローチを変えて,文字列の長さが埋め込みでなくなった.
Racketの継続プリミティヴ
Racketの継続がどうなっているのか,Racketのドキュメントを読みつつ分かったことをまとめる.
(注意: この記事は継続の解説ではない.寧ろ継続自体は大体分かっている前提で進める)
まず,主な継続プリミティヴについて簡単に説明する.次に,組み合わせたときの動作を例を出しつつ示す.さらに,これらの継続プリミティヴを使って shift
/ reset
や他の演算子が実装できることを確認する.
Racketの主な継続プリミティヴ
- (call-with-continuation-prompt proc [prompt-tag handler] arg ...)
-
略称
call/prompt
継続に区切り(継続プロンプト)を付けて proc を呼ぶ.区切られた継続の部分を継続フレームと呼ぶ(多分).継続プロンプトは prompt-tag で識別される.
reset
はこれのラッパマクロである.handler は,後述の
abort/cc
によってこのcall/prompt
呼び出しへ脱出してきた時に呼び出される.handler にはabort/cc
に渡した残りの引数が渡される.handler のデフォルト値は,(lambda (abort-thunk) (call-with-continuation-prompt abort-thunk prompt-tag #f))
である.残りの引数は単に proc に渡される.
- (abort-current-continuation prompt-tag v ...)
-
略称
abort/cc
現在の継続を破棄する.破棄されるのは,prompt-tag に対応する直近の継続プロンプトまでである.残りの引数は
call/prompt
のハンドラへと渡される. - (make-continuation-prompt-tag [sym])
-
新しいプロンプトタグを作る.
- (default-continuation-prompt-tag)
-
プロンプトタグを指定しない時に使われるデフォルトプロンプトタグを得る.
- (call-with-current-continuation proc [prompt-tag])
-
略称
call/cc
直近の継続プロンプトまでの継続を取り出す.この継続を起動すると現在の継続の一部を破棄する: 起動しようとする継続と現在の継続で継続フレームを共有している場合は,そのような最初の継続フレームまでが破棄される.さもなくば,直近の継続プロンプトまでの継続が破棄される.
- (call-with-composable-continuation proc [prompt-tag])
-
略称
call/comp
直近の継続プロンプトまでの継続を取り出す.この継続を起動しても現在の継続は破棄されない.
- (continuation-prompt-available? prompt-tag [cont])
-
((省略時は)現在の)継続において prompt-tag で識別される継続プロンプトが有効なら
#t
,さもなくば#f
を返す. - (dynamic-wind before body after)
-
基本的にR5RSのと同じだが,継続プロンプトによって継続を区切り,切り取ることができるので,ちょっと様子が異なる.
dynamic-wind
による動的ハンドラは,継続フレームにくっついている(多分)ので,継続プロンプトによって区切られ,call/cc
等で捕捉される継続に含まれない継続フレームに相当する部分の動的ハンドラは保存されない.
Racketの継続プリミティヴの相互作用
call/prompt + call/comp
(cons 'a (reset (cons 'b (call/comp (lambda (k) (cons 'c (k '()))))))) ; => (a b c b)
call/comp
の呼び出しから reset
(即ち call/prompt
) に戻るまでの継続に k が束縛される.
call/prompt + call/cc
call/comp
と同様に call/cc
の呼び出しから reset
に戻るまでの継続を捕捉するのだけど,この継続は起動したときに現在の継続の一部を破棄する性質を持つ.
(cons 'a (reset (cons 'b (call/cc (lambda (k) (cons 'c (k '()))))))) ; => (a b)
k を起動すると,まず現在の継続のうち reset
までが破棄される.つまり (cons 'b ... (cons 'c <>) ...)
の部分が破棄される.そして (cons 'a (reset <>))
の部分は残る.次に k に保存されていた継続を復元し,継続は (cons 'a (reset (cons 'b <>)))
となる.
この場合は,継続フレームが共有されているので,そのような最初の継続フレームまでが破棄されるパターンである.
以下のように,継続の起動を別の継続フレームにしても,根元の継続フレームが共有されているので同じ結果になる.
(cons 'a (reset (cons 'b (call/cc (lambda (k) (cons 'c (reset (k '())))))))) ; => (a b)
次の例は,継続フレームを共有していないパターン.
(let ((k #f)) (cons 'a (reset (cons 'b (call/cc (lambda (k1) (set! k k1) '()))))) (cons 'c (reset (cons 'd (k '()))))) ; => (c b)
k には (cons 'b <>)
の部分しか含まれていないので,この例は継続フレームを共有していないパターンになる.k 起動時の継続からは (cons 'd <>)
の部分だけが破棄され,k に保存されている (cons 'b <>)
に挿げ替わることになる.
call/prompt + dynamic-wind + x の準備
簡単のために
(define d displayln) (define (d$ x) (lambda () (d x))) (define (%dw x thunk) (dynamic-wind (d$ `(,x before)) thunk (d$ `(,x after)))) (define-syntax dw (syntax-rules () ((_ x body ...) (%dw x (lambda () body ...)))))
と定義しておく.
出力例
(dw 0 (d 'body)) ;; (0 before) ;; body ;; (0 after) (d (let/cc return (dw 0 (return 42)))) ;; (0 before) ;; (0 after) ;; 42
call/prompt + dynamic-wind + abort/cc
call/prompt
のデフォルトのハンドラがサンクを受け取るようになっているのは,(多分) shift
等の限定継続演算子を実装する時に都合がいいから.shift
の「脱出してから本体を実行する」という流れが,abort/cc
に本体を包んだサンクを渡すことで実現できる.
(reset (dw 0 (abort (d 'foo)))) ;; (0 before) ;; foo ;; (0 after) (reset (dw 0 (abort/cc (default-continuation-prompt-tag) (lambda () (d 'foo))))) ;; (0 before) ;; (0 after) ;; foo
abort/cc
に渡されるサンクは動的環境を巻き戻した後に呼び出されることになる.
abort
は
(define (abort . args) (abort/cc (default-continuation-prompt-tag) (lambda () (apply values args))))
と定義できる.
call/prompt + dynamic-wind + call/comp or call/cc
(let ((k #f)) (dw 0 (reset (d (call/comp (lambda (k1) (set! k k1) 'a))))) (dw 1 (k 'b))) ;; (0 before) ;; a ;; (0 after) ;; (1 before) ;; b ;; (1 after)
継続 k に保存される継続フレームには (dw 0 ...)
の呼び出しでインストールされた動的ハンドラ(以下 dw0 と呼ぶ)は含まれていない.間で reset
によって区切られているからである.よって k は,dw0 の内側で捕捉されたにもかかわらず,呼び出しても dw0 は呼び出されない.
また call/comp
によって捕捉された継続の呼び出し時には脱出を伴わない(=現在の継続は破棄されない)ため,(1 after)
は k の呼び出しから返ってきた後で出力される.
(let ((k #f)) (dw 0 (reset (d (call/cc (lambda (k1) (set! k k1) 'a))))) (reset (dw 1 (k 'b)))) ;; (0 before) ;; a ;; (0 after) ;; (1 before) ;; (1 after) ;; b
一方 call/cc
で捕捉した継続では,呼び出し時に脱出を伴う(=現在の継続が破棄される)ので,そのタイミングでafter thunkが実行される.
タグ付き限定継続
プロンプトタグを指定した場合,そのプロンプトタグによって識別される継続プロンプト以外の継続プロンプトは存在しないかのように扱われる(はず.でないと未知の継続プロンプトに干渉されることになるし,それを防ぐためのタグだよね).
以下,
(define t1 (make-continuation-prompt-tag 't1)) (define t2 (make-continuation-prompt-tag 't2))
と定義しておいて,
(reset-at t1 (cons 'a (reset-at t2 (cons 'b (call/comp (lambda (k) (k '())) t1))))) ; => (a b a b) (reset-at t1 (cons 'a (reset-at t2 (cons 'b (call/comp (lambda (k) (k '())) t2))))) ; => (a b b)
とすると,タグに対応する継続プロンプトまでの継続が捕捉される.
(let ((k #f)) (reset-at t1 (cons 'a (reset-at t2 (cons 'b (call/cc (lambda (k1) (set! k k1) '()) t1))))) (reset-at t1 (cons 'c (reset-at t2 (cons 'd (k '())))))) ; => (a b) (let ((k #f)) (reset-at t1 (cons 'a (reset-at t2 (cons 'b (call/cc (lambda (k1) (set! k k1) '()) t2))))) (reset-at t1 (cons 'c (reset-at t2 (cons 'd (k '())))))) ; => (c b)
call/cc
で捕捉した継続を起動したときも同様で,どの継続プロンプトまでの継続を破棄するかはタグで識別される.
dynamic-wind
は(恐らく)継続フレームに関連付けられるもので,タグの識別はない.
限定継続演算子を使って他の限定継続演算子を実装する
reset-at + call/cc で call/prompt + abort/cc
(define *current-prompt-alist* (make-parameter '())) (define (abort/cc tag . args) (apply (cdr (assoc tag (*current-prompt-alist*))) args)) (define call/prompt (case-lambda ((proc tag handler . args) (let* ((tag (or tag (default-continuation-prompt-tag))) (handler (or handler (lambda (thunk) (call/prompt thunk tag #f))))) (reset-at tag (call/cc (lambda (return) (handler (call/cc (lambda (abort) (parameterize ((*current-prompt-alist* (cons (cons tag abort) (*current-prompt-alist*)))) (return (apply proc args)))) tag))) tag)))) ((proc tag) (call/prompt proc tag #f)) ((proc) (call/prompt proc #f #f))))
call/cc
と継続プロンプトを設置するだけの reset-at
で,ハンドラ機能のついた call/prompt
と abort/cc
が実装できた(はず).ついでに1引数の continuation-prompt-available?
もすぐに実装できると思う.call/cc
の取り出す継続はその call/cc
自体の継続と同じだけど,隙の生じぬ二段構えで call/prompt
のように普通にリターンしてくるのと脱出してくるのを区別できる.
parameterize
の所これでいいのかちょっと不安だけど実は大丈夫…?
call/comp + abort/cc で call/cc
(define call/cc (case-lambda ((proc tag) (call/comp (lambda (k) (define call/cc-tag (make-continuation-prompt-tag 'call/cc)) (reset-at call/cc-tag (proc (lambda args (if (continuation-prompt-available? call/cc-tag) (abort/cc call/cc-tag (lambda () (apply values args))) (abort/cc tag (lambda () (apply k args)))))))) tag)) ((proc) (call/cc proc (default-continuation-prompt-tag)))))
call/cc
で捕捉された継続を起動すると,その継続に入る前に脱出する.継続の中身自体は call/comp
と同じはず.abort/cc
を使うと丁度トランポリンできるので,そこで call/comp
で捉えた継続を呼び出すようにすればいいみたい.
起動する継続と現在の継続で継続フレームを共有している場合,つまりその call/cc
の動的寿命の中にいるときは,その call/cc
呼び出しまで脱出する必要がある.ある継続フレームの動的寿命の中にいるかどうかは,reset-at
と continuation-prompt-available?
で判定できる.そして call/cc-tag を作っておいて一先ずそれで脱出する.でその後再び abort/cc
を使って(継続フレームを共有していない場合と同様に) (abort/cc tag (lambda () (apply k args)))
してもいいんだけど,ここで破棄しようとしている継続は k と同じなので,ここは単に (apply values args)
でいい.
call/prompt + call/comp + abort/cc で shift
(define (call-with-shift proc) (call/comp (lambda (k) (abort/cc (default-continuation-prompt-tag) (lambda () (proc (lambda args (reset (apply k args))))))))) (define-syntax shift (syntax-rules () ((_ k e ...) (call-with-shift (lambda (k) e ...)))))
shift
のやることは,「限定継続を捕捉→脱出→限定継続を継続プロンプトで包んで本体に渡す」なので,call/prompt
と call/comp
と abort/cc
があればすぐできる.
多分包みの reset
を抜けば control
も同様に実装できる.shift0
/ reset0
は reset0
側でハンドラがトランポリンするときにプロンプトを再設定しないようにすればいいんじゃないかな.control0
/ prompt0
も同様.
MIT/GNU Scheme 9.1.1
MIT/GNU Scheme - GNU Project - Free Software Foundation
今回は無気力なのとなんかいろいろあってよく分からないのでバイナリを入れた."Compiled on OS X 10.6" だし.x86-64のをダウンロードしてコピーして終わり.
かと思いきやパスにコロンが入ってて PATH
が通らないので(よく分からなかった)リンクをはる方向に.と思ってたら丁度そういうサイトをみつけた.
Using MIT-Scheme in MacOS X on the Command Line - Dustin Ingram
リンクして
sudo ln -s /Applications/MIT\:GNU\ Scheme.app/Contents/Resources/mit-scheme /usr/local/bin/mit-scheme
設定ファイルして
export MITSCHEME_LIBRARY_PATH="/Applications/MIT\:GNU\ Scheme.app/Contents/Resources"
動いた.
s id id idに型が付かない件について
s id id idに型が付かない.
ghci> let s f g h = f h $ g h ghci> :t s id id id <interactive>:1:5: Occurs check: cannot construct the infinite type: a = a -> b Probable cause: `id' is applied to too few arguments In the second argument of `s', namely `id' In the expression: s id id id
フンギャー
gosh> (define (s f g x) ((f x) (g x))) s gosh> (s values values values) #<subr values>
Schemeなら問題なく動くのに!ムキー!
って考えてたらもっと短く,型が付かないコードを見付けた.
ghci> let w x = x x <interactive>:1:10: Occurs check: cannot construct the infinite type: t = t -> t1 Probable cause: `x' is applied to too many arguments In the expression: x x In the definition of `w': w x = x x
いや,待てよ.Haskellには魔法の関数 unsafeCoerce
があるじゃないか!
ghci> import Unsafe.Coerce ghci> let w' x = x $ unsafeCoerce x ghci> (w' id :: a -> a) 42 42
これを使えばさっきの関数も…!
ghci> let s' f g h = f h $ unsafeCoerce $ g h ghci> :t s' s' :: (t -> b1 -> b) -> (t -> a) -> t -> b ghci> (s' id id id :: a -> a) 42 42
動いた!わーい!
何かがおかしい…
ところで s'
を a -> a
に特化した s''
を書こうと思ったのだが
ghci> let s'' f g h = s' f g h :: a -> a <interactive>:1:16: Inferred type is less polymorphic than expected Quantified type variable `a' is mentioned in the environment: f :: t -> b1 -> a -> a (bound at <interactive>:1:8) In the expression: s' f g h :: a -> a In the definition of `s''': s'' f g h = s' f g h :: a -> a
ghci> let s'' f g h = s' f g h `asTypeOf` h ghci> :t s'' s'' :: (t -> b1 -> t) -> (t -> a) -> t -> t ghci> s'' id id id <interactive>:1:4: Occurs check: cannot construct the infinite type: t = b1 -> t Probable cause: `id' is applied to too many arguments In the first argument of `s''', namely `id' In the expression: s'' id id id
ghci> let s'' :: (a -> a) -> (b -> b) -> (c -> c) -> c -> c; s'' f g h = s' f g h <interactive>:1:54: Occurs check: cannot construct the infinite type: c = c -> c When generalising the type(s) for `s'''
分からん…
c-wrapperのインストール
c-wrapper,c-wrapper は更新されていないけど http://hg.koguro.net/c-wrapper で更新されていた.ダウンロード→タグからダウンロードできる.
./DIST gen ./configure CC=/usr/bin/gcc make DYLD_INSERT_LIBRARIES=/usr/lib/libffi.dylib make check
普通にやると (use c-wrapper)
でエラーになる.
$ gosh gosh> (use c-wrapper) *** ERROR: Compile Error: Compile Error: Compile Error: failed to link /usr/local/lib/gauche-0.9/site/x86_64-apple-darwin10.8.0/c-ffi.so dynamically: dlopen(/usr/local/lib/gauche-0.9/site/x86_64-apple-darwin10.8.0/c-ffi.so, 10): Symbol not found: _ffi_type_double Referenced from: /usr/local/lib/gauche-0.9/site/x86_64-apple-darwin10.8.0/c-ffi.so Expected in: flat namespace in /usr/local/lib/gauche-0.9/site/x86_64-apple-darwin10.8.0/c-ffi.so "/usr/local/share/gauche-0.9/site/lib/c-wrapper/c-parser.scm":31:(define-module c-wrapper.c-parser (u ... "/usr/local/share/gauche-0.9/site/lib/c-wrapper.scm":31:(define-module c-wrapper (use srfi-1 ... "(standard input)":1:(use c-wrapper) Stack Trace: _______________________________________ 0 (eval expr env) At line 179 of "/usr/local/share/gauche-0.9/0.9.4_pre3/lib/gauche/interactive.scm"
DYLD_INSERT_LIBRARIES=/usr/lib/libffi.dylib
をつけると動くので,これが見えていないのが問題らしい.otoolとかinstall_name_toolとかあたってみたがよく分からず,DYLD_INSERT_LIBRARIES
を指定しなくてもすむ方法は分からなかった.
$ otool -L /usr/local/lib/gauche-0.9/site/x86_64-apple-darwin10.8.0/c-ffi.so /usr/local/lib/gauche-0.9/site/x86_64-apple-darwin10.8.0/c-ffi.so: /usr/local/lib/libgauche-0.9.dylib (compatibility version 0.0.0, current version 0.0.0) /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 125.2.11)
ここに/usr/lib/libffi.dylibがないといけないのではないかと予想したのだけどなぁ.
それとなんかmakeの最初の方でconfigureみたいなのが走っているのだけど,rm: conftest.dSYM: is a directory
って出てこけまくっているように見える.大丈夫なのだろうか.
型クラスのパラメタ多相?
Either a b
(但しaとbは何らかの共通の制約を持つ)に適用できる f g = either g g
という関数を書こうとしたが,書けなかった.特定の型クラスに限定すれば書くことができる.
{-# LANGUAGE RankNTypes #-} fshow :: (Show a, Show b) => (forall c. (Show c) => c -> d) -> Either a b -> d fshow g = either g g feq :: (Eq a, Eq b) => (forall c. (Eq c) => c -> d) -> Either a b -> d feq g = either g g -- f :: (k a, k b) => (forall c. (k c) => c -> d) -> Either a b -> d -- f g = either g g -- => malformed class assertion g h = print $ map h [Left 42, Right 'a'] main = g (fshow show) >> g (feq (\x -> x == x))
Show
や Eq
のところをパラメタにすればよさそうに思ったのだけど,GHCではコンパイルエラーになってしまった.一体どうすれば…