作成: 2017-12-02T00:00:00+09:00

更新: 2020-06-25T04:10:58+09:00

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

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

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

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

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

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

また, このスタイルガイドは一応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 には then と else が必須です. なので, この then と else の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との相互変換も容易だからです.

ファンクター演算子, アプリカティブファンクター演算子の改行位置は定まっていません <$> や <*> の前に改行を入れるのか, 後ろに改行を入れるのは未だに定まってなくていつも悩んでいます. これをどちらかに定める合理的な理由を探しています. <*> は列になることが多いのでカンマの理屈からすると前に改行を入れるべきですが, モナドバインド演算子 >>= は後ろに改行を入れるので, そちらに統一したいという気持ちもあります.

他の演算子の改行位置は定まっていません 基本的に演算子の後ろで改行しています.