• 作成:
  • 更新:

私のHaskellコーディングスタイルガイド, 改行出来るポイントを紹介

Haskell (その3) Advent Calendar 2017 - Qiitaの2日目の記事です.

Haskellは各構文を文ではなく式として扱えるため, 適当に書いていくと, どんどん一行が長くなっていきます. その結果, ワンライナーのようなコードが作られることがよくあります.

この目のチカチカを避けるためか、どうか、出来るだけ間隙を狭くするために、Haskellプログラマーは無意識にワンライナーになります。例えば上の例だと 2 のケースをだらだらーと一行に書きたがるのですね。その結果、一行500文字の Haskellコードなどが産み出されるのです。私は出来るだけ長くプログラム書くキャリアを続けたいんで、フォントは大きいんですよ。何ポイントか知らないけど、30inch のモニタでウィンドウいっぱいいっぱいにして190文字位しか一行に出せないんです。500文字のためには30inchモニタが三枚必要なんです。あなたは金を出してくれるんですか?ってことです。さすがに Haskell プログラマも一行が長すぎるとやはり気が咎めるらしいのですが、それでやる事と言えば、変数名の長さをケチるのです。CamelCase は始まりでしかありません。ただ、 a -> とか。a たあなんだよ。読む人のこと考えろよ。

