Haskell - 個人的な(そして異端的な)コーディングスタイル
はじめに
書くときの効率、美しさ、読みやすさを考えながら、コードを書いています。その結果として、実を結んだ、個人的なコーディングスタイルを紹介します。
この文書について
この文書は拙書「Haskell - 教養としての関数型プログラミング」の「補足資料D 個人的なコーディングスタイル」の内容を公開したものです。
どこが異端的か
Haskell界では、OS間での可搬性を重視してか、「タブを使わないようにしよう」という「紳士協定」がある。それに対して、Linux文化に長いことひたっている著者は、すくなくとも、「タブを使うかどうか、自分で決めさせてくれ」というスタンスをとる。
それ以外については、まあまあ、妥当で穏当なことを書いた。
字下げ
8文字タブを使います。これについては、Linuxカーネルの創始者である、リーナスの言葉を引用しておきます。彼は、つぎのように、言います。
Tabs are 8 characters, and thus indentations are also 8 characters. There are heretic movements that try to make indentations 4 (or even 2!) characters deep, and that is akin to trying to define the value of PI to be 3.
タブは8文字なので、字下げも8文字だ。字下げを4文字に(さらには、2文字に!)しようという、異端者たちも、いるようだけど、これは、円周率を3にするようなものだ。
Rationale: The whole idea behind indentation is to clearly define where a block of control starts and ends. Especially when you've been looking at your screen for 20 straight hours. you'll find it a lot easier to see how the indentation works if you have large indentations.
論理的根拠 - そもそも、字下げってのは、ブロックのはじまりとおわりとを、はっきりさせるためのものだ。とくに、20時間ぶっつづけで、モニタを見つめたあとなら、大きな字下げが役に立つことが、よくわかるだろう。
Now, some people will claim that having 8-character indentation makes the code move too far to the right, and makes it hard on a 80-character terminal screen. The answer to that is that if you need more than 3 levels of indentation, you're screwed anyway, and should fix your program.
ここで、「8文字の字下げだと、コードが右によりすぎて、80文字の画面で読みにくくなる」と、文句を言う人がいるかもしれない。答えはこうだ。「もし、3段階よりも深い字下げが必要だとしたら、どっちにしたって、コードはぐちゃぐちゃになってるんだから、コードを直すべきだ」。
In short, 8-char indents make things easier to read, and have the added benefit of warning you whern you're nesting your functions too deep. Heed that warning.
つまり、8文字字下げはコードを読みやすくして、さらに、関数を深くネストしすぎたときの、警告にもなるってことだ。警告には、耳をかたむけよう。
これで、言いつくされています。GHCでは、宗教戦争を避けるために、「タブ文字を使わないようにする」という方向での、解決を図っているようです。
4文字字下げ派に気をつかった、公平な解決というつもりなのでしょうが、そもそも、タブを本当に必要としているのは、8文字字下げ派です。4文字字下げ派にとっては、タブをスペースに置き換えることに、大きな問題はないはずです。しかし、8文字字下げ派にとっては、大きな問題です。タブを1回打ちこむ代わりに、スペースを8回打ちこまされることを、考えてみましょう。エディタの設定を変えればいいと言うかもしれません。それでは、字下げを浅くするときのことを、考えてください。「タブ」は8文字字下げ派のために、とっておくべきです。
いまのところは、つぎのオプションで、なんとかしておきましょう。
-fno-warn-tabs
スペース
演算子の前後
演算子の前後には、スペースを置きます。これは、とくに、Haskellでは必要です。Haskellでは、つぎの表記は、「変数valueに修飾名Someがついたもの」です。
Some.value
それにたいして、つぎの式は、「値構築子Someと関数valueとが、関数合成されたもの」と、解釈されます。
Some . value
「演算子(.)では、前後のスペースが必須である場合がある」と、いうことです。これとの統一のために、演算子の前後に、スペースを置くべきでしょう。
演算子の部分適用
演算子の部分適用のときにも、おなじように、演算子と値とのあいだにスペースを置いて、つぎのようにします。
(3 +)
(/ 8)
カンマのあと
カンマのまえには、スペースを置きません。カンマのあとには、スペースを置きます。これについては、英文タイプのルールにしたがうということです。
["Hello", "Haskell", "Welcome", "to", "Haskell"]
ただし、例外があります。この本では解説しませんでしたが、言語拡張TupleSectionsを使うと、タプルの部分適用ができます。タプルの部分適用のときには、つぎのようにします。
(3 ,)
(, 4)
演算子の部分適用との、統一感を出すために、カンマのまえにスペースを置きます。
括弧のまえ
括弧のまえには、スペースを置きます。ほかの言語では、「関数適用」を意味するために、つぎのように、関数と括弧とを、くっつけます。
f(x)
しかし、Haskellでは、関数適用に括弧は必要なく、つぎのように書けます。
f x
括弧をつけるのは、結合のしかたを変えるためです。つぎの例を見てみましょう。
f (g x)
このように、「値gに、関数fを適用する」のではなく、「値xに、関数gを適用する」ということを、示すためです。よって、括弧のまえに、スペースがあるほうが、論理的な統一感があります。
括弧のなか
基本的に、開き括弧のあとや、閉じ括弧のまえには、スペースを入れません。たとえば、つぎのようにはしません。
( 3, 4 )
つぎのようにします。
(3, 4)
ただし、例外がみっつあります。
レコード構文
レコード構文については、開き括弧のあとと、閉じ括弧のまえに、スペースを入れます。理由は、とくにありません。個人的な感覚ですが、つぎの、ふたつを比較すると、うえのかたちのほうが、きれいな感じがします。
Some { slot1 = value1, slot2 = value2, slot3 = value3 }
Some {slot1 = value1, slot2 = value2, slot3 = value3}
したのかたちだと、すこし窮屈な感じがします。
内包表記
リスト内包表記などでも、開き括弧のあとと、閉じ括弧のまえに、スペースを入れます。つぎの、ふたつを見てみましょう。
[ f x y | x <- [1, 2, 3], y <- [4, 5, 6] ]
[f x y | x <- [1, 2, 3], y <- [4, 5, 6]]
したのかたちだと、すこし窮屈な感じがします。
複数行に、またがる括弧
リストの要素が多くなったときなどに、つぎのようにします。
list1 = [
item1,
item2,
item3,
item4 ]
ここで、閉じ括弧の行を変えると、閉じ括弧だけの行ができてしまい、無駄な感じがします。かといって、スペースを入れずに、おわりの要素とくっつけると、「改行によって、はじめの要素と、大きくわけられている開き括弧」にたいして、バランスが悪くなります。よって、すこしでも、バランスをとるために、スペースでわけることにしています。
1行の幅
1行の幅は、80文字までとします。歴史的な理由ではありますが、まあまあ、妥当なところかと思います。さらに、本当ならば、あとの編集にそなえて、あらかじめ、75文字くらいで、改行しておいたほうがいいでしょう。
演算子と改行
通常の演算子
ひとつの式のなかで、改行が必要になったときには、通常の演算子については、演算子が行のおわりにくるようにします。
hello = "Hello, hello, hello, hello, hello, hello, ..., " ++
"world!"
関数適用や、それに類似した機能をもつ演算子
関数適用や、それに類似した機能をもつ演算子については、演算子が行のはじめにくるようにします。
foo = fun1
. fun2
. fun3
$ fun4 x
bar = monad1
>>= monad2
>>= monad3
>>= monad4
baz = fun
<$> app1
<*> app2
<*> app3
<*> app4
このような演算子では、「適用の連鎖」や「処理の流れ」を、あらわすことが多いので、このような書きかたのほうが、読みやすく感じます。
変数名
変数名の長さ
スコープのせまい変数には、短い名前をつけます。スコープのひろい変数には、長い名前をつけます。モジュールから、そとの世界に公開する変数には、できるだけ、4文字以上の名前をつけます。引数など、関数のなかでローカルな変数には、できるだけ、3文字以下の名前をつけます。モジュールのなかを、スコープとする変数には、長い名前と短い名前とを、場面に応じて使いわけます。
複数の単語からなる変数
「複数の単語から、できている変数」を表現するのに、スネークケースを使うか、キャメルケースを使うかも、長年のテーマです。スネークケースとは、つぎのような、書きかたです。
some_long_variable
キャメルケースは、つぎのように、なります。
someLongVariable
Haskellでは、おもにキャメルケースを、採用しています。しかし、スネークケースのほうが、美しく思える場面も、ときどき、あります。いまのところは、それぞれの場面によって、使いわけています。個人的には、「キャメルケースを基本として、イレギュラーなことをするときに、スネークケースを使う」という、使いわけをしています。たとえば、C言語の関数を呼ぶときに、つぎのような名前をつけるなどです。
c_someFunction
演算子($)の運続
演算子($)が、つぎのように、連続することがあります。
f $ g $ h $ ... y $ z a
その場合には、つぎのように、おわり以外を、演算子(.)に置き換えます。
f . g . h . ... y $ z a
見ための美しさと、意味論的にも「関数fからyまでを、関数合成したもの」を「値aに、関数zを適用したもの」に適用するというほうが、きれいなように思います。つぎのように、しないのは、演算子の数が増えて冗長になるからです。
f . g . h . ... y . z $ a
複数の関数をまとめて型宣言するとき
たとえば、つぎのように、関数について、まとめて型宣言することを考えます。
f, g, h :: Char -> Bool -> Integer
すべての関数が1行におさまれば、つぎのように、行間をあけずに定義します。
f, g, h :: Char -> Bool -> Integer
f c b = ...
g c b = ...
h c b = ...
ひとつでも、複数行にまたがる関数定義があれば、つぎのように、関数定義のあいだ、すべてに、1行ずつ空行を入れます。
f, g, h :: Char -> Bool -> Integer
f c b = ...
g c b = ...
...
h c b = ...
where節
where節のなかみが、1行で80文字におさまるときは、つぎのように、改行をせずに、予約語whereのあとに、そのまま、変数などの定義を続けます。
fun x = ...
where y = ...; z = ...
1行にすると80文字におさまらないときは、つぎのように、改行したうえで、続く、変数などの定義について、予約語whereとインデントをそろえます。
fun x = ...
where
y = ...
z = ...
let-in式
let-in式は、1行で80文字におさまるなら、そのまま、つぎのように続けます。
let x = ...; y = ... in exp
1行にすると80文字におさまらないときは、予約語letにタブを続けて、つぎのように、ローカル変数の定義と、予約語inに続く式の、インデントをそろえます。
let x = ...
y = ... in
exp
予約語letを含む行が、8文字以上あるときは、つぎのように、予約語letのあとで改行したうえで、インデントを一段、深くします。あとは、おなじように、インデントをそろえます。
some = let
x = ...
y = ... in
exp
ワイルドカードを使う/使わない
ワイルドカードを使う
変数を、あとで使わない
そのあとの式で使うことのない変数を、パターンマッチのところに置くのは、あまり、いいスタイルではありません。ワイルドカードを使いましょう。ただし、捨てられる値の、意味がわかるようにしておきたいときには、_ではじまる変数名を使います。つぎのようには、しません。
fun x y = y * 3
つぎの、どちらかの、書きかたをします。
fun _ y = y * 3
fun _x y = y * 3
do記法で
do記法で、モナドをかえす値が、ユニット値ではないにもかかわらず、値を捨てる場合があります。このときは、「わかってて、捨ててるんだよ」という気持ちを示すために、_ <-をつけます。たとえば、値m1からm4までの、モナドであるような文脈つきの値を、考えます。モナドm2は、ユニット値をかえします。それ以外は、ユニット値ではない値をかえすとします。モナドm3が、かえす値は使わないので、変数に束縛せずに、捨てるとします。このとき、つぎのようには、しません。
do x <- m1
m2
m3
m4
つぎのように、書きます。
do x <- m1
m2
_ <- m3
m4
ワイルドカードを使わない
単一の値構築子の代わりに、ワイルドカードを、使うことは可能です。つぎのようにできます。
fun (Just x) = ...
fun _ = ...
しかし、これは、よくないスタイルです。Maybe値であれば、値構築子が追加されることはありませんが、自作の型では、値構築子が追加されることがありえます。そのようなときに、うえのような表現があると、修正もれが起きます。単一の値構築子であれば、ワイルドカードを使わずに、ちゃんと書きましょう。
fun (Just x) = ...
fun Nothing = ...
いくつかの選択肢があったとしても、数個であり、冗長になりすぎなければ、きちんと列挙したほうが安全です。つぎのようには、できるだけしないようにしましょう。
data A = X | Y | Z | W
fun X = ...
fun _ = ...
つぎのようにしましょう。
fun X = ...
fun Y = ...
fun Z = ...
fun W = ...
このようにしておけば、型Aに、たとえば、値構築子Vが追加されたときに、未修正の箇所を、処理系が警告してくれます。
モジュールの導入
インポート文は、直観的に、より一般的なものから、より具体的なものになるように、並べます。直観的、かつ、主観的なルールですが
* Control
* Data
* System
* Text
* Network
のような、順とします。より細かくは
* Control.Functor
* Control.Applicative
* Control.Monad
のように、制約が厳しくなっていくような順で、並べています。