Haskell書いてるときになんとなく気をつけていること
信頼性はない
都市伝説かもしれん
大体枕詞に特別な理由がなければがつく
思いついたら徐々に更新する
それは違うよってことがあったら @Lugendre まで
ghc 8.6.5の話(8.8.1は仕事で使ってなくてな......)
コンパイル通るかの確認だけなら最適化オプションO0でコンパイルする
stackでいうとstack build --fast
動かすときはO2ビルドしたほうがいいが,インライン展開に5億年消費する
なんならこのくらいは静的解析に任せてもいいが,でかいプロジェクトだと一定時間動いてハングアップするのしか世の中にないのでオワリ
VSCode のせい説もある
ghcideに期待
haskell-language-serverがすべてを解決した
CIするときは必ずキャッシュする
毎回一から依存関係やGHCのビルドが走って余裕で1時間超えるようになるので
データ構造のフィールドは特に理由がない限り正格にする
意図しないサンクを潰すため
単相の場合なら最適化で自動でunpackしてくれる
UNPACKプラグマは自前で書いたほうがいいという話もある
データ型がUnboxにできるなら極力Unboxにする
関数は特に理由がない限り多相に書く
後からこれに救われることが多い
多相に書いても大体の場合最適化でコストゼロ
特に理由のない限りmtl-styleに適合するように書いておく
具体的な型に言及せずに型クラス制約を持つ型変数にたいして言及するようにかけると良い
当たり前
必要がないならデータ構造は多相にしない
必要があるかないかの判断が難しいみたいなのはある
データ構造は理由がない限りはLazyではなくStrictを使う
タプルにデータを入れるときはWHNF(NF)まで評価してから入れる
タプルは遅延データ構造なので
速度に関する関心如何ではタプルの代わりに正格だったりUnboxedなデータ構造を作って使ったほうがいい場合もある
再帰やfoldなどを使う時,アキュームレータはWHNFまで評価する
場合によってはスペースリークするので
foldはO2ならdemand analysisやcall arityとかでうまいことやってくれると思われる
可読性を著しく損なわない限り,ポイントフリーに書く
最適化の効きが違う
ラムダ抽象なども極力排除したいところ
再帰は自前で書かない
高階関数でも融合変換+stream fusionで自前で書いたのと同等以上のものができることも多い
stream fusion効かないと思ったときや無理やり高階関数の組み合わせで書いた結果著しく可読性が損なわれたときにに初めて書く
beautiful foldingとかがはまるときもある
本当に速くなるかは五分なのでちゃんとベンチとって確認しよう
リストではなくVectorを使う
遅延リストはメモリバカ食いする上遅い
極力,unboxed vectorを使う
作ってどこからも参照せず即畳むようなのもstream fusionでリストが作られないので許される
多相性を犠牲にして速度を出したい場合,VectorではなくByteArray(あるいはByteString)を扱うことも考えるべき
vectorを使う際はstream fusionを気にする
stream fusionが効かない場合Mutable Vectorを使うことも考える
どっちが速いかはベンチで確認しよう
replicateMは遅い
vectorをメモリに保持する可能性がある場合にMaybeを保持するのは避ける
stream fusionで消える場合は問題ないが,vectorを作ってしまう場合に中身がMaybeであると効率的に不安
MonadをPrimMonadのインスタンスにできる場合はしておく
vectorのhogeMがはやくなったりする
可変参照は基本使わない
遅い
ReaderT env IOパターンを用いる場合,可変参照を用いてもパフォーマンスが劣化することが少なく,見通しもよくなる(実際にReaderT env IOパターンの紹介記事では,mutableなグローバルコンテキストはIORefで持つことを提案している)
純粋に状態を扱ったほうがいろいろな最適化も効き,速い
真にmutableな状態が欲しい場合はMutable Vector使ったほうがマシ
遅延評価を信じない
遅延評価だしここはいい感じに......というのは裏切られることが多い
コストが高く総合的に損という場合も少なくない
基本的にGHC(Haskell)では強力な最適化や遅延評価,融合変換などが組み合わさってパフォーマンスの見積もりがとても難しい.GHCマスターでもない限りあたりはつけられど,ベンチしか信用できない.
最適化前後のCoreを読んで期待通りに最適化されているか見るという手もあるが,実用的なライブラリの場合Coreを読むのは大変
最適化がうまく言ってるかどうかくらいならなんとか読める範囲であるので,GHCを読んで沼に突っ込んでもいい
一旦正格に書いて,遅延評価で効率良くなると期待できる場所はベンチで確信を持ってから書き換えよう
StrictData拡張,Strict拡張でだいぶ気にすることが減るらしいが,僕は使ってないので挙動がわからんため解説できず
ライブラリはこれらの拡張を使ったないことが大半なので,沼にハマってしまうことも稀にある
部分関数を使わない
Prelude.head: empty listじゃねぇんじゃ!!!!
部分関数のエラーはマジでなにもわからないので勘弁
Maybeを返す関数を使おう
Preludeに関してはalt preludeも検討すべき
せめてfromMaybe (error "...")でわかりやすいエラーメッセージを出す努力をしよう
これもどうかと思うがまだマシ
依存ライブラリの依存ライブラリのエラーで部分関数を使っていてエラーが出た場合は泣こう
スタックトレース取るしかない
Stringを使わない
StringはCharのリストなので当然効率が悪い
CharはUnicodeのコードポイント
ByteStringかTextを使うべき
マルチバイト文字列の操作が多い場合,Textのほうが便利
ASCIIの範囲内であればByteStringで十分か
結合する処理が多い場合,fast-builderのBuilderでもって最後に一括で結合して出力するのが無難
ByteStringのBuilderは遅いので使わない
型にできる条件は極力型にしてコンパイルチェックできるようにする
不用意な実装をかなり抑制できる
QuantifiedConstraintsとかも使っていきたいが,まだ動作が不安定
重い型ハックしたときのコンパイル時間への影響はインライン展開に比べれば実質ゼロみたいなもの
やりすぎると可読性が死ぬほど悪くなっていくのでほどほどに
型クラスの型変数には種注釈をつける
型変数は基本kindはTypeに推論される
DataKindsなどと併用していると,インスタンス解決ではまる(インスタンス解決にはkindの情報も使われるため)(1敗)
型変数のkindが決まりきってない場合は,PolyKindsを使って,多相なインスタンスの定義をする際にその型変数に種注釈をつけるほうがよいかも
インスタンスのメソッドは基本INLINEプラグマをつける
unsafePerformIOや再帰関数などはNOINILINEをつける
INLINEプラグマをつけた際phase制御無しだとエラーが発生する場合,phase制御してINLINE展開してもパフォーマンスが下がることが多い
SPECIALIZEプラグマは積極的に利用する(コードサイズと相談)
orphan instanceは極力避ける
意図せずinstace宣言が重複することも
まぁ何となにが重複してるかはコンパイルエラーででてくるから気にしなくてもいいという派閥もある要出典 ライブラリの場合,ほかへの影響も考えてorphan instancesの定義や,定義を行ってるモジュールのインポートは避けるべき
newtypeしてinstance宣言によって型を変えるという方法が一般に用いられている気がする
とんちか?
ライブラリの場合,依存を小さくするためにhoge-orphansというライブラリを作って依存を分割する場合がある
メソッドへの型適用を行う
意図しないインスタンスのメソッドが使われるのを防ぐ(あんまない),型エラーが詳しくなる,曖昧性のエラーを未然に防げるなどの効果が見込める
モジュールは必要最低限のものをexportするよう心がける
抽象型にするのも大事
ライブラリのユーザがライブラリ内部をいじりたいことも多いので,Internalとそれをimportした表層のモジュールに分けて,表層のほうでexportのコントロールをし,Internalは全部exportするみたいな運用が良さげ
関数をインポートする際,必ずqualifiedする
特にメソッドの場合,インスタンス定義の際に意図せず再帰的な定義になってしまうことがある(1敗)
where節を使う際,変数のスコープには気をつける
letと違いwhereはスコープが広いので,意図しない再帰を誘発する場合がある(1敗)
coerceに置き換えられる関数は積極的にcoerceにする
ゼロコストなので
単純なnewtypeの値コンストラクタの使用は,castに置き換えられる
map Hoge xsはmap coerce xs とかにはならない
coerceには型適用で引数,返り値の型を指定する
coerceは範囲が広すぎるので,意図しない型に変換してしまう可能性がある(2敗)
頻出する場合,専用のcoerceをラップした関数を用意するのも良い
自前でShowのインスタンスを定義する際はshowParenなどと関数合成を駆使してshowsPrecを実装する
Stringをアペンドなんかしてしまった日には効率が終わる
関数合成で各showを組み合わせられるように各関数が定義されている
くわしくはHaskell Language Reportにのっとる
存在型は基本的には避けたい
存在型の中に型クラス制約が入っていると多相関数使う際の辞書渡しのコストがでかく効率が悪い
最適化でどうにかなるのかもしれないがここに関してはよくわからず(有識者求む)
割と致し方ない場合も多いので,配列とかで持たない,型クラス制約は少なくするなどで対応する
monad transformerは3つまでみたいな感覚要出典 自前で特化したMonadを作るのも良さげ
THよりかはGenerics
保守しやすい
Genericで自動導出できるようにできるなら必ずする
やらない理由がない
バイナリ出力はcerealを使う
binaryは糞遅い
wineryも速くて良いが,Typeableが地味につらいかも
多相な関数で使うとTypeableの制約が必要になる
その手間の分速くはなるし,スキーマも便利
OverloadedStrings,OverloadedListsなどを積極的に使う
コンパイル時に変換されるので効率が良い
hspecよりもtastyを使う
tastyに置き換えたら10倍はやくなったみたいなことが(僕の書き方の問題か?)
hspec越しにHUnitを使うよりもtasty-hunitを使うほうが速いという話っぽい要検証 cabalなどでも使われ信頼性もあるので安心
ライブラリをあまり信用しすぎない
有名ライブラリでも微妙過ぎる実装,微妙過ぎるAPIなことがある
妙に依存が多いみたいなこともある
ライブラリの実装と依存を見て,納得できなかったら車輪の再発明も視野に入れるのもあり
THの関数やspliceはモジュールの最後に使う
使った時点でスコープがぶった切られる(1敗)
速度改善にFFIは最後の手段
FFIで速くなるとは限らない
当然だが,GHCの最適化や融合変換の恩恵がうけられない
stream fusionなどの恩恵を受け入れられないため,巨大なVectorなどをわたす場合どうしても一回Vectorを構築する必要がある
(Safeな)FFIを呼び出す操作は意外とコストが重い
別threadからの呼び出しや呼び出し元のHaskellコードへの参照などに対応するため,新しいOS threadを作ってそこにコピーする動作が発生する
+RTS -Aでnurseryのサイズを変えると速くなる
Haskellの遅さの一因は遅延評価による空間効率の悪さとGCのstop the worldのコンビネーション攻撃
nurseryがあふれなければGCは走らない(はず)
これで劇的に速度が改善される場合はデータの持ち方などが悪く,不要にGCが走っているので改善すべき
型レベル自然数の自明なequality constraintを勝手に挿入してほしい
4 + 3 ~ 7みたいなやつ
ghc-typelits-natnormaliseなど
自前の型レベル関数などで自明な制約が出てきてしまう場合はライブラリとして公開するならこういったコンパイラプラグインを公開することを検討すべき
Coreをある程度理解していればGHC API越しに辞書を書き換えれば良い
Coreの理解が求められるためハードルは高い
deriving時には必ずStrategyを指定する
これをしないと一回は必ずnewtypeとanyclassで衝突して死ぬ(1敗)
anyclassは使わないでinstance Hoge Intなどを一生懸命書くというのもあり
Data.List.nubは使わない
そもそもリストを使うなと言ったので使わないと思うが......
SetとかVectorならソートした後にuniqとかでも十分だったり
各プロジェクトのビルド時間が短くなるように依存を整理する
前述の通りHaskell(GHC)はO2ビルドをすると死ぬほど時間がかかるので,大規模なプロジェクトを作る時うまくリビルドが走らないように設計に配慮する必要がある
デバッグ速度に直結するのでとても大事
undefinedは折を見て消しておく
最悪errorにしておく
書き換えが発生したりネストしてたりするレコードはLensを定義しておいて,レコードアクセッサは公開しない
Lensを作っておくと書き換えやネストしたレコードへのアクセスが楽
Envとかはとくにこうしておくべき
お役立ちサイト
公式以外の場所でお役たち情報が載ってるサイト
GHC拡張に関してはユースケースも含めて公式ドキュメントが詳しい
逆引きてきな使い方ができる
パッケージティアリストは絶対見て
パッケージの細かい使い方や,stack, cabalなどの使い方も豊富載ってる
ベストプラクティス的なものが多く載ってるHaskellerの総本山のうちの一つ
HPからのHaskell情報の探索がつらすぎるという難点がある
トピック的なことはだいたい載ってる魔法のサイト
HPからの情報の探索が(ry