TaPL読書録 #10
前回:
番号がやたらとんでるのは、勉強会はつつがなく開催されていたのですが僕が書くのをめんどくさがっていたら書かれなかったという回がたくさんあるからです。
今回は chap13最初-13.3まで。勉強会の slack があるのですが、TaPLチャンネルではなぜか Rust の話しかしておらず、当日の様子が全く振り返れず謎です。
いままで純粋な言語機能しかなかったが、今回からは純粋でないものを扱う。chap13では特に参照について話す。
p.120 オブジェクト
の話があるが、「副作用を及ぼす関数のかたまり」として定義しているような印象を受けた
p.121 ぶらさがり参照 = dangling reference
「明示的な解放操作のもとでは、型安全性の達成が極めて困難になる」
p.122
非解釈の集合Lをストアでの位置の集合とし
ここでいう「非解釈の」は、集合の要素の並びとか型とかそういうのは気にしないぜ、くらいの意味合いで言っていると思われる。
中段
...がストアμを変更せずに返すことに注意されたい。関数適用は、それ自体は何の副作用も起こさない。
t の操作はすべて純粋な操作であり、ここでは副作用とはμに対する変更として表されている。
脚注
例えばストアについて、位置nがFloatを保持しているという事実は、位置n+4の方について有用な情報を何も与えない。」
それをプログラマに任せているという話であり、人間はよく自分の足を撃つのであった……。
次回 chap 13 終わりまで。
オフラインリアルタイムどう書く E29 の誤回答 (Ruby)
タイトルどおり爆死しました
問題 : http://nabetani.sakura.ne.jp/hena/orde30sumt/
実装リンク集 : https://qiita.com/Nabetani/items/725e09cc5913a8569c04
以下考えたこと。
ざっくりいうと、最終行の必要な幅からどんどん上にいくにつれて必要な幅がたされていく。最終的に、その値がいきつくのが1行目なので、開始終了の幅の分だけとってきたら合計するだけ。
と思っていたのだけどそれでやると40ケース以上おちまして。
だめなパターンがこんな感じ。
あー、これで足すやつどうやって調べようかなーと思っていたら時間切れ。
あとこれは「面白い判定」をそんなに真面目にやってないので、試してないけど並びによってはたまたま面白い数が並んでしまう(例えば上図なら3と5のところ)と合算値が合わなくなる。残念賞ーーー!!
メモ化使いつつ計算するほうほうで再実装するか迷ってるんだけど、書いても無難になりそうなのでスキップ。久々に手ひどく負けたのでした……。
gowrtr で名前付き戻り値を扱う
先日リリースされた gowrtr が非常に便利でめちゃくちゃ助かってます。buf に自分で fmt.Fprintf とかしてたのが懐かしいです。
お気に入りポイントは、immutable なつくりになっているところと、goimports とかをかけた状態で出力できるところです。マジでいい
特に goimports をかけてくれるおかげでパッケージの import 文とかはすげえ適当に書いておいて後からツールに削ってもらうというのが可能になります。makeファイルに後処理書かなくて済んでうれしい。
さて、そんな go-wrtr で func の戻り値を定義しようとすると *FuncSignature
に対して AddReturnTypes
とすることになります。
package main import ( "fmt" "github.com/moznion/gowrtr/generator" ) func main() { g := generator.NewRoot() sig := generator.NewFuncSignature("foo") sig = sig.AddReturnTypes("int32", "error") g = g.AddStatements( generator.NewFunc( nil, sig, generator.NewReturnStatement("1, nil"), ), ) src, err := g.Gofmt("-s").Goimports().Generate(0) if err != nil { panic(err) } fmt.Print(src) }
$ go run tekitou.go func foo() (int32, error) { return 1, nil }
しかし自動生成するときには名前付き戻り値を使いたくなったりします。具体的にはライブラリの関数をラップする関数を書くとき、途中でエラーになったから早期リターンしたいが、各戻り値のデフォルト値を毎回書くのがダルいとかです。
// Foo() (int32, string, err) のようなシグネチャだとして func WrapFoo() (int32, string, error) { err := preprocess() if err != nil { return 0, "", err // ここでデフォルト値を書くのがめんどい } return Foo() } func WrapFoo() (i int32, s string, err error) { err = preprocess() if err != nil { return // こうしたい } return Foo() }
しかし、AddReturnTypes
は string しか受け付けません。これはどうするのかというと AddReturnTypes にそのまま文字列で渡しちゃうのがよさそうです。
package main import ( "fmt" "github.com/moznion/gowrtr/generator" ) func main() { g := generator.NewRoot() sig := generator.NewFuncSignature("foo") sig = sig.AddReturnTypes("i int32", "err error") // 名前も書いちゃう g = g.AddStatements( generator.NewFunc( nil, sig, generator.NewReturnStatement("1, nil"), ), ) src, err := g.Gofmt("-s").Goimports().Generate(0) if err != nil { panic(err) } fmt.Print(src) }
と、ここまで書いたところで気づいたのですが、実は多値を返さずに1つしか return しないときは生成時にエラーになっちゃいますね……。多値のときにお試しください。1
2019-01-21 00:54 ライブラリの名前が間違ってたので直しました(ハイフンとった)
-
パッチ書いてみる予定↩
gocode での補完をやめるが vim-go は使う
どうも vim 界隈でも lsp の機運が高まっており gocode ではなく golsp などを使うことを推奨されているっぽい。
一方で今はもうすでに vim-go に手が馴染んでいるわけだが、と思って腰が重かったのだけど、補完部分だけを golsp にすることができた(気がする)
雰囲気で gocode とか使ってそうなところを消している。
実際書いてるとこんな感じに golsp が vim から立ち上がってるのがわかるのでまあよさそう。
$ pstree -s golsp -+= 00001 root /sbin/launchd \-+= 01328 hkdnet tmux \-+= 29150 hkdnet -zsh \-+= 45875 hkdnet vim \--= 46034 hkdnet golsp -mode stdio
なお最近は GoLand で書いている
低レイヤを知りたい人のための Cコンパイラを Rust で作っている
↑の続きというわけで Rust 版を作っている
Rust 化して気づいたのは Vec
また、Node について binary operator を考えると当然 Node<Node, Node> のようなものにならざるを得ないのだが1、この場合に Node が再帰的に定義されてしまうため、sizeof(Node)
が一意に定まらないという問題があることがわかった。この場合に Rust 的には Box
あー、Aに必要なメモリを確保しようとしたときに、再帰的にAをもってると何バイト必要なのかが定まらなくて困るのか。そこでヒープに確保してそこへのポインタをもつようにしてやればサイズが決定されると
— はくどー (@HKDnet) January 4, 2019
Rust 特有のアレコレについて悩みながらもやっている。特にCが書けるようになりたいわけでもないので2このまま Rust 版を育てるかもしれない。
『低レイヤを知りたい人のための Cコンパイラ作成入門』感想
インターネット上で公開されている範囲については実装が終わりました。
内容について
非常によかったです。わかりやすかったです。僕は技術書の場合、だいたい冒頭だけ読んで、実装してみて、本の内容と比較するという読み方をしています。この本については、ある機能を実装するにあたって、「このへんは今はやらなくていいや」と割り切るのが上手いと感じました。実装していると「これもやらなきゃじゃん」「こうなってるほうがいいよなー」みたいな気持ちがどんどん生まれますが、実際にを読んでみると「これは後ででいいです」とバッサリ。これはCコンパイラを複数回作った経験とコンピューターサイエンスに関する体系的な知識があるがゆえにできているのかなーと思っています。
このコンテンツを読む前に期待している点としては、「言語実装に関する理解が深まること」と「アセンブリ、機械語を生成するフェイズの知識が得られること」の2点でした。やってみた感じとしては、前者は特に深まったなーと思っています。tokenize, parse というフェイズごとに何をしているのかという点や実際のバイナリをつくるまでの過程というのが理解できました。
一方で後者は、いまだによくわかりません。吐くアセンブリの意味を正しく理解できているかというとあやしいし……。これはどちらかというと文章としての問題ではなく、読み手としてアセンブリと仲良くなる努力がまだ足りてないのかなーと思っています。どうにかしないとなー。いい教材みたいなのがあると助かります。fizzbuzzみたいなのを自分で書いてみるか。
これから
上記のアセンブリと仲良くなるっていうのと、あとはCで書くのがダルいので他言語で実装しなおすかもしれません。あるいはもうちょい自力ですすめるか。あと解析フェイズの理解が進んだせいで TaPL の言語実装が適当すぎることに気づいたのでそっちを直したくもなっています。どうしよう。
動いているようす
こんな感じになります。ちゃんと動いてますね。中間生成物のアセンブリもチラ見せ。
$ make test bin/hkdcc -test OK ./test.sh 0; => 0 42; => 42 1+2; => 3 1+10+2; => 13 3-1; => 2 23-12; => 11 3 - 1; => 2 70+2*5*5; => 120 4/2; => 2 5+6*7; => 47 5*(9-6); => 15 (3+5)/2; => 4 1; 2; => 2 a = 1; => 1 a = 1; a; => 1 a = 1; a + 1; => 2 a = b = 1; a + b; => 2 1 == 1; => 1 a = 1; 1 == a; => 1 a = 2; 1 == a; => 0 a = 2; a == 1; => 0 a = 2; a == a; => 1 a = 1 == 1; a; => 1 a = 0 == 1; a; => 0 1 != 1; => 0 2 != 1; => 1 OK $ cat tmp.s .intel_syntax noprefix .global _main _main: push rbp mov rbp, rsp sub rsp, 208 push 2 push 1 pop rax pop rdi cmp rdi, rax setne al movzx rax, al push rax pop rax mov rsp, rbp pop rbp ret
signal.Notify したチャンネルを close すると死ぬ(可能性がある)
TL; DR
signal.Stop
しよう
https://golang.org/pkg/os/signal/#Stop
検証
ダメな例
package main import ( "fmt" "os" "os/signal" "syscall" "time" ) func main() { c := make(chan os.Signal, 1) signal.Notify(c, syscall.SIGINT, syscall.SIGTERM) close(c) fmt.Println("C-c plz") time.Sleep(10 * time.Second) }
$ go run main.go C-c plz ^Cpanic: send on closed channel goroutine 5 [running]: os/signal.process(0x10dbc60, 0xc00002aa30) /usr/local/Cellar/go/1.11.1/libexec/src/os/signal/signal.go:227 +0x163 os/signal.loop() /usr/local/Cellar/go/1.11.1/libexec/src/os/signal/signal_unix.go:23 +0x52 created by os/signal.init.0 /usr/local/Cellar/go/1.11.1/libexec/src/os/signal/signal_unix.go:29 +0x41 exit status 2
ある channel がクローズ済かは送信者はわからないので仕方がない。
大丈夫な例
package main import ( "fmt" "os" "os/signal" "syscall" "time" ) func main() { c := make(chan os.Signal, 1) signal.Notify(c, syscall.SIGINT, syscall.SIGTERM) signal.Stop(c) // signal.Stop を追加した close(c) fmt.Println("C-c plz") time.Sleep(10 * time.Second) }
$ go run main.go C-c plz ^Csignal: interrupt
OK
背景
graceful restart 的なことをするときに、シグナルでハンドリングをしていた。のだが、場合によっては即座に死んでほしくないパターンがあった。API通信相手がメンテ中だとわかっている場合とかは即死すると即死→メンテ→即死→メンテの無限ループって怖くね?状態になる。
しょうがないのでメンテのときはある程度待ってから死ぬようにしてみるか、と思って書いているわけだが、シグナルハンドリングが終わったあとの息が長くなるとリソースの解放漏れが気になってきた。そこでさくっと close しようとしたら、これ死ぬんじゃね?って思って調べて今に至る。
ここまで書いた後でさっさと死んで起動直後にメンテ中か確認すれば特に悩む必要なかったなって思った。おしまい。