経験15年のOCaml ユーザーが Haskell を仕事で半年使ってみた - Oh, you `re no (fun _ → more)

この記事では, 私が最近私自身に定めているHaskellのコーディングスタイルガイドを, 改行できるポイントを交えて文書化していきます.

一般的なスタイルガイドは

などがあります. 同じ部分もありますが, 異なっている箇所もあります.

また, このスタイルガイドは一応Haskell初心者に向けても解説をしています.

stylish-haskellを使いましょう

Haskellを書くときはstylish-haskellとhlintを使って労せずして綺麗なコードを書きましょう - ncaq に書きましたが, Haskellにはjaspervdj/stylish-haskell: Haskell code prettifierという優れたコードフォーマッタが存在します.

まずこれを使いましょう. これをデフォルト設定で使っているだけで, いくつかのコードの整形は自動的になされます.

繰り返し書きますが, stylish-haskellは絶対に使いましょう. 自分の設定だと使えなくても諦めないでください. stylish-haskellはある程度柔軟な設定が可能で, 例えば使っていないプラグマの除去などはTemplate Haskellで誤動作することがあるため削除することが可能です.

Emacsの設定

私のEmacsのhaskell-modeの変数設定は以下のようになっています.

(custom-set-variables
 '(ac-modes (append '(haskell-mode inferior-haskell-mode haskell-interactive-mode) ac-modes))
 '(haskell-indentation-layout-offset 4)
 '(haskell-indentation-left-offset 4)
 '(haskell-indentation-starter-offset 4)
 '(haskell-indentation-where-post-offset 2)
 '(haskell-indentation-where-pre-offset 2)
 '(haskell-stylish-on-save t)
 )

基本的にインデントは2, stylish-haskellを保存時に実行する設定になっています.

1行は100文字まで

最初の説でリンクを貼ったHaskellのコーディングスタイルガイドでは1行は80文字までとなっていますが, 私は流石にそれは厳しすぎると考えていて, 1行は100文字としています.

何故100文字かと言うと, 私の環境のEmacsは1ウィンドウをフルにすると200文字ちょっと表示できるので, 横にflycheckなどを表示している縦2分割のスタイルだと折り返しが発生しないのは100文字ちょっとだからです.

LANGUAGEプラグマは1行ごとに書いてソートする

LANGUAGEプラグマは

{-# LANGUAGE NamedFieldPuns    #-}
{-# LANGUAGE OverloadedStrings #-}

として欲しいということです. これは

{-# LANGUAGE NamedFieldPuns, OverloadedStrings #-}

と1行で書くことも出来ますが, 1行で書かれるとどのプラグマが有効になっているのかわかりにくいですし, 追加や削除も行削除で出来ないので面倒になります.

stylish-haskellを使っていると, 自動で複数行にしてソートしてくれるので, 是非有効にしましょう.

data定義の等号前には改行を入れる

data Color
    = Red
    | Green
    | Blue
    | Rgb
      { r :: Int
      , g :: Int
      , b :: Int
      }
    deriving (Eq, Ord, Show, Read)

のように, dataの等号の前に改行を入れると丁度良いことに最近気がつきました. このスタイルだと, 直和型の他のコンストラクタが増えたときに等号=と同じ位置に直和記号|を入れることが出来て気持ちが良い. 当初はコンストラクタが1つだと思っていても, 後から追加することもあるので, 最初から追加しても問題ないようにコードを書いておくとdiffが気持ちいいですね.

カンマ区切りのブロックは初めの記号前に改行, カンマは先頭に

先程フィールド定義した時も見せましたが, Haskellではカンマでものを区切る時はカンマを先頭に置きます.

module NewlineSyntax
    ( exampleList
    , rgb
    ) where

exampleList :: [Int]
exampleList =
    [ 0
    , 1
    , 2
    ]

rgb :: (Int, Int, Int) -> Color
rgb (red, green, blue) = Rgb
    { r = red
    , g = green
    , b = blue
    }

カンマを末尾に持ってくる文化の言語から見ると奇妙に思えるかもしれませんが, これには利点があります. Haskellではケツカンマ(RustやJavaScriptにある末尾のカンマを許す文法)がないので, 末尾にカンマを付けると, 要素を追加する時に2行の変更が必要になります. 対してこの形式では先頭に要素を追加する時以外では要素の追加が1つなら変更は1行で済みます.

カンマ区切りのブロックが始まる場合, その前に改行をした方が良いでしょう. そうでないと横幅に余裕がなくなります.

PureScriptなどのHaskellの影響を受けた言語もこの形式を採用していますが, Elmはそうではありません. 詳しくはElm Style Guide.mdを参照してください. Elmはケツカンマが許されており, DOMを書くことに重点を置いたDSLなので, こうなっているのでしょう. 実際Elmを書いてみるとわかりますがこの形式でDOMを書くと気持ちが良い.(脱線)

カンマ区切りのブロックを1行で書く時はカンマの後にしかスペースを入れない

exampleOneLineList :: [Int]
exampleOneLineList = [0, 1, 2]

exampleOneLineRecord :: Color
exampleOneLineRecord = Rgb{r = 0, g = 1, b = 2}

このようにスペースはカンマの後ろだけに入れるようにします.

リストは型名が[Int]のようにスペースが入らないことと一貫性を保っています.

レコードはRecordWildCards拡張を使ったとき, Rgb{..}のように書きたいので, そちらとの一貫性を保っています.

こういう風に1行で書きたい時は横幅を節約したいことが多いので, 可読性に影響しない範囲で横幅を削っています.

ラムダ式では矢印の右側で改行しましょう

ラムダ式を使っている行改行したくなったら矢印の右側で改行しましょう.

useLambda = \foo ->
    foo + foo

ただし, doを使う時はdoの後に改行を入れましょう.

useLambdaDo :: IO a -> IO a
useLambdaDo = \foo -> do
    foo
    foo

if-then-elseはそれぞれの前に改行を入れましょう

Haskellのifにはthenelseが必須です. なので, このthenelseの2つはそれぞれ同列の存在として扱ったほうが良いでしょう.

つまり以下はOKで

okIfThenElse p y n =
    if p
    then y
    else n

以下はダメということです.

ngIfThenElse p y n =
    if p then
        y
    else n

シェルスクリプトの影響などで, thenをここに置きたがる気持ちはわかりますが, Haskellではthenには必ずelseがくっついているため, この2つは同列に扱いましょう.

importはstylish-haskellに任せましょう

繰り返しますが, stylish-haskellを使いましょう. そしたら自動でうまくいきます.

関数が長いけれど関数内で改行するほどの長さでない場合等号の後に改行を入れましょう

foo a b c =
    veryLongLineFunction a b c

のような感じです. データ定義のときとは違う改行の仕方になってしまいますが, こうしないとエラーになるので仕方がありません.

モナドバインド演算子の改行は演算子の後ろにしましょう

bindNewLine a b =
    a >>=
    b

のようにするべきです. 何故ならバインド演算子の後にはラムダ式の引数が来ることなどが多く, このスタイルを使っているとdoとの相互変換も容易だからです.

ファンクター演算子, アプリカティブファンクター演算子の改行位置は定まっていません

<$><*>の前に改行を入れるのか, 後ろに改行を入れるのは未だに定まってなくていつも悩んでいます. これをどちらかに定める合理的な理由を探しています.

<*>は列になることが多いのでカンマの理屈からすると前に改行を入れるべきですが, モナドバインド演算子>>=は後ろに改行を入れるので, そちらに統一したいという気持ちもあります.

他の演算子の改行位置は定まっていません

基本的に演算子の後ろで改行しています.

まとめ

コードの綺麗さというのは所詮主観的な美意識の問題で, これと言った正解はありません. しかし, Haskellはインデントセンシティブな言語なので, 折り返しが発生すると混乱が増します. なので, エディタが折り返さない程度のカラム幅には留めておきたいものですね.