『Scalaスケーラブルプログラミング第3版』読了

Scalaスケーラブルプログラミング第3版

Scalaスケーラブルプログラミング第3版

読了しました。いわゆるコップ本。手を動かした結果は以下のレポジトリに置きました。

github.com

注意点

まず最初に手を動かすにあたっての注意点が1つ。 2.12.8 ではパーザコンビネータとSwingに関しては別ライブラリを追加する必要がありました。 過去のバージョンでは標準ライブラリとして添付されていたものが分離したようです。

$ scala -version
Scala code runner version 2.12.8 -- Copyright 2002-2018, LAMP/EPFL and Lightbend, Inc.

https://github.com/hkdnet/cup-book/blob/4c855822a4711b2677507f0018f240a43eb1c28d/build.sbt#L14-L15

// build.sbt
libraryDependencies += "org.scala-lang.modules" %% "scala-parser-combinators" % "1.1.2"
libraryDependencies += "org.scala-lang.modules" %% "scala-swing" % "2.1.1"

感想

Scala 面白いですね。強力な言語だなーと思いました。

特に言語機能として面白いのは implicit ですかね。最初見たときはこれなにが起きてるかわけがわからんのでは……と思ったのですが、4つのリーズナブルな制約のもとでならそんなに変なことにならなそうでした。import をどこにでもかけてそれがレキシカルに参照されるので、読んだ当初は Ruby の refinements っぽいなーという気持ちになりました1

あと trait に、「これを継承するなら継承するやつはこの型でなければならない」ということを明示できるのは面白かった。

PFDSやTaPLをやんわり読んでいるおかげでかなりスッと腹落ちしやすかったという感じはあります。純粋関数型の Deque とか出てくるし。償却時間とか言い出すし。共変・反変、Top/Bot あたりも出てくるし2。学んだことが別の本に出てくると嬉しいですね。

純粋関数型データ構造

純粋関数型データ構造

型システム入門 −プログラミング言語と型の理論−

型システム入門 −プログラミング言語と型の理論−

  • 作者: Benjamin C. Pierce,住井英二郎,遠藤侑介,酒井政裕,今井敬吾,黒木裕介,今井宜洋,才川隆文,今井健男
  • 出版社/メーカー: オーム社
  • 発売日: 2013/03/26
  • メディア: 単行本(ソフトカバー)
  • クリック: 68回
  • この商品を含むブログ (11件) を見る

一方でなんかちょっと深入りしてないトピックもありそうでした。上述の variance の + - を書けるところのルールとかはかなり難しそうですし。あとパス依存型もさっと出てきて引っ込んでいったのであまりよくわかっていない。パス依存型のほうは言われて気づいたのでそういえばよくわからねえな、くらいのわかってなさです。

あとコレクションを自前実装するところ、制約が多くて実装者に知識を要求するなーという印象。まあいくつかの triat を無視しててもあとで必要になったときに実装すればいいのかもですが。

もともと Scala 学習のモチベは elasticmq のソースを読むところだったのですがそっちは全然手がついてません。たぶんこの本では触れられてなかったトピックとして、 Akka とかのことを知る必要がありそう。

github.com

elasticmq を読むモチベは仕事で使うからだったのですが、まあなんやかんやあって仕事で使わなくなったのでこの先学習するかはまた気分次第ですね。 別件でCコンパイラとかやってたらCPUの気持ちが知りたくなってきたのでまた別の分野に手を出すかもしれません。


  1. 今思うと定義済みメソッドの挙動を変えるまではいかないしそうでもないかもしれません。

  2. いやまあ型の話をしたら不可避でしょうけど

C拡張のある gem を作れるようになる

はじめに

fukuokarb.connpass.com この記事は上記イベント会場で書いています。 Speee さん、 Fukuoka.rb の皆様ありがとうございます。

そういえばC拡張のgemって作ったことないなと思ったので作りました。RubyKaigi いくとなんかこういうことやりたくなりますよね。

https://github.com/hkdnet/fibc

このエントリではC拡張の gem ってどうなってるんだっけの解説、というほど高尚なものでもないですが今回認識したことを書きます。

C拡張を動かす

今回はCで複雑なロジックを書くことは考えず、フィボナッチ数列の n 番目の数を返すメソッドを追加することにする。つまりこんなメソッドを追加することを目的とする。

Fibc.fib(5) # => 8

bundle gem --ext fibc で雛形ができる。 --ext でC拡張用のファイルも作ってくれる。gemspec にTODOとかが追加されているので、以下それを解消しているものとして扱う。

ファイル構成はだいたいこんな感じ。

