仕事で使うHaskell
TL;DR
Haskellはいいぞ。ただ仕事で使うならビルド、デプロイ周辺は工夫する必要が色々出てくるぞ。
あ、nixもいいぞ。
はじめに
Haskellを用いている会社HERPに転職してからそろそろ1年が経つので久しぶりに記事でも書いてみます。そういえばzennでは初投稿ですね。
最近はHERPでHaskellを書きつつシステム基盤整備みたいなことをやっています。あとマネージメントみたいなこともやってたりします。
僕の書いたHaskell microserviceは既に稼働して売り上げに貢献しています。
あ、HERPはHaskell FoundationのFunctorスポンサーになっています。スポンサーの名前が面白いですよね。
Haskellを仕事で使う感想
最高ですね。簡単便利十分速い保守楽拡張楽、という感じです。
並行プログラミングツールとしてstmが提供されているのがお気に入りです。これによってデッドロックがない世界で安全に並行プログラミングが出来ます(ライブロックは起こり得ます)。好きなモナドはSTMモナドです。
Haskellとlibrary
Haskellはlibraryの数は多いのですが質を考慮するとどうなんでしょうね。大きな問題があればとりあえずforkして自前で直す気合いみたいなものが必要になったりならなかったりするかもしれません。
libraryの中は良く読みます。このclientちゃんとthreadsafeかな、とか細かい挙動が知りたいくて読むことが多い気がします。hackageやstackageのhaddockはコードも読めて便利ですね。手元にgit cloneして読むことも良くあります。
最近yesod触ったのですが、template-haskellでの生成コードを出力して読めば(あとyesodのlibraryも必要に応じて読めば)普通に分かるということに気づきました。一般にコード中にtemplate-haskellが挟まると適当な単語でgrepしても何もhitしないことがあってすごいイライラするんですよね。コード生成して読めば良いだけでした。学生の頃yesodにすごい苦労した思い出が有るんですが...まあいいか。
HaskellとDocker
HERPでは kubernetes に docker コンテナをデプロイしているのですが、haskell projectでナイーブにDockerfileを書くと無圧縮で4GBくらいのサイズのimageが出来上がります。流石にそのままだと大きすぎてインフラに負荷が掛かるので小さくしました。
方法は単純に Dockerfile の multi-stage build で必要なものだけ最終stageに持ってくる、というやつです。これによって60MBくらいまで小さくなりました。なんかスマートな方法が他にある様な気がしないでもない。
HaskellとRPC
grpc+protobufを導入することにしました。前職ではScalaを使っており、rpcとしてはthriftを用いていた(finagleとその周辺を用いていた)のですが、haskellのthrift libraryはあまり良いものがないように見えたため、grpcを導入することにしました(その後Simon MarlowがFacebookでThriftライブラリを公開したため、今ならそちらも検討してもいいかもしれません)。
haskellのgrpc libraryには複数ありますが、gRPC-haskellを選択しました。これは公式grpcのwrapperとして実装されているためです。公式grpcライブラリが使われていて、Interfaceも悪くなさそう、ビジネスでの使用実績もある、コードも読んで問題があってもある程度は自前で解決出来そう、ということで決定しました。
ここで一つ問題があって、このライブラリはほぼ nix build しかサポートしていない状態だったのです。なので導入はそこそこ頑張る必要があります(が、頑張ると出来ます)。
Haskellとbuild
Haskellのビルドは遅いです。これはまあ間違いないんですが、普段の開発では僕はそこまで気になっていません。開発スタイルはrepl(ghci)を立ち上げておいて、コードを修正したらreplで :reload
(:r
) する、という単純なものです。
前職ではScalaを用いていましたが、こちらもビルドが遅いと評判の言語です。ただ同じくreplを用いた開発で、僕としてはそんなにストレスはありませんでした。
haskellやscalaでビルドが遅い遅いと文句を言う人を見るたびにみんな開発手法間違っているのでは?と思っているくらいです。
まあHaskellの場合、localにcacheがない初回ビルドは本当にすごい長いのですが。その辺はnixのところで話をしましょう。
Haskellとnixpkgs
nixの話をしましょう。NixOSではなくnixpkgsです。HERPはnixを導入しています。HERP入社した当初「nix使う会社て本当に存在するんだな、nixとか何も分からねー」とか思いながら軽く論文やUser Guideを読んだんですが、僕は割とすぐにnixという技術が好きになりました。
nixは再現性あるbuildを提供します。また、再現性のある環境を提供します。また、その環境はrollbackが可能です。
実は10年とかもっと前にnixの話を少し聞いたことがあります。その時「環境がrollback可能」と聞いてまず考えたことは、「むしろ何故aptやyum等のpackage managerはrollback可能になってないの??」ということでした。一体みんなどうやって仕事してるんだ...といった気分になったことを覚えています。まあ時は流れて現在、コンテナ技術が流行っているのでむしろ環境は一から作り直す方が主流でしょうか。
nixの仕組みは原理自体は割と単純で、あるpackageが同じかどうかをhashで判断します。そのhashはビルドするための依存packagesやパラメータやフラグ等から生成されるため、それらが少しでも異なると別のpackageと判断される、と言うわけです。また、ロールバック可能性についても単純で、基本的に全てオブジェクトがimmutableであるためです。多分gitにおいて内部オブジェクトが全てimmutableである(だから任意の時点へ戻ることが簡単)ことと同じ様なもの、と僕は認識してます。そしてgitに git gc
が有る様に、nixにもgc操作が提供されています(nix-store --gc
)。
さてこのnixの単純な仕組みから色んな機能が提供出来ることがわかります。例えば開発環境やCIでのbuild binary cacheです。誰かが一度buildしたものをbiuld cacheとして保持できたらそれを他の人も使えるわけです(もちろん環境やフラグ等条件が完全に合えば、ですが)。nixpkgsはbuild cache先としてS3をサポートしているので、少し整備すれば快適なビルド環境が出来るかもしれません。また、最近はnix cache serverを提供しているサービスもある(Cachix: https://www.cachix.org/)ため、そちらを使うのが楽かもしれません。
nixを調べて驚いたのはnixのclosureという概念です。nixはあるpackageに対してその依存関係を全て知っているため、executableからshared libraryも含めて関係するものを纏めて一つのファイルに固めることができます(closureという名前の由来恐らくはexecutable + environmentからでしょうか)もちろんそのファイルはネットワーク越しにやりとりが出来て、送信先での展開も簡単に出来ます。nix-copy-closureの例ではFirefoxをremote machineに送る例が載っています。そんなことあるか?と思いつつもインパクトのある面白い例ですね。
型検査とbuild時間とnix
さてHaskellだけの問題でもないのですが、今後も様々なチェックをコンパイル時に型検査したい人間としては今後の(数年、数十年のスパンで)ビルド時間は増大していく一方で、CPUのトランジスタ集積密度は限界を迎えつつありフリーランチの時代は終わろうとしています(いや何も知らないんですがムーアの法則ってまだ生きてるんですかね)。
そんな時代に対してnixは悪くない武器なのではないかなあ、とぼんやり考えていたりします。どうですかnix始めませんか。
nixとその使用
nixのコンセプトを好きになったからと言っても、その使用が簡単だとは言ってません。
最近nix2.4がリリースされてエラーメッセージが改善されたよ、などと有りますが、nixのエラーメッセージは不親切でたまに何が起こったかわからないことがあります。nixコマンドのソースコードを読んでいって何故エラーが出たのか探ることもありました(ちなみにその際のエラーはパラメータ指定不足で起こるものでした)。toolとしてまだまだ途上感があります。
僕も違和感なく利用するには程遠い段階です。一緒に頑張りましょう。
nixのキャッシュコントロール周りについて詳しくなりたい。
GHCの最近の動き所感
ghc-9.2が最近リリースされて非常にとても本当に!!めでたいのですが、haskellがどちらへ向かっているか外部の人はわかりにくい様です。まあそれはそう。
私的に、GHCの最近の動きは実用性を高める方向と型システムを統合、拡張する方向かなあと思ってます。僕の興味を抜き出しただけかもしれません。release note見返してたんですが割と量が多かったので途中で諦めました。8.0は昔すぎた(5年前)。
- industories 向けに実用性を高める
-
Strict
(8.0)- モジュール単位で遅延評価、先行評価を切り替えられる
- Haskell の初心者が最もとっつきにくい箇所第一位(多分)は既に調整可能になってる
- Callstack error messages (8.0)
- 親切なエラーメッセージは重要
- User-defined type error messages (8.0)
- 親切なエラーメッセージは重要
- Colourful error messages (8.2)
- 親切なエラーメッセージは重要
- Record Dot Syntax (9.2)
-
record.field
みたいな syntax が使える。導入まで長かった.. - 個人的に Haskell の最も使いにくい部分が直った
- これによって Haskell が普通の言語になったと言っても過言では無いな!!
-
- Low latency GC (8.10)
- major GC が concurrent mark & sweep になって stop-the-world が非常に短くなる
- web server に向いてる
- Compact Region (8.2)
- 大きいheapのGCでの効率的な扱い
- text internal を utf16 -> utf8 へ (HaskellFoundation)
- libraryを整備しようという Haskell Foundation の動き
- heap profiling mode "-hi" (9.2)
-
profile build が必要なく 詳しくmemory leakが調べられるとか
- app profileのためにlibraryも全部profile buildが必要?マジで?正気?みたいな状況が改善されたかも
- 簡易版"-hT"とかあったけど
-
profile build が必要なく 詳しくmemory leakが調べられるとか
- DWARF support
- 未調査..今はちゃんと動くのだろうか?
-
- 型システムのadhoc部分を統合し、拡張する
- TypeApplications (8.0)
- 型引数に型を明示的に渡す
- これまだ5年しか経ってなかったのか、もっと昔からある気分
- Unlifted types(⊥が無いstrictな型)の扱いを統一、一般化
- Levity Polymorphism (8.0)
- lifted/unlifted の抽象化
- UnliftedNewtypes (9.0)
- Unlifted types の newtype が定義できる
- UnliftedDataTypes (9.2)
- Unlift type をユーザーが定義出来る
- Levity Polymorphism (8.0)
- kind 周辺の整備、一般化
- StandaloneKindSignatures (8.10)
- そこそこ破壊的な変更が入ってる気がする
- kind Char導入 (9.2)
- ImpredicativeTypes 導入 (9.2)
- 型引数に
forall
付きの型を渡せたりする - 簡単な推論もできる様になった(QuickLook)
- 型引数に
- LinearTypes 導入 (9.0)
- 線形型、これも長かった..
- リソースの安全な管理
- DependentTypes へ向けた長い道のり (これから)
- Proposals では少しずつ話が進んでいるproposal:dependent-type-design
- 実装も少しずつ進んでる
- TypeInType (8.0)
- Visible dependent quantification (8.10)
- 他にも色々あるらしいけど略
- TypeApplications (8.0)
そんなわけで
楽しくHaskell書いてます。
最近アウトプット少なくてすまんな、という気持ちはあります。
Discussion