よいプログラムレイアウトはコードの論理構造を明瞭にします。したがってよいレイアウトを心がけることで,わかりやすいコードを生みやすくなるでしょう。
F# のようにインデントが文法的に意味を持つ言語においては,レイアウトがある程度制限されているため,一定レベルのレイアウトは保証されますが,細部においてはコードを書く人の裁量にまかされます。そこで,どのようなレイアウトでコードを書けばよりわかりやすいコードになるのか,特にインデントを中心として考えてみました。
ここで述べる内容が正しいと主張するわけではありませんし,実際に遵守すべき事項であると提言するわけでもありません。あくまで参考程度にどうぞ。
インデントの基本ルール
1 段階のインデントは 2 つ以上の決まった数のスペースにします。例えば本文では 3 つのスペースを 1 段階のインデントとしています。インデントが深くなるほど行が横に伸びるため,可読性を向上させる目的以外ではみだりにインデントを深くしてはいけません。
インデントの深さは変更に対して頑健であるべきです。すなわち,前の行になんらかの変更があった場合でも,インデントの深さを変更しなくても大丈夫なようにします。例えば次の場合を考えます。
let fizzbuzz n = match n % 3, n % 5 with
| 0, 0 -> "FizzBuzz"
| 0, _ -> "Fizz"
| _, 0 -> "Buzz"
| _, _ -> string n
このパターンマッチのインデントは頑健ではありません。なぜならば,関数名やパラメーター名が長くなった場合に | が match キーワードより左側にくることになります。
let fizzbuzz value = match value % 3, value % 5 with
| 0, 0 -> "FizzBuzz"
| 0, _ -> "Fizz"
| _, 0 -> "Buzz"
| _, _ -> string value
一行か複数行か
一行の式は次のような例が挙げられます。
ignore 42
let f x = x + 1
do printfn "%s" "Hello, world!"
fun x -> x + 1
複数行の式は次のような例が挙げられます。
let f x = x + 1
do printfn "%s" "Hello, world!"
fun x -> x + 1
一行の式の場合は当然インデントが生じませんが,複数行の式の場合はインデントが生じます。一行にするか複数行にするかは,基本的には行の長さとの兼ね合いですが,たとえばインデントの基本ルールを破る場合などは行が短くても複数行の記法を選択します。
関数適用
関数適用は原則として一行で記述します。
someFunction parameter1 parameter2 ... parameterN
パラメーターが長く,行が長くなりすぎる場合は,部分適用した関数やパラメーターに一時変数を導入して工夫するか,あるいはパイプライン演算子を用いて次節のルールにしたがい複数行に分割します。
演算子
演算子を含む演算は基本的に一行で記述しますが,オペランドが長くなる場合は複数行にしてもかまいません。ただし,複数行にできるのは中置記法の場合に限り,前置記法の場合は,前述の関数適用のルールを採用します。
演算を複数行で記述する場合,演算子の前で改行して行の先頭に演算子がくるようにします。また, 1 つの式中に演算子が複数あり,複数行で記述する場合は,すべての演算子の前で改行します。
seq { 1 .. 10 }
|> Seq.map (fun i -> i * i)
|> Seq.iter (printfn "%d")
ループ
for ループや while ループは一行でも複数行でも記述できます。
一行の場合は次のようになります。
for i = 1 to 10 do printfn "%d" i
複数行の場合は,次のように do キーワードをループ条件と同じ行に続けます。冗語構文でループの終了に done キーワードをつける場合は,ループ式のキーワードと同じ深さにインデントします。
for i = 1 to 10 do printfn "%d" i [done]
もし do キーワードの前で改行してしまうと, do キーワードはインデントする必要があります。このとき do 束縛内のコードは複数行の式である場合にインデントが必要となり,インデントが深くなります。
for i = 1 to 10
do
let squared = i * i
printfn "%d^2 = %d" i squared
if 式
if 式は一行でも複数行でも記述できます。
一行の場合は次のようになります。
if x = 0 then "zero" else "not zero"
複数行の場合は,次のように条件の行に then キーワードを続けます。単純な if .. else ではなく elif キーワードを用いて複雑な条件分岐をする場合は常に複数行のスタイルを選択します。 elif や else は if とインデントの深さを揃えます。
if x = 0 then "zero" elif x < 0 then "negative" else "positive"
このレイアウトスタイルは, then キーワードの位置がループの do キーワードと同じように条件の直後にくるため,見た目における一貫性があります。
パターンマッチ
function 式や match 式は,原則としてパターンごとに行を分けます。各パターンは 1 つインデントします。
match x with | 0 -> "zero" | _ -> "not zero"
パターンをインデントすることにより,次の行にパイプライン演算子が続いても,パターンの | とパイプライン演算子を区別することができ,可読性が良くなります。
match x with | 0 -> 0 | x when x < 0 -> -1 | _ -> 1 |> printfn "%d"
パターンが単一の場合,もしくは option 型やパーシャルアクティブパターンのように単純な条件の場合で,かつマッチ後の処理が単純である場合は,一行にすることができます。一行の場合の最初のパターンの前の | は省略できますが,パターンが複数存在する事を認識しやすいように,単一パターンの場合を除き省略しません。
function | Some (_) -> "some" | None -> "none"
let 束縛で match 式を使う場合, = の直後に match キーワードがくることを避け,常に複数行の記法を選択します。これはインデントの基本ルールにしたがっています。
let rec fac n =
match n with
| 0 -> 1
| n -> n * fac (n - 1)
function 式の場合は let 束縛において = の直後に行を変えずにキーワードを続け,インデントが深くなるのを避けます。
let rec fac = function | 0 -> 1 | n -> n * fac (n - 1)
match 式と function 式でのルールは一貫していませんが,インデントを深くしないという基本ルールには合致します。
function 式が単体で完結しない場合は, = の後ろで改行するようにします。
let fizzbuzz =
function
| n when n < 0 ->
failwith "negative value"
| n ->
let isFizz, isBuzz = n % 3 = 0, n % 5 = 0
let fizz = if isFizz then "Fizz" else ""
let buzz = if isBuzz then "Buzz" else ""
let number = if not isFizz && not isBuzz then string n else ""
fizz, buzz, number
>> (<|||) (sprintf "%s%s%s")
コンピュテーション式
コンピュテーション式を一行で記述する場合は次のようになります。
seq { yield 0 }
複数行の場合は,始まりの波括弧 '{' をビルダー名に続け,終わりの波括弧 '}' は 1 つインデントします。これらは前述のループの冗語構文における do と done の位置に対応します。
seq {
yield 1
yield 2
}
複数行の let 束縛では, function 式と同様に, = の直後にビルダー名を続ける事ができます。
let rec drift x0 = seq {
yield x0
let x' = x0 + if random.NextDouble () < 0.5 then 1 else -1
yield! drift x'
}
コンピュテーション式が単体で完結しない場合は,ビルダー名の前で改行します。式の中身はインデントを深くします。
let squared =
seq {
yield 1
yield 2
}
|> Seq.map (fun i -> i * i)
レコード
フィールドが多く,一行が長くなる場合は複数行にします。一行の場合も複数行の場合も,コンピュテーション式と同様の見た目になるようにします。
type Person = { Name : string; Birthday : DateTime }
type Person = {
Name : string
Birthday : DateTime
}
メンバーを定義する場合は,波括弧の前で改行します。
type Person =
{ Name : string; Birthday : DateTime }
override this.ToString () = this.Name
フィールドを複数行で記述する場合は,括弧を 1 段階,フィールドを 2 段階それぞれインデントを深くします。
type Person =
{
Name : string
Birthday : DateTime
}
override this.ToString () = this.Name
判別共用体,列挙体
判別共用体や列挙体はパターンマッチと同様の見た目になるようにします。すなわち,行を分ける場合は, 1 段階インデントします。
type Verbosity = Quiet | Normal | Verbose
type Verbosity = | Quiet | Normal | Verbose
インターフェイスの実装
次のように with キーワードをインターフェイス名の後ろに改行せずに記述します。メンバーの実装は 1 段階インデントします。
type ISome with member this.Foo () = "foo"
オブジェクト式
前述のコンピュテーション式とインターフェイスの実装を合わせたような形式にします。
let implementation = {
new ISome with
member this.Foo () = "foo"
interface IAnother with
member this.Bar () = "bar"
}
let foo =
{
new ISome with
member this.Foo () = "foo"
}
|> fun some -> some.Foo ()
まとめ
オフサイドルールを採用する F# において,インデントにはそれなりの自由度があります。何も考えずに書くとレイアウトに一貫性のないコードができてしまいます。このガイドラインを採用するかどうかはともかく,一度自分のレイアウトルールを見直してみるのも一興かもしれません。
後記
[2013-03-19 1:15 追記] この記事を読まれた方々に反応をいただきました。ありがとうございます。
if 式,パターンマッチが人によって違ってくるだろうなというのは,おおむね予想通りでした。後コンピュテーション式も違ってくるかと思いましたが,上記のお二方は同じだったようです。この辺りは何を重視するかによってスタイルが変わってくるのかなと思います。