「すごいHaskell たのしく学ぼう!」を読んだ

- 作者: Miran Lipovaca
- 出版社/メーカー: オーム社
- 発売日: 2012/09/21
- メディア: Kindle版
- 購入: 4人 クリック: 9回
- この商品を含むブログを見る
本書は一度は8章あたりで挫折したが、今回13章あたりまで読みファンクタ―、アプリカティブファンクタ―、モノイド、モナドといった概念がなんなのか理解とまでは言えないけど知ることができた。
一度は挫折したが今回またリベンジしようと思った理由は、今後モバイルアプリを開発していくにあたって関数型プログラミングの概念を理解して採り入れていくことが必要になってくると思ったからだ。Swiftはletによる不変型の宣言やOptional型などの文脈付きの型など関数型プログラミング言語としての側面をもっていると思う。また、データバインディング(SwiftBond/Bondなど)やJSONのパース(thoughtbot/Argoなど)といった場面で関数型プログラミングの概念が登場してきている。Swiftのポテンシャルを最大限に発揮して、堅牢で生産性の高いコードを書くには関数型プログラミングの知識が必要になってきていると最近感じている。
本書を読んだ結果として、データの構造について新しい視点を得ることができた。MaybeやEitherといった概念を"文脈"と呼んでいるのが自分の中にはなかった発想だった。例えば、MaybeとMaybe Intを区別して考えるのはとても抽象的だけど強力な考え方と思った。Maybeは「あるかもしれないし、ないかもしれない」という文脈を表し、Maybe Intは「Int型かもしれないし、何もないかもしれない」型を表している。これらを分けることで、文脈を保ったまま計算するという発想が出てくるのだと思う。文脈を保ったまま計算する段階として、本書ではFunctorやApplicative、そしてMonadが登場してきた。
Swiftでは、Haskellにおける型コンストラクタにあたる概念がない。Genericsを使うことでMaybeのような型を表現することはできるが、ある型が型引数をとるのかとらないのか、とるとしたらいくつとるのかを知る術はない(はず)。Haskellではそれらは種類という概念で説明されている。Maybeの種類はMaybe :: * -> *だし、Eitherの種類はEither: * -> * -> *となっているので、それぞれ型引数を1つと2つとることがわかる。HaskellのFunctorは種類が* -> *の型コンストラクタしかインスタンスにできないのだけど、こういう概念をSwiftで表現できない。
というわけで、Swiftで関数型プログラミングをするにはHaskellほどうまくはできないことがなんとなくわかった。Genericsなどで擬似的に表現するしかない。Functorのfmapを以下のように実装してみた。
extension Optional { func fmap<U>(f: T -> U) -> U? { switch self { case .Some(let value): return f(value) case .None: return .None } } } let maybeOne: Int? = 1 let maybeTen = maybeOne.fmap({ x in x * 10 })
SwiftのOptional<T>型はつまりT?型のことなのだけど、Optional型を拡張してfmapを追加している。return f(value)のところは暗黙的にU?型にラップしている。このように実装することで、Optional型のもつ「あるかもしれないし、ないかもしれない」という文脈を保ちつつ、中身の1というIntを計算している。
ここではFunctorだけを簡単に実装してみたが、これに加えてApplicativeとMonadを実装するとより抽象的な計算が可能になってくる。JSONのパースなどを実装する際にはApplicativeの操作が必要になってきそうな感じがする。自分はまだ関数型プログラミングの実装を実際にしたわけではないので、理解したとは到底いえない。パーサーの実装をしてみたり、上で紹介したライブラリのコードを読んでみることで関数型プログラミングを実践的に理解していきたい。
naoty/todoとnaoty/nowisで定期的なtodoを管理できるようにした
前回のエントリで紹介したnaoty/todoと今回作ったnaoty/nowisを組み合わせることで、定期的なtodoをコマンドラインで管理できるようにした。
使い方
$ nowis saturday && echo 'Today is Saturday!' Today is Saturday!
nowisコマンドは、現在時刻が引数で与えた曜日かどうかを判定して真なら終了コード0を返し偽なら1を返す。上のように&&で任意のコマンドと組み合わせることで、特定の曜日だけ実行できるようになる。
定期的なtodoの管理
nowisを組み合わせて定期的なtodoを管理するにはいくつか方法が考えられるが、zshの設定ファイルを使う。
# .zlogin nowis sunday && todo add --once 部屋を掃除する (nowis tuesday || nowis thursday) && todo add --once 燃えるゴミを出す
上のように設定することでzshにログインするたびに上のスクリプトが実行される。todo add --onceで既に存在する場合は追加しないようにできるので、これで特定の曜日になると自動的にtodo addされるようになる。
15分くらいで作ったので現状は曜日の判定しかできないけど、応用範囲が広そうなのでもうちょっと細かく判定できるようにするかも。
自分専用のtodo管理ツールを書いた
最近、プライベートでの開発したいことや勉強したいことが増えてきたので、それらを管理するツールを書いた。
使い方
$ todo add Go言語を勉強する $ todo add todo管理ツールを書く $ todo add ブログ記事を書く $ todo list [ ] 001: Go言語を勉強する [ ] 002: todo管理ツールを書く [ ] 003: ブログ記事を書く $ todo done 1 $ todo done 2 [x] 001: Go言語を勉強する [x] 002: todo管理ツールを書く [ ] 003: ブログ記事を書く $ todo clear $ todo list [ ] 001: ブログ記事を書く
その他、todoの削除や移動などができる(詳細はGitHubのページを参照)。個人的に便利だと思っている機能がtodoをmarkdownのtask list形式で出力する機能だ。
$ todo list -m - [x] Go言語を勉強する - [x] todo管理ツールを書く - [ ] ブログ記事を書く
これを使ってQiita:Teamの日報に今日やったこと、やれなかったことを簡単にコピペできる。一日の作業フローはこうだ。
todo listで残タスクを確認する。- 適宜
todo addでタスクを追加したり、todo moveで順番を入れ替えて優先度を調整する。 - 完了したら
todo doneでタスクを完了させる。 - 一日の終わりに
todo list -mで作業内容を出力してQiita:Teamにコピペして、感想などを付け加えて日報として公開する。 todo clearで完了したタスクを消去する。
tips
- todoはLTSV形式のファイルとして保存され、ファイルのパスは
TODO_PATHという環境変数で指定できる(デフォルトはHOME)。なので、環境変数でDropbox内のパスを指定すれば簡単にtodoを同期できる。 - zimbatm/direnvを使うと、プロジェクトルートに
cdしたときにTODO_PATHを書き換えられるのでプロジェクトのスコープのtodoを別に管理できる。
実装
最近はGoが気に入っているので、コマンドラインツールを作るときはすべてGoで書いている。CLIを作る際のフレームワークはいくつかあるようだが、一番Starが多そうだったcodegangsta/cliを使っている。標準の出力とmarkdown形式の出力の切り替えを実装する際にinterfaceを使ってみた。ファイルの入出力にはioutilパッケージが手っ取り早かった。ファイルの扱いを通じてio.Writerインターフェイスについても理解が深まった。
done
$ todo done 1
#potatotips でTimepieceについて発表した
potatotips
資料
最近のTimepiece
- GW前あたりから急激にバズってきた。一時GitHubのトレンドで1位になった。それまでは☆70くらいだったけど、もうそろそろ☆500になりそうな勢いだ。
- それに伴っていくつかの要望をPull requestでいただいた。それらはほぼすべてmergeした。機能追加やバグ修正まで自分では見落としていた部分を指摘していただいて、多くの方に使われていそうだという実感がある。
イベントの感想
- 最近はiOSではなくAndroidアプリ開発をしているので、iOS/Android両方楽しめて非常に良かった。
- Timepieceを検討したけど採用を見送った方の意見を聞けたのが非常に良かった。そういう方の意見を聞ける機会は多くないからだ。いただいた要望について今実装方針を考えていて、ちゃんと形にしていきたい。
- 最近気になっているResultについての議論はとても勉強になった。naoty/SwiftCSVでエラー情報を扱う際にResultが使えそうだと思っていた。ただ、議論を聞いてオレオレResultが乱立しそうな流れがありそうだというのを知った。そうなると、ライブラリ提供者が実装するよりも利用者側でResultを定義する方が利便性を損ねないのでは、という意見に変わった。
- ドキュメントだけではよく理解できなかったDagger 2については、あまりよくわかってなかった
@Provideについて理解が深まった。Androidのテストについて意見交換をさせていただいて、自分の意見は間違ってなさそうだという確信を得られたのもよかった。 - その他、Androidの
@Nullable,@NonNullはすぐに使おうと思ったし、Lastlaneやdeliverといったワークフローを自動化するツールも実践的な内容で勉強になった。
コミット毎に実行環境をビルドするoasisを書いた
Dockerの理解を深めるため、またGo言語の経験を積むためにoasisというツールを書いた。「とりあえず動いた」レベルの完成度であり、実用で使うにはもっと時間をかけて改善していく必要がある。
これはコミット毎の実行環境をdockerのコンテナとして提供するリバースプロキシだ。例えば、以下のようにoasisを起動する。
% oasis start \
--proxy master.oasis.local:8080 \
--container-host 192.168.99.100 \
--repository github.com/naoty/sample_rails_app
このときhttp://master.oasis.local:8080にアクセスすると、oasisは以下のようなことを行う。
--repositoryで指定されたリポジトリをgit cloneする。- サブドメインで指定されたリビジョン、ここでは
masterにgit checkoutする。 - リポジトリに含まれるDockerfileを使って
docker buildする。 - ビルドしたイメージを
docker run -P -dして、コンテナを起動する。 - コンテナのホスト側ポート(例:
49154)を調べて、oasisへのアクセスを--container-hostで指定されたホスト上のコンテナ(例:192.168.99.100:49154)にリダイレクトする。

実際にOSXで試す場合は、--proxy 127.0.0.1:8080のようなオプションで起動して、サブドメインの解決をPowに任せるといいと思う。
% cd ~/.pow % echo 8080 > oasis
上のようにすると、http://*.oasis.devのように任意のサブドメインにアクセスできるようになり、8080ポートのoasisにポートフォワーディングされる。
所感
もともとは同僚の方が開発に携わっているmookjp/poolを見て、もうちょっとシンプルにセットアップできるようにしたいと思ったのがきっかけだった。実行ファイルをダウンロードして即実行できるようなものが理想だったので、Go言語を勉強しはじめこんなものを作ってみた。名前の「oasis」は最近ハマっているドミニオン・異郷に出てくるアクションカードであること、コンセプトのオリジナル実装であるpoolに雰囲気が似ていることから採った。
Go言語はとてもシンプルですんなり理解できたし、標準パッケージでリバースプロキシを簡単に実装できたため、短時間でここまで作ることができた。ちょっとしたツールを作るとき、これまではRubyでrubygemを書くようなことをしていたが、Go言語であればrubygemを書くほどのハードルの高さもなく、シンプルで生産性の高いコードを書いてそのまま配布することができていい感じだなと思った。
また、DockerについてもDocker Remote APIを触ってみたり、docker runの-pと-Pの違いを理解できたり、理解が深まったと思う。あんまり関係ないけど、サンプルアプリで使ったDockerfileはDocker Hubで配布された公式のrails用イメージを使ってるだけで、何も考えなくてよくて便利だった。
ghqを読んだ
Goの勉強のため、普段からお世話になっているmotemen/ghqを読むことにした。なお、現在の僕のGoの知識はgotourを完走した程度だ。最初から現在のコミットを追いかけるのは骨が折れそうだったので、最初のコミットbad21c7df65ccefd74530d6fcc5f0707b63e0266から読むことにした。
Goのプログラムはmainパッケージのmain()から実行されるため、main.goのmain()から読む。
import { // ... "github.com/codegangsta/cli" } func main() { app := cli.NewApp() app.Name = "ghq" app.Usage = "Manage GitHub repository clones" app.Version = "0.1.0" app.Author = "motemen" app.Email = "motemen@gmail.com" app.Commands = []cli.Command{ { Name: "get", Usage: "Clone/sync with a remote repository", Action: CommandGet, }, { Name: "list", Usage: "List local repositories", Action: CommandList, Flags: []cli.Flag{ cli.BoolFlag{"exact, e", "Exact match"} } } } app.Run(os.Args) }
cliパッケージはcodegangsta/cliというコマンドを簡単に作成するライブラリのもののようだ。cli.NewApp()は*cli.App(構造体Appのポインタ)を返している。この構造体はCLIアプリケーションを表している。これに続くコードはそのCLIアプリケーションの情報を設定している。app.Commandsというフィールドにはcli.Command型のスライスが入る。cli.Command型はCLIアプリケーションのサブコマンドを定義するために使われる。サブコマンドの名前、ドキュメント、フラグなどを設定し実際に実行される関数を指定することができる。実行される関数はActionというフィールドに指定する。このフィールドはfunc(context *Context)という型になっている。ここでは、getとlistというサブコマンドが定義されており、それぞれCommandGet,CommandListという関数が実行されるように設定されている。- 最後に
app.Run()でコマンドライン引数を受け取ってCLIアプリケーションを実行している。
とりあえずgetサブコマンドを理解したいので、CommandGetを見ていく。
func CommandGet(c *cli.Context) { argUrl := c.Args().Get(0) if argUrl == "" { cli.ShowCommandHelp(c, "get") os.Exit(1) } // ... }
- 上述の通り、
Command.Actionはfunc(context *Context)という型なので、CommandGet関数もそれに従っている。 cli.Context.Args()はcli.Args型を返すが、これはtype Args []stringと定義されており、実体はstringのスライスだ。Args.Get(n int)はnがスライスのサイズより大きかった場合に空文字を返すようになっている。ShowCommandHelpはContextポインタとサブコマンドを表す文字列を渡すことで、そのサブコマンドのヘルプメッセージを出力する。- 第1引数をURLとして取得し、それが空であればヘルプメッセージを表示するようになっている。
func CommandGet(c *cli.Context) { // ... u, err := ParseGithubURL(argUrl) if err != nil { log.Fatalf("While parsing URL: %s", err) } path := pathForRepository(u) if err != nil { log.Fatalf("Could not obtain path for repository %s: %s", u, err) } // ... }
ParseGithubURL()とpathForRepository()いう関数についてはあとで見ていくことにする。- エラーがあった場合、
log.Fatalf関数でエラーメッセージを表示するものと思われる。logパッケージはGoの標準パッケージで、log.Fatalf関数はエラーメッセージを表示するだけでなくexit(1)によってプログラムを異常終了させる。
func CommandGet(c *cli.Context) { // ... newPath := false _, err := os.Stat(path) if err != nil { if os.IsNotExist(err) { newPath = true err = nil } mustBeOkay(err) } // ... }
os.Stat関数はファイルの存在をチェックする際によく用いられるようだ。この関数は指定したパスにあるファイルの情報を表すFileInfo構造体と、エラー時にはエラーを返す。os.IsNotExist()関数も存在チェックを行うように見える。ファイルの存在をチェックするGoの実装は諸説あるようだ。mustBeOkay()関数は定義を見てみると、引数の渡したエラーが存在すればエラーメッセージを表示して異常終了させるようだ。アサーションのような役割を果たしているようだ。- ファイルパスが存在しない場合は
newPathがtrueになる。また、errがnilになるため、mustBeOkay()で異常終了は起きなくなる。
func CommandGet(c *cli.Context) { // ... if newPath { dir, _ := filepath.Split(path) mustBeOkay(os.MkdirAll(dir, 0755)) Git("clone", u.String(), path) } else { mustBeOkay(os.Chdir(path)) Git("remote", "update") } }
filepath.Split()は与えられたパスをディレクトリとファイル名に分け、ディレクトリ、ファイル名の順に返す。Git()関数はあとで詳しく見る。newPathがtrueになるのは上述の通りpathが存在しない場合で、このときはgit cloneが行われ、そうでなければgit remote updateが行われるようだ。
ghq getコマンドの全体像についておおまかに理解できたので、飛ばした関数について1つずつ読んでいく。
type GitHubURL struct { *url.URL User string Repo string } func ParseGitHubURL(urlString string) (*GitHubURL, error) { u, err := url.Parse(urlString) if err != nil { return nil, err } if !u.IsAbs() { u.Scheme = "https" u.Host = "github.com" if u.Path[0] != '/' { u.Path = '/' + u.Path } } if u.Host != "github.com" { return nil, fmt.Errorf("URL is not of github.com: %s", u) } components := strings.Split(u.Path, "/") if len(components) < 3 { return nil, fmt.Errorf("URL does not contain user and repo: %s %v", u, components) } user, repo := components[1], components[2] return &GitHubURL{u, user, repo}, nil }
url.Parse()は与えられた文字列をパースしてURL構造体のポインタと失敗した場合はerrorを返す。URL構造体はSchemeやHostといったフィールドを持っているため、相対パスであればこれらを設定している。fmt.Errorf()はフォーマット化された文字列からエラー値を返す。strings.Split()は文字列を第2引数で渡されたセパレータで分解しstringのスライスとして返す。
続いてpathForRepository()関数を読んでいく。
func reposRoot() string { reposRoot, err := GitConfig("ghq.root") mustBeOkay(err) if reposRoot == "" { usr, err := user.Current() mustBeOkay(err) reposRoot = path.Join(usr.HomeDir, ".ghq", "repos") } return reposRoot } func pathForRepository(u *GitHubURL) string { return path.Join(reposRoot(), "@"+u.User, u.Repo) }
path.Joinはパスの要素を/で結合してパスにする。GitConfig()は後ほど読んでいく。おそらくリポジトリのルートパスを返すものと思われる。reposRootが空であれば$HOME/.ghq/reposを返すようになっている。user.Current()はカレントユーザーを表すUser構造体のポインタを返す。User構造体はユーザー名やホームディレクトリなどの情報を持っている。usr.HomeDirでホームディレクトリを取得している。
続いてGit()関数を読んでいく。
func Git(command ...string) { log.Printf("Running 'git %s'\n", strings.Join(command, " ")) cmd := exec.Command("git", command...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr err := cmd.Run() if err != nil { log.Fatalf("git %s: %s", strings.Join(command, " "), err) } }
...stringのように引数の型名の前に...をつけると可変長引数をとることができる。この引数の型は型名で指定した型のスライスとなる。つまりここではstringのスライスとなる。fmt.Printf()関数は標準出力に出力するものだが、log.Printfはロガーで指定された出力先に出力する点が異なる。exec.Command()関数は、第1引数で指定された名前のコマンドを渡された可変長引数で実行するコマンドを表すCmd構造体のポインタを返す。...で渡された可変長引数は上述の通りスライスなのだけど、スライスを展開して可変長引数として関数に渡す場合はcommand...のようにスライスのあとに...とつける。cmd.Runで指定されたコマンドを実行する。
続いてGitConfig()関数を読んでいく。
func GitConfig(key string) (string, error) { defaultValue := "" cmd := exec.Command("git", "config", "--path", "--null", "--get", key) cmd.Stderr = os.Stderr buf, err := cmd.Output() if exitError, ok := err.(*exec.ExitError); ok { if waitStatus, ok := exitError.Sys().(syscall.WaitStatus); ok { if waitStatus.ExitStatus() == 1 { return defaultValue, nil } else { return "", err } } else { return "", err } } return strings.TrimRight(string(buf), "\000"), nil }
Railsにコントリビュートした
軽い気持ちでpull requestを送ってみたら数十分後になんとmergeされてしまった。間違ってmasterブランチに送ってしまったため、おそらくrails 5で公開されることになる。
追加したのはrake initializerという簡単なrakeタスクで、railsの起動時に実行されるinitializerを実行順に出力する。
% rake initializer set_load_path set_load_path set_load_path set_load_path set_autoload_paths set_autoload_paths set_autoload_paths set_autoload_paths add_routing_paths add_routing_paths add_routing_paths add_routing_paths add_locales add_locales add_locales add_locales add_view_paths add_view_paths add_view_paths add_view_paths load_environment_config load_environment_config load_environment_config load_environment_config load_environment_hook load_active_support set_eager_load initialize_logger initialize_cache initialize_dependency_mechanism bootstrap_hook active_support.deprecation_behavior prepend_helpers_path prepend_helpers_path prepend_helpers_path prepend_helpers_path load_config_initializers load_config_initializers load_config_initializers load_config_initializers active_support.halt_callback_chains_on_return_false active_support.initialize_time_zone active_support.initialize_beginning_of_week active_support.set_configs action_dispatch.configure active_model.secure_password action_view.embed_authenticity_token_in_remote_forms action_view.logger action_view.set_configs action_view.caching action_view.collection_caching action_view.setup_action_pack action_controller.assets_config action_controller.set_helpers_path action_controller.parameters_config action_controller.set_configs action_controller.compile_config_methods active_record.initialize_timezone active_record.logger active_record.migration_error active_record.check_schema_cache_dump active_record.set_configs brancher.rename_database active_record.initialize_database active_record.log_runtime active_record.set_reloader_hooks active_record.add_watchable_files global_id active_job.logger active_job.set_configs action_mailer.logger action_mailer.set_configs action_mailer.compile_config_methods setup_sass setup_compression jbuilder web_console.initialize web_console.insert_middleware web_console.templates_path web_console.whitelisted_ips web_console.whiny_requests web_console.acceptable_content_types engines_blank_point engines_blank_point engines_blank_point engines_blank_point append_assets_path append_assets_path append_assets_path turbolinks append_assets_path add_generator_templates ensure_autoload_once_paths_as_subset add_builtin_route build_middleware_stack define_main_app_helper add_to_prepare_blocks run_prepare_callbacks eager_load! finisher_hook set_routes_reloader_hook set_clear_dependencies_hook disable_dependency_loading
もともとは、naoty/brancherというrubygemを作るときに、railsの初期化プロセスにコードを差し込みたくてinitializerが実行される順番を確認するデバッグ用のコードを書いていたのがきっかけだった。似たようなrakeタスクにrake middlewareというものがあり、これがあるならrake initializerもあっていいだろうという軽い気持ちでpull requestを送ってみた。
意外とすんなりmergeしてもらったので、railsへのコントリビュートに対して心理的なハードルがかなり低くなった。上の出力を見てもらえればわかるとおり同じようなinitializerが実行されており、起動が遅くなったり、initializerで定数を定義したときに大量のwarningが出たりする。前々から定数のwarningが大量に出る問題は不思議に思っていたが、これが原因なのかもしれない。またコントリビュートするとしたら、ここらへんを解決するところになりそう。