.
├── Gemfile
├── LICENSE.txt
├── README.md
├── Rakefile
├── bin
│   ├── console
│   └── setup
├── ext
│   └── fibc
│       ├── extconf.rb
│       ├── fibc.c
│       └── fibc.h
├── fibc.gemspec
├── lib
│   ├── fibc
│   │   └── version.rb
│   └── fibc.rb
└── spec
    ├── fibc_spec.rb
    └── spec_helper.rb

実は Rakefile にもそういう定義が追加されている。さっと確認すると compile などという単語が見え、:default のタスク定義にも compile が追加されているので、ビルドしてからテストするような感じになることがわかる。

$ bundle exec rake -T
rake build            # Build fibc-0.1.0.gem into the pkg directory
rake clean            # Remove any temporary products
rake clobber          # Remove any generated files
rake compile          # Compile all the extensions
rake compile:fibc     # Compile fibc
rake install          # Build and install fibc-0.1.0.gem into system gems
rake install:local    # Build and install fibc-0.1.0.gem into system gems without net...
rake release[remote]  # Create tag v0.1.0 and build and push fibc-0.1.0.gem to TODO: ...
rake spec             # Run RSpec code examples
# Rakefile
require "bundler/gem_tasks"
require "rspec/core/rake_task"

RSpec::Core::RakeTask.new(:spec)

require "rake/extensiontask"

task :build => :compile

Rake::ExtensionTask.new("fibc") do |ext|
  ext.lib_dir = "lib/fibc"
end

task :default => [:clobber, :compile, :spec]

gemspec の定義にも spec.extensions = ["ext/fibc/extconf.rb"] という設定があり、gem install 時にどう build すればよいのかを示していることがわかる。extconf.rb については後述する……予定だったがまあ実際 makefile を生成してるっぽいことくらいしかわからなかった。生成後の結果は tmp 配下に吐かれるので確認可能。

rake compile すると lib/fibc/fibc.bundle というファイルができる。これがC拡張をコンパイルしたもの。 最初 gem のエントリポイントである lib/fibc.rbrequire 'lib/fibc/fibc' と書いてあって、そんなのないじゃんって思ってたのだけど、コンパイルしたものを require している。

ここまでわかったので、あとは ruby/ruby のコードを参考にしながら雑にメソッドを追加してみる。C言語において Ruby のモジュールを定義しているところを適当に参考にしながらやればよい。RubyKaigi で大人気だった RubyVM::AbstractSyntaxTree.of の定義している箇所を参考にする。ちょうどよく module に1引数のシングルトンメソッドを定義するという意味で非常に似通っている。

ruby/ast.c at 03c6cb5e8f503dcf6bc80a70a48a9775bbd2a47e · ruby/ruby · GitHub

cruby の世界では、Rubyのオブジェクトはすべて VALUE 型であり、また Ruby のメソッドは必ず戻り値をもつ。なので (VALUE self, VALUE n) -> VALUE な関数を定義してそれを使うことにする。とりあえずステップバイステップに実装するために、引数をそのまま戻す関数を定義してみる。

diff --git a/ext/fibc/fibc.c b/ext/fibc/fibc.c
index 71788c7..4beb8a1 100644
--- a/ext/fibc/fibc.c
+++ b/ext/fibc/fibc.c
@@ -2,8 +2,15 @@
 
 VALUE rb_mFibc;
 
+static
+VALUE fibc_fib(VALUE self, VALUE n)
+{
+    return n;
+}
+
 void
 Init_fibc(void)
 {
   rb_mFibc = rb_define_module("Fibc");
+  rb_define_singleton_method(rb_mFibc, "fib", fibc_fib, 1);
 }

テストは適当に書いて動作確認おk1。 あとは適当にCで書いた関数を使いつつ FIX2INT, INT2FIX を使いながらCの世界とRubyの世界のオブジェクトを変更しておけば動く。

せっかくなのでベンチマークをとってみる。手元のマシンでやった適当な結果ですけどやっぱはやい。

$ cat bench.rb
require 'benchmark/ips'
require 'fibc'

Benchmark.ips do |x|
  # These parameters can also be configured this way
  x.time = 5
  x.warmup = 2

  # Typical mode, runs the block as many times as it can
  x.report("naive") { Fibc.naive(10) }
  x.report("fib") { Fibc.fib(10) }

  # Compare the iterations per second of the various reports!
  x.compare!
end

$ bundle exec ruby bench.rb
Warming up --------------------------------------
               naive    19.611k i/100ms
                 fib   223.955k i/100ms
Calculating -------------------------------------
               naive    195.295k (± 8.4%) i/s -    980.550k in   5.061215s
                 fib      4.174M (± 9.1%) i/s -     20.828M in   5.043693s

Comparison:
                 fib:  4174204.0 i/s
               naive:   195295.2 i/s - 21.37x  slower

動いてC拡張すごい、という気持ちになったところでおわり。ここまでは特に難しくなかったですね。

C拡張書くときに何が使えるのかとかがわからなかったり、Rubyでの処理がCのどの関数呼べばいいのかとかがわからないので次はもうちょい実践的な何かをチャレンジしていきます。

おまけ

objdump するとダミーシンボル的なものがあることがわかる(わからない(読めない

$ objdump -no-show-raw-insn -arch-name x86-64 -macho -x86-asm-syntax intel -D lib/fibc/fibc.bundle 
lib/fibc/fibc.bundle:
(__TEXT,__text) section
_Init_fibc:
     ec0:   push    rbp
     ec1:   mov rbp, rsp
     ec4:   lea rdi, [rip + 213] ## literal pool for: "Fibc"
     ecb:   call    0xf5c ## symbol stub for: _rb_define_module
     ed0:   mov qword ptr [rip + _rb_mFibc], rax
     ed7:   lea rsi, [rip + 199] ## literal pool for: "fib"
     ede:   lea rdx, [rip + _fibc_fib]
     ee5:   mov ecx, 1
     eea:   mov rdi, rax
     eed:   pop rbp
     eee:   jmp 0xf62 ## symbol stub for: _rb_define_singleton_method
     ef3:   nop word ptr cs:[rax + rax]
     efd:   nop dword ptr [rax]
_fibc_fib:
     f00:   push    rbp
     f01:   mov rbp, rsp
     f04:   mov rdi, rsi
     f07:   call    0xf68 ## symbol stub for: _rb_fix2int
     f0c:   mov edi, eax
     f0e:   call    _fibc_fib_int
     f13:   cdqe
     f15:   lea rax, [rax + rax + 1]
     f1a:   pop rbp
     f1b:   ret
     f1c:   nop dword ptr [rax]
_fibc_fib_int:
     f20:   push    rbp
     f21:   mov rbp, rsp
     f24:   push    r14
     f26:   push    rbx
     f27:   mov r14d, 1
     f2d:   cmp edi, 2
     f30:   jl  0xf53
     f32:   mov ebx, edi
     f34:   add ebx, 2
     f37:   mov r14d, 1
     f3d:   nop dword ptr [rax]
     f40:   lea edi, [rbx - 3]
     f43:   call    _fibc_fib_int
     f48:   add r14d, eax
     f4b:   add ebx, -2
     f4e:   cmp ebx, 3
     f51:   jg  0xf40
     f53:   mov eax, r14d
     f56:   pop rbx
     f57:   pop r14
     f59:   pop rbp
     f5a:   ret

  1. 見たければコミットを追ってください

GitHub の PullRequest でもう使ってない外部サービスがチェックに出てきて邪魔なとき

レポジトリ側の Settings > Branches のブランチルールに残っているケースが多い。

ブランチ側のルールの Require status checks to pass before merging から該当のサービスを抜いてやればよい。

f:id:hkdnet:20190405134049p:plain

f:id:hkdnet:20190405134150p:plain

TaPL読書録 #10

前回:

hkdnet.hatenablog.com

番号がやたらとんでるのは、勉強会はつつがなく開催されていたのですが僕が書くのをめんどくさがっていたら書かれなかったという回がたくさんあるからです。

今回は 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)

タイトルどおり爆死しました

yhpg.doorkeeper.jp

問題 : http://nabetani.sakura.ne.jp/hena/orde30sumt/
実装リンク集 : https://qiita.com/Nabetani/items/725e09cc5913a8569c04

以下考えたこと。

ざっくりいうと、最終行の必要な幅からどんどん上にいくにつれて必要な幅がたされていく。最終的に、その値がいきつくのが1行目なので、開始終了の幅の分だけとってきたら合計するだけ。

と思っていたのだけどそれでやると40ケース以上おちまして。

だめなパターンがこんな感じ。

f:id:hkdnet:20190203152545p:plain

あー、これで足すやつどうやって調べようかなーと思っていたら時間切れ。

あとこれは「面白い判定」をそんなに真面目にやってないので、試してないけど並びによってはたまたま面白い数が並んでしまう(例えば上図なら3と5のところ)と合算値が合わなくなる。残念賞ーーー!!

メモ化使いつつ計算するほうほうで再実装するか迷ってるんだけど、書いても無難になりそうなのでスキップ。久々に手ひどく負けたのでした……。

github.com

gowrtr で名前付き戻り値を扱う

先日リリースされた gowrtr が非常に便利でめちゃくちゃ助かってます。buf に自分で fmt.Fprintf とかしてたのが懐かしいです。
お気に入りポイントは、immutable なつくりになっているところと、goimports とかをかけた状態で出力できるところです。マジでいい

moznion.hatenadiary.com

特に 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 ライブラリの名前が間違ってたので直しました(ハイフンとった)


  1. パッチ書いてみる予定