Scala Style Guide リーディングメモ
Scala Style Guide
Overview
Indentation
Line Wrapping
- 一行が読めないくらい長くなったら行を折り返す(長さは80文字以上の任意の値でOK)
- 以下のような pipeline operator でのやり方は望ましいが、必ずしもそれが実務的なやり方とはいえない
implicit def any2PipelineSyntax[A](a: A) = new { def |>[B](f: A => B): B = f(a) } def add(a: Int)(b: Int) = a + b 2 |> add(3) |> println 2 |> add(3) |> println
- 改行された2行目以降は1行目よりもスペース2つでインデントする
val numbers = 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9
Methods with Numerous Arguments
- たくさんの引数(5個以上とかそれくらい)を持つメソッドの場合、メソッド呼び出し部分を複数の行に折り返す
- 折り返したパラメータの行はスペース2つでインデントする
foo( someVeryLongFieldName, andAnotherVeryLongFieldName, "this is a string", 3.1415)
- インデントが深くなるケース(例えばスペース50個以上)では以下のようにする
val myOnerousAndLongFieldNameWithNoRealPoint = foo( someVeryLongFieldName, andAnotherVeryLongFieldName, "this is a string", 3.1415)
Naming Conventions
- 基本的にはキャメルケース(先頭文字以外で単語の区切りで大文字にする)
- アンダースコアはScalaの文法において特別な意味を持つので、使用は強く非推奨
Classes/Traits
- キャメルケースかつ先頭の文字が大文字
- Java を模したルール
Objects
- 基本的には class と同様(キャメルケースかつ先頭の文字が大文字)
- ただし、パッケージ、関数を表現する場合はこの限りでない
package object example { } object increment { def apply(x: Int): Int = x + 1 } increment(2) // -> 3
Methods
- キャメルケース、先頭の文字は小文字
Accessors/Mutators
Scala は Java の setter/getter の慣習に従っていない
- プロパティの getter のメソッド名はそのプロパティの名前であるべき
- boolean 値の getter の場合「is」を先頭に付与することは許容されるが、これは対応する setter が存在しない場合に限る
- Lift では boolean 値の getter に「_?」を末尾につけるという慣習があるが、これは Lift 以外ではスタンダードな慣習ではない
- setter の名前はプロパティ名に「_=」をつけたもの、呼び出し側ではあたかも代入のように(foo.bar = "aaa")呼び出せる
- Scala で private なプロパティ自身とアクセサの名前を分けたい場合はプロパティ名を「_name」のようにする(ハンガリアン記法や「name_」よりもこちらを推奨する)
Parentheses
parenthesisの複数形、丸括弧のこと。
- Ruby と違って Scala では「()」が宣言されているかどうかに重要な意味がある、定義時に「()」つきで宣言した場合は「()」ありでもなしでも呼び出せるが、「()」なしで宣言した場合は「()」つきで呼び出すとコンパイルエラーになる
- フィールドや論理的なプロパティへの getter として宣言される場合、「()」なしで宣言されるべき(副作用を持つ場合を除く)
- Ruby や Lift は副作用の表現に「!」を用いるが、「()」の方が望ましい
- 呼び出し側も同じルールに従うべき(文字数を減らすために常に「()」を省略するのは読みやすさやメンテナンス性から推奨しない)
Symbolic Method Names
Values, Variable and Methods
- キャメルケース、先頭の文字は小文字
Type Parameters(generics)
- 型パラメータは A から(Java の T からの慣習とは異なる)始まる大文字一文字で表現するべき
- 型パラメータが特定の意味を持つ場合は、説明的な名前をつけてもよいが全て大文字は避けること
class Map[Key, Value] // よい名前 class Map[KEY, VALUE] // 全て大文字は避けるべき
- 型パラメータのスコープが十分小さい場合は、説明的な名前よりも簡略記号を用いるのがよい
class Map[K, V] { def get(key: K): V def put(key: K,value: V): Unit }
Higher-Kinds and Parameterized Type parameters
- Higher-kind も通常の型パラメータと同様
class HigherOrderMap[Key[_], Value[_]] { ... }
- Monad のようなよく知られた基本的な概念については一文字で表現することが許容される
def doSomething[M[_]: Monad](m: M[Int]) = ...
Annotations
Types
Inference
- 可能な限り型推論を使うべき、valで定義されたフィールドの型は明確に決まるので型を記述する必要はない
- 一方、public メソッドでは型推論が悩みの種になることがあるので、クラスの public メソッドには全て型を明示するべき
Function Values
- map 関数の引数などでは型は確定するので型を明示する必要はない
- implicit conversion では明示的な型の指定が強制される
"Void" Methods
- Unit を返すメソッドは例外的に 型を指定せず「=」を省略する記法の方が望ましい
Annotations
- 型の指定は「value: Type」のフォーマットで記述すべき(値とコロンはくっつけてコロンと型との間にスペースを入れる)
Ascription
Ascription は帰属。以下のような型チェッカーを満足させるために必要となるもの。
- 型の指定と同様のフォーマットで書くべき
Nil: List[String] Set(values: _*) "Daniel": AnyRef
Functions
- 関数定義ではパラメータの型と「=>」の間はスペースを入れるべき
def foo(f: Int => String) = ... def bar(f: (Boolean, Double) => List[String]) = ...
- 丸括弧は可能なら省略することを推奨する(Int => Stringのように)
Arity-1
Arity は関数がとる引数の個数のこと。
- Arity-1 では丸括弧を省略することを推奨する(丸括弧を省略する事で型指定の可読性が向上するため)
// 可読性に問題がある def foo(f: (Int) => (String) => (Boolean) => Double) = ... // 省略可能な丸括弧は省略すべき def foo(f: Int => String => Boolean => Double) = ...
Structural Types
- 50文字以内なら構造型は一行で記述すべき、その場合必要ならセミコロンとスペース1つで行を区切る
- それ以上になる場合は複数行で記述するべき
- 「{」直後と「}」直前にそれぞれスペースを1つ入れる
def foo(a: { val bar: String }) = ... private type FooParam = { val baz: List[String => String] def bar(a: Int, b: Int): String }
Nested Blocks
Parentheses
- 丸括弧の「(」直後と「)」直前にはスペースを入れないこと(パーザーコンビネータを使った記述の場合を除く)
Declarations
Classes
- コンストラクタは一行で記述すべきだが、長すぎる場合(約100文字以上)は改行する
- 改行する場合は各パラメータは一行ずつに記述し、スペース4つでインデントすること
- 継承の場合は100文字以上なら改行し、スペース2つでインデントすること(これによりコンストラクタ引数と視覚的に区別する)
Ordering Of Class Elements
- メンバーは基本的に間に空行を入れて定義するが、var、val宣言だけは空行を入れずに定義する
- フィールドはメソッドよりも先に定義する
- ただしブロックつきのメソッドのような性質のものであれば、上記に従う必要はないがval、lazy valに限る
Methods
- 「def foo(bar: Baz): Bin = expr」のような形で定義すること
- Unit 型を返す場合は「def foo(bar: Baz) { expr }」にように書くべき
- 引数のデフォルト値は「=」の前後にスペースを入れて「(x: Int = 6, y: Int = 7)」のように記述すること
Modifiers
修飾子は以下の順序で記述すること
- アノテーションをそれぞれ一行で
- override
- アクセス修飾子(protected、private)
- final
- def
@Transaction @throws(classOf[IOException]) override protected final def foo() { ... }
Body
- メソッドの中身が30文字以下であれば一行で記述する
- 30文字以上でだいたい70文字より短ければ改行してスペース2つでインデントする
def sum(ls: List[String]) = (ls map { _.toInt }).foldLeft(0) { _ + _ }
- 一時変数があったりして関数的でないメソッドの場合はブレースで囲って記述する
def sum(ls: List[String]) = { val ints = ls map { _.toInt } ints.foldLeft(0) { _ + _ } }
- match 節だけを含むようなメソッドは以下のように記述する
def sum(ls: List[Int]): Int = ls match { case hd :: tail => hd + sum(tail) case Nil => 0 }
Multiple Parameter Lists
カリー化された引数リストについて。
- 一般論としては(そうすべきでない理由がないなら)カリー化された引数リストだけを使うべき
その理由は以下の3つ。
- For a fluent API
流れるようなインタフェース、制御構文のように書ける。
def unless(exp:Boolean)(code: => Unit) = if (!exp) code unless(x < 5) { println("x was not less than five") }
- Implicit Parameters
implicit parameters を使うにはカリー化された引数リストが必要。
- For type inference
シンプルなシンタックスで型推論が機能するため。
// 引数リストを使うと以下のように簡潔に書けるが・・ def foldLeft[B](z:B)(op: (A,B) => B): B List("").foldLeft(0)(_ + _.length) // 引数リストでない場合は・・ def foldLeft[B](z: B, op: (B, A) => B): B // このように書かなければならなくなる List("").foldLeft(0, (b: Int, a: String) => a + b.length) List("").foldLeft[Int](0, _ + _.length)
- 引数リストを一行で書くことが難しい場合は引数リスト毎にそれぞれ改行して記述する
Higher-Order Functions
- 高階関数で以下のように渡される引数の関数は丸括弧で囲まずに記述する
def foldLeft[A, B](ls: List[A])(init: B)(f: (B, A) => B) = ... foldLeft(List(1, 2, 3, 4))(0) { _ + _ }
Fields
- メソッドと同様のルールで修飾子の順序に注意すること
- lazy val の lazy は val の直前である必要がある
Function Values
関数の定義の仕方は以下のように複数のやり方がある
- val f1 = { (a: Int, b: Int) => a + b }
- val f2 = (a: Int, b: Int) => a + b
- val f3 = (_: Int) + (_: Int)
- val f4: (Int, Int) => Int = { _ + _ }
f1 と f4 はどのような場合にも望ましいスタイルである。
f2 はこの例だと短くなるが関数を複数行に渡って記述する場合は見苦しくなるかもしれない。
同様に f3 も簡潔ではあるが理解しづらい(Scala に慣れていないとそれが関数値を返すとわかりづらい)。
f1 f4 だけが使われている場合、どちらもブレースで囲まれているので関数が使われている箇所を判断するがとてもやりやすくなる。
Spacing
- ブレース「{」直後と「}」直前にスペースを1つ入れること
Multi-Expression Functions
- ブレースの開始「{」と引数リストと「=>」は同じ行にあるべき
Control Structures
制御構文でのルール。
- if や for と丸括弧の間にはスペース1つ入れる
if (foo) bar else baz for (i <- 0 to 10) { ... } while (true) { println("Hello, World!") }
Curly-Braces
以下のようなケースでは波括弧は省略する。
- if 式では else 節を持つ場合は波括弧を省略してよい、 else 節がない場合はたとえ一行で書ける場合でも省略不可
- while 文では波括弧を省略しないこと
- for 式では yield 節がある場合は波括弧を省略してよい、 yield 節がない場合はたとえ一行で書ける場合でも省略不可
- case 節は一行で書ける場合は波括弧を省略してよい、それ以外の場合は明快さのために波括弧を省略しないこと
Comprehensions
- 内包表記でジェネレータが複数ある場合はセミコロン区切りで一行に書かず、改行して一行ずつにする
for { x <- board.rows y <- board.files } yield (x, y)
- ただし yield 節がない場合は(実質 for ループなので)簡潔な記述の方を推奨する
for (x <- board.rows; y <- board.files) { printf("(%d, %d)", x, y) }
Method Invocation
- Java と同様の慣習に従うべき
- メソッドを呼び出す対象とメソッド名の間は「.(ドット)」でつないで間にスペースを入れないこと
- メソッド名と丸括弧の間にスペースを入れないこと
- カンマ区切りの引数の間にスペースを1つ入れること
- 名前付き引数では「=」の前後にスペースを1つずつ入れること
Arity-0
- 副作用のないもの(a.size)は丸括弧を省略してよい、副作用がある場合(println())は省略せずに丸括弧を書くこと
Suffix Notation
- arity-0 では「.(ドット)」を省略する記法(name toList)を使ってよい、可読性は劇的に向上するが十分注意して使うべき
- 丸括弧なしで定義されているような副作用がないメソッドの呼び出しでのみ使用されるべき
- 副作用のないメソッドチェーンの最後のケースでは慣用的に許容される
// これは許容されうる、慣用的なやり方 names map { _.toUpperCase } filter { _.length > 5 } toStream
- 代入時には特殊な状況下でなければ避けるべき
// 好ましくない val ls = names toList // こちらの方がよい val ls = names.toList
- パーザーコンビネータのようなDSLでは例外的に許容できる
// 許容範囲 val reg = """\d+(\.\d+)?"""r // より安全 val reg = """\d+(\.\d+)?""".r
Arity-1
- arity-1 での記法は「infix notation」としても知られている、副作用のない mkString などのメソッドで慣用的に使われる
names foreach { n => println(n) } names mkString "," optStr getOrElse "<empty>" // 副作用のあるものはNG javaList add item
Higher-Order Functions
// ドットでつなぐよりも・・ names.map { _.toUpperCase }.filter { _.length > 5 } // infix notation の方が望ましい、スタンダードなやり方 names map { _.toUpperCase } filter { _.length > 5 }
Symbolic methods/Operators
- 「+」などの記号文字によるメソッドの呼び出しは前後にスペースを1つずつ入れること
"daniel" + " " + "Spiewak" foo ** (bar, baz)
- 記号が名前になったメソッドはあまり多くないが API をデザインする場合はこれをさけるべき
- 「/:」「:\」を使うのはさけて「foldLeft」「foldRight」を使用すべき(文字数を削減できるよりも極端に分かりにくくなるデメリットの方が大きい)
Files
- 一つのソースファイルは論理的に一つのコンパイル単位だけを含むべき
- 論理的に、とは具体的には class、trait、object を意味する
- ただし、コンパニオンオブジェクトはこの限りでない
たとえば、以下のケースでは「com/novell/coolness/Inbox.scala」として一つのソースファイルに記述されるべき。
package com.novell.coolness class Inbox { ... } object Inbox { ... }
Multi-Unit Files
一つのソースファイルに複数のコンパイル単位を含めるべきケースがある。
ありふれた例としては sealed な trait とそのサブクラスの場合である。
sealed な super class/trait の場合はすべてのサブクラスがすべて同じソースファイルに記述される必要がある。
sealed trait Option[+A] case class Some[A](a: A) extends Option[A] case object None extends Option[Nothing]
- 密接な関係や概念を共有するクラス群が一つのソースファイルに存在することでメンテナンス性が向上する場合も、複数のコンパイル単位を同じソースファイルに書いてもよい
- 複数のコンパイル単位を含むソースファイルはキャメルケース、先頭は小文字のファイル名にすること
ScalaDoc
- ScalaDoc は基本的には Javadoc の慣習を踏襲しているが、よりシンプルに記述するためにいくつか付け加えられた特徴がある
- 二行目以降のアスタリスクのインデントがJavadoc のように1つではなく2つになっている点に注意
- ScalaDoc は経験豊富なユーザだけでなく新米のユーザにとっても役立つものでなければならない
- まず簡潔な概要を記述する(これは経験豊富なユーザにリファレンスとして役立つ)
- 次により詳細な例示や詳細な項目を提供する(これは経験豊富なユーザには無視されるかもしれないが、新米のユーザにはかなり役立つ)
/** まずここに概要を記述します。 * * ここにさらにドキュメントを記述します。 * より詳細な内容やどのように動作するのか、また何をするものなのかなどを記述します。 */ def myMethod = {}
- 一行で完結するようなシンプルなドキュメントは以下のようなフォーマットでもかまいません
/** 簡潔に説明する場合の書き方です */ def simple = {}
General Style
- 「if some condition return true」よりも「returns true if some condition」
- 値を返すメソッドの場合「This mehod returns xxx」「Get xxxx」よりも「Returns xxx」
- クラスの場合「This class does xxx」よりも「Does xxx」
- リンクは「[[scala.Option]]」
- 戻り値に関する記述は「@return」をつけて記述し ScalaDoc のメイン部分で冗長な記述を書かないようにする
- メソッドが何を返すか一行で簡潔に書けるような場合は「@return」で同じ内容をくりかえさない
- そのメソッドが「何をするか」を記述し「何をすべきか」は記述しないこと(細かいニュアンスだが「Return xxx」よりも「Returns xxx」)
- インスタンスについて言及するときは「the xxx」よりも「this xxx」「this」とする、object の場合は「this object」とする
- コード例はこの Style Guide に従ったスタイルで記述すること
- 可能な限り HTML よりも wiki スタイルで記述すること
- コード例は実行に必要となるすべてを記述するか、あるいは REPL での実行結果をそのまま貼り付けること
- 特殊なフォーマッティングを必要とするよく出てくる値には @macro をふんだんに使用すること?
Packages
- まず package に属するクラス群について説明し、次に package object 自身やそれが何を提供するかについて記述する
- package に属するすべてのクラスの本格的なチュートリアルを記述する必要はない、主要なクラスの概要だけを述べるにとどめる
package parent.package.name /** This is the ScalaDoc for the package. */ package object mypackage { } package my.package /** Provides classes for ... * * ==Overview== * ... */ package complex {}
Classes, Objects, and Traits
- 最初の行はその class/trait/object が何をするかについてのサマリを記述する
- すべての型パラメータについて @tparam でドキュメントをつける
Classes
- もしコンパニオンオブジェクトを使ってインスタンス化すべきクラスの場合は、クラスの概要の説明のあとにそれを述べること
- 現状 ScalaDoc 内でコンパニオンオブジェクトへのリンクをつける方法はないが、生成された ScalaDoc ではたどることができるようになっている
- コンストラクタを使ってインスタンス化する場合は @constructor でその内容を記述する
Objects
- どのようにその object を使用するのか(ファクトリーとして、implicit メソッドの集合として)を記述することが重要
- ファクトリーとして使用する場合は apply メソッドのドキュメントにもその内容を記述すること、また apply ではないメソッドをファクトリーメソッドとして使用する場合には、その旨を述べること
/** Factory for [[mypackage.Person]] instances. */ object Person { def apply(name:String,age:Int) = {}
- implicit conversion を持つものとして使用する場合は使用例を記述すること
/** Implicits conversions and helpers for [[mypackage.Complex]] instances. * * {{{ * import ComplexImplicits._ * val c:Complex = 4 + 3.i * }}} */ object ComplexImplicits {}
Traits
- 何をする trait なのかを記述した後でメソッドや型の概要を記述する
- もしこの trait を使用している既知のクラスがあればそれについて触れる
Methods and Other Members
- メソッドの場合も上記と同様、最初の一文はこのメソッドが何をするかについてサマリを記述し、以降により詳細な内容や使用例などを記述する
- 型パラメータについては @tparam をつけて記述する
- カリー化された関数の場合はどのような慣用的な使用を想定しているかより詳細な使用例が示されている方がよい
- implicit parameter についての記述は特別な配慮が必要、そのパラメータがどのように渡ってくるのか、ユーザはそのパラメータが有効になっているか確認するためにさらにどのようなことをする必要があるかなど