LoginSignup
37
39

More than 5 years have passed since last update.

Databricks Scala Style Guide (和訳)

Posted at

最近Scalaを使っているのですが、チームで共有できるスタイルガイドが欲しくて探していたところSparkを作っているDatabricksのスタイルガイドを見つけたのでそれを和訳しました(twitterのも良いんですけど、Sparkも使っているし、感触的に「これはやって良い」、「これはやらないで」が比較的きっぱりと書いてある印象があったので選びました)。

お恥ずかしいことに私のJVM周りのバックグラウンドが乏しいのもあり、根本的にスタイルガイドを間違えて翻訳している可能性もあるため、 ご指摘いただけると大変有り難く思います 。指摘事項に関しては随時反映させていただきます。

(※一部翻訳できていない部分がありますが、今日、明日中には更新予定)

構文のスタイル(Syntactic Style)

命名規則

原文へのリンク

命名規則については概ねJavaとScalaの標準的なものに則っています。

  • class, trait, object はJavaの命名規則に従います。 (i.e. パスカルケース(PascalCase))
  class ClusterManager

  trait Expression
  • package はJavaの命名規則に従います。 (i.e. ASCII文字で全て小文字)
  package com.databricks.resourcemanager
  • methods/function は キャメルケース(camelCase) にします。

  • 定数(constant)は全て大文字にしてコンパニオンオブジェクトに入れます。

  object Configuration {
    val DEFAULT_PORT = 10000
  }
  • enum はパスカルケース(PascalCase)にします。

  • アノテーションもJavaの命名規則に従います。(i.e. パスカルケース(PascalCase)。Scalaの公式ガイドとは異なるので注意してください)

  final class MyAnnotation extends StaticAnnotation

変数の命名規則(Variable Naming Convention)

原文へのリンク

  • 変数はキャメルケース(camelCase)で、わかりやすい名前をつけます。
  val serverPort = 1000
  val clientPort = 2000
  • 小さなスコープの中であれば1文字で変数を表現しても問題ありません。例えば "i" は単純な(e.g. 10行程度の)ループの添字としてよく使われます。ただし、 "l" (Larryのl) を識別子に使ってはいけません。 "l", "1", "|", "I" を見分けるのが難しいからです。

行の長さ

原文へのリンク

  • 1行につき100文字まで
  • 例外としてimport文やURLは許容してもかまいません。(その場合でも100文字以内に収める努力はしてください)

30の原則(Rule of 30)

原文へのリンク

『一つの要素が30以上のサブ要素で構成されている場合、そこには大きな問題が潜んでいる可能性が高いです』 - Refactoring in Large Software Projects.

一般的に:

  • メソッドは30行以内に収めましょう
  • クラス内のメソッドは30以内に収めましょう

スペースとインデント

原文へのリンク

  • インデントにはスペースを2文字使います。
  if (true) {
    println("Wow!")
  }
  • メソッドの宣言の際、パラメータが1行に収まらない場合はスペースを4文字使ってインデントします。戻り値の型はパラメータと同じ行でも良いですし、改行してスペース2文字でインデントしても良いです。
  def newAPIHadoopFile[K, V, F <: NewInputFormat[K, V]](
      path: String,
      fClass: Class[F],
      kClass: Class[K],
      vClass: Class[V],
      conf: Configuration = hadoopConfiguration): RDD[(K, V)] = {
    // メソッドの中身
  }

  def newAPIHadoopFile[K, V, F <: NewInputFormat[K, V]](
      path: String,
      fClass: Class[F],
      kClass: Class[K],
      vClass: Class[V],
      conf: Configuration = hadoopConfiguration)
    : RDD[(K, V)] = {
    // メソッドの中身
  }
  • classのヘッダ定義が1行に収まらない場合はextend以降を改行してスペース2文字でインデントして、classヘッダの後に空行を1行加えます。
  class Foo(
      val param1: String,  // パラメータはスペース4文字インデント
      val param2: String,
      val param3: Array[Byte])
    extends FooInterface  // ここはスペース2文字インデントとなります
    with Logging {

    def firstMethod(): Unit = { ... }  // この上は空行にします
  }
  • 縦方向にコードの要素を揃えるのは、余計な懸念事項を増やすのでやめましょう。
  // このように縦方向は揃えないでください
  val plus     = "+"
  val minus    = "-"
  val multiply = "*"

  // 以下のように自然に記載してください
  val plus = "+"
  val minus = "-"
  val multiply = "*"

空行(Blank Lines (Vertical Whitespace))

原文へのリンク

  • 下記の場合空行を1行挿入します:
    • クラス内の連続したメンバー(または初期化子)間: クラス、フィールド、コンストラクタ、メソッド、ネストしたクラス、static初期化子、インスタンス初期化子
    • 例外: フィールド間(間に他のコードがないもの)の空行は任意です。論理的にグループ分けをしたい場合に空行を挿入しましょう。
    • メソッド内で論理的なグループ分けをしたい場合
    • クラス内の最初のメンバーの前、あるいは最後のメンバーの後(必須ではなく任意)
  • classの定義間には1〜2行の空行を挿入します。
  • 極端に空行を使いすぎるのはやめましょう。

括弧(Parentheses)

原文へのリンク

  • メソッドは括弧つきで定義します。副作用の無いアクセサメソッドの場合に限り括弧無しでも可。(ある状態を変えることや、I/Oが関連する動作は副作用とみなします)
  class Job {
    // 悪い例: killJobは状態を変化させるので()つきで定義すべき
    def killJob: Unit

    // 良い例:
    def killJob(): Unit
  }
  • 呼び出し側が括弧をつけるかどうかはメソッドの定義に合わせます。(メソッドが括弧付きで定義されていたら括弧付きで呼び出します) これは構文上の話だけでなく、 apply においては処理そのものも変わる可能性があるので注意が必要です。
  class Foo {
    def apply(args: String*): Int
  }

  class Bar {
    def foo: Foo
  }

  new Bar().foo  // これはFooを返します
  new Bar().foo()  // これはIntを返してしまいます!

中括弧(Curly Braces)

原文へのリンク

1行で終わるような条件付き処理やループであっても中括弧で囲みます。ただひとつの例外として、if/elseで3項演算(ternary operator)かつ副作用を伴わない処理を行う場合のみ中括弧を省略できます。

// 良い例:
if (true) {
  println("Wow!")
}

// 良い例:
if (true) statement1 else statement2

// 良い例:
try {
  foo()
} catch {
  ...
}

// 悪い例:
if (true)
  println("Wow!")

// 悪い例:
try foo() catch {
  ...
}

Longリテラル

原文へのリンク

long型リテラルには大文字の L を用います。 l1 の区別がつきにくいからです。

val longValue = 5432L  // 良い例

val longValue = 5432l  // 悪い例

ドキュメントのスタイル

原文へのリンク

ドキュメントにはJavaDocのスタイルを採用します。(ScalaDocのスタイルは使わない)

/** これは1行で短い説明を書く場合の正しい書き方です */

/**
 * これは複数行に跨る場合のJavaDocの正しい書き方で、
 * 2行目に入るときはこのようになりますし、さらに書き続けて
 * 3行目はこのように書きます。
 */

/** Sparkに於いてはScalaDocのスタイルは使いません。なので
  * この書き方は誤っています。
  */

クラス内の要素の順番

原文へのリンク

クラスが肥大化してきた場合は、論理的なグループ分けをして、コメントヘッダを用いて整理します。

class DataFrame {

  ///////////////////////////////////////////////////////////////////////////
  // DataFrame operations
  ///////////////////////////////////////////////////////////////////////////

  ...

  ///////////////////////////////////////////////////////////////////////////
  // RDD operations
  ///////////////////////////////////////////////////////////////////////////

  ...
}

もちろん、これだけクラスが大きくなってしまうのは問題であり、特定のpublic APIを作る場合だけに限定すべきです。

import文

原文へのリンク

  • ワイルドカードを用いたimportを使うのは控えます, ただし、7個以上になる場合や、implicitなメソッドをimportする場合は許容します。ワイルドカードを用いたimportは外部の改修に対して弱くなります。
  • パッケージをimportする場合は必ず絶対パスで行います (e.g. scala.util.Random は正しく util.Random は誤りです)
  • さらに、importは以下の順番で記載します:
    • java.* and javax.*
    • scala.*
    • サードパーティ製のライブラリ (org.*, com.*, etc)
    • プロジェクト内のクラス (com.databricks.* またはSparkを使っているなら org.apache.spark )
  • それぞれのグループ内ではアルファベット順に並べます
  • IntelliJの import organizer を使えば以下の設定で自動的に制御可能です:
  java
  javax
  _______ blank line _______
  scala
  _______ blank line _______
  all other imports
  _______ blank line _______
  com.databricks

パターンマッチ

原文へのリンク

  • パターンマッチのみが実装されたメソッドの場合、可能であれば match をメソッドの宣言と同じ行に記載し、無駄なインデントを1段減らします。
  def test(msg: Message): Unit = msg match {
    case ...
  }
  • closure(または部分関数)を含んだ関数を呼び出す場合でcaseが一つしか無い場合は、caseを関数の呼び出しと同じ行に記載します。
  list.zipWithIndex.map { case (elem, i) =>
    // ...
  }

caseが複数ある場合はインデントを使って中括弧で囲みます

  list.map {
    case a: Foo =>  ...
    case b: Bar =>  ...
  }

インフィックスメソッド(Infix Methods)

原文へのリンク

記号で構成されていないメソッドは インフィックス表記法(infix notation)を避けます (i.e. 演算子オーバーロード)。

// いい例
list.map(func)
string.contains("foo")

// 悪い例
list map (func)
string contains "foo"

// ただし、演算子オーバーロードされているものはインフィックススタイルを使います
arrayBuffer += elem

匿名メソッド(Anonymous Methods)

原文へのリンク

匿名メソッドでは 無駄な中括弧を避けます

// 良い例
list.map { item =>
  ...
}

// 良い例
list.map(item => ...)

// 悪い例
list.map(item => {
  ...
})

// 悪い例
list.map { item => {
  ...
}}

// 悪い例
list.map({ item => ... })

Scala言語の機能

applyメソッド

原文へのリンク

クラスにapplyメソッドを定義するのは避けます。Scalaに詳しくない人にとって読みにくいコードになりますし、IDE(またはgrep)にとってもコードを追いにくくさせます。最悪の場合括弧(Parentheses)で示したように、処理自体が変わってしまうこともあります。

applyメソッドをコンパニオンオブジェクトのファクトリメソッドとして定義するのは問題ありません。この場合applyメソッドはコンパニオンクラスの型を返すようにします。

object TreeNode {
  // これは正しい使い方
  def apply(name: String): TreeNode = ...

  // これはTreeNode型を返していないので誤りです
  def apply(name: String): String = ...
}

override指定子

原文へのリンク
メソッドをオーバーライドする場合やabstractメソッドを実装する場合共に必ずoverride指定子を記載します。Scalaのコンパイラはabstractメソッドを実装する場合に override を必須としませんが、必ず override を明示的に用いるべきです。 override を自明にすることの他に異なるシグネチャによる override の空振りを防ぎます。

trait Parent {
  def hello(data: Map[String, String]): Unit = {
    print(data)
  }
}

class Child extends Parent {
  import scala.collection.Map

  // 以下は親メソッドのParent.helloをオーバーライドしません。
  // なぜなら、パラメータのMapの型が異なるからです。
  // "override"指定子を記載していればコンパイラがこの誤りを指摘することができます
  def hello(data: Map[String, String]): Unit = {
    print("これは親メソッドをオーバーライドするはずでしたが、実際はしません!")
  }
}

構造化代入 (Destructuring Binds)

原文へのリンク

構造化代入(Destructuring bindまたはtuple extractionとも呼ばれる)は一つの式で二つの変数に値を代入する方法です。

val (a, b) = (1, 2)

ただし、コンストラクタで使ってはいけません。特に ab がtransientの場合はScalaのコンパイラが余分なTuple2フィールドを生成してしまいます。

class MyClass {
  // これは正常に動作しません。(a, b)に対してコンパイラはtransientではないTuple2を
  // 生成してしまいます。
  @transient private val (a, b) = someFuncThatReturnsTuple2()
}

名前渡し(call-by-name)

原文へのリンク

名前渡し(call-by-name)を使うのは避けます。 明示的に () => T を使います。

理由: Scalaはメソッドのパラメータをby-nameで定義することができます。 e.g. 次の例は動作します:

def print(value: => Int): Unit = {
  println(value)
  println(value + 1)
}

var a = 0
def inc(): Int = {
  a += 1
  a
}

print(inc())

上のコードで inc()print にクロージャとして渡されてprintメソッド内で2回実行されます。 1print に渡されるわけではありません。名前渡し(call-by-name)の問題点としては、呼び出し側が名前渡し(call-by-namme)と値渡し(call-by-value)の区別をつけることができない点です。よって、式が実行されるかどうか判断がつきません(最悪の場合複数回実行されるかもしれません)。副作用を含む式の場合は特に危険です。

複数の引数リスト(Multiple Parameter Lists)

原文へのリンク

複数の引数リストを使うのは避けます。演算子オーバーロードを複雑化しますし、Scalaに明るくないプログラマを惑わせます。例えば:

// これは避けましょう!
case class Person(name: String, age: Int)(secret: String)

例外として低水準ライブラリを作る際にimplicitを用いる場合は許容します。ただし、implicitも使用を控えるべきです

記号を用いたメソッド (演算子オーバーロード)

原文へのリンク

メソッド名に記号は使いません。ただし算数の演算子(e.g. +, -, *, /)を定義する場合はかまいません。それ以外は絶対に使用してはいけません。記号を用いたメソッド名はそのメソッドの用途を読み取るのが非常に困難になります。以下の例を参考にしてください:

// 記号を用いたメソッド名は意図が理解しにくい
channel ! msg
stream1 >>= stream2

// 記号を用いない方が意図がわかりやすい
channel.send(msg)
stream1.join(stream2)

型推論

原文へのリンク

Scalaの型推論、特に式の左側の推論やクロージャの推論はコードを簡潔にします。よって明示的に型を書く場面は限られています:

  • publicメソッドは明示的に型を記載します。そうしなければコンパイラの推論に度々驚かされることでしょう。
  • implicitメソッドは明示的に型を記載します。そうしなければインクリメンタルコンパイルの際にScalaのコンパイラがクラッシュする可能性があります。
  • 型を人の目で推論し難い変数やクロージャは明示的に型を記載します。コードレビューする人が3秒で型を推論できないものが一つの目安です。

return

原文へのリンク

クロージャ内でreturnを使うのは避けます. return はコンパイラによって scala.runtime.NonLocalReturnControltry/catch に変換され、予期しない動作を引き起こす可能性があります。下記の例をみてください:

  def receive(rpc: WebSocketRPC): Option[Response] = {
    tableFut.onComplete { table =>
      if (table.isFailure) {
        return None // これはやってはいけません!
      } else { ... }
    }
  }

.onComplete は匿名クロージャ { table => ... } を受け取って違うスレッドに渡します。このクロージャはいずれその 違うスレッドで NonLocalReturnControl をスローしてキャッチされます。悲しいことに元々のメソッドには何の影響も起こさないのです。

しかしながら、いくつかの場面で return は推奨されています。

  • guardによる早期returnを行う場合。これによって余分なインデントを避けることができます。
  def doSomething(obj: Any): Any = {
    if (obj eq null) {
      return null
    }
    // 何かしらの処理 ...
  }
  • ループを早期にreturnする場合。これによって状態フラグでの制御が不要になります。
  while (true) {
    if (cond) {
      return
    }
  }

再帰処理(Recursion)と末尾再帰(Tail Recursion)

原文へのリンク

再帰処理の使用は避けます。ただし、再帰処理を行うことが自明(e.g. グラフ走査、木の走査)な場合は許容します。

末尾再帰メソッドであれば @tailrec アノテーションをつけてコンパイラにチェックしてもらいましょう。(クロージャや関数変換の影響で実際は末尾再帰ではないことに頻繁に気付かされることでしょう)

ほとんどのコードは単純なループや状態マシンを使う方が簡単です。末尾再帰(あるいはアキュムレータ)を使うと逆に冗長になったり、読み難くなることがあります。下の例であれば、上の再帰処理版よりも下の手続き型版の方が読みやすいコードであることがわかります:

// 末尾再帰版
def max(data: Array[Int]): Int = {
  @tailrec
  def max0(data: Array[Int], pos: Int, max: Int): Int = {
    if (pos == data.length) {
      max
    } else {
      max0(data, pos + 1, if (data(pos) > max) data(pos) else max)
    }
  }
  max0(data, 0, Int.MinValue)
}

// 手続き型なループ版
def max(data: Array[Int]): Int = {
  var max = Int.MinValue
  for (v <- data) {
    if (v > max) {
      max = v
    }
  }
  max
}

implicit

原文へのリンク

implicitを使うのは避けましょう。ただし、以下の場合は可:
- DSL(ドメイン固有言語)を作っている
- implicit型パラメーターとして使っている(e.g. ClassTag, TypeTag)
- クラス内だけで冗長な型の変換を行っている(e.g. ScalaクロージャからJavaクロージャへ)

implicitを使う場合は、他者がそのimplicitの定義そのものを読まずとも意味が汲み取れることを保証しなければいけません。implicitはとても複雑なルールをもっていて、コードをとてつもなく追いづらく、理解のし難いものにします。TwitterのEffective Scalaによれば、『implicitを使うときには必ずimplicitを使わずに実装できないか自問してください』とあります。

使わざるを得ない場合(e.g. DSLを作る場合)は、implicitメソッドをオーバーロードしてはいけません。必ず違う名前のメソッドを作って、使い手が個別にメソッドをimportしやすいようにします。

// これはやってはいけません。使い手が片方のメソッドだけimportすることができません。
object ImplicitHolder {
  def toRdd(seq: Seq[Int]): RDD[Int] = ...
  def toRdd(seq: Seq[Long]): RDD[Long] = ...
}

// 以下のように定義します:
object ImplicitHolder {
  def intSeqToRdd(seq: Seq[Int]): RDD[Int] = ...
  def longSeqToRdd(seq: Seq[Long]): RDD[Long] = ...
}

例外処理 (Try vs try)

原文へのリンク

  • ThrowableやExceptionはcatchしてはいけません。scala.util.control.NonFatal を使います:
  try {
    ...
  } catch {
    case NonFatal(e) =>
      // 例外ハンドラ; NonFatalはInterruptedExceptionとマッチしないことに注意してください
    case e: InterruptedException =>
      // InterruptedExceptionのハンドラ
  }

このように書くことで NonLocalReturnControl をcatchしないことを保証します。(詳細については return 参照)

  • Try をAPIでは使いません。言い換えるとTryをreturnしてはいけません。その代わり異常処理の場合は明示的に例外をスローしてJavaのようにtry/catchで例外をハンドルします。

上記のようにした背景:Scalaは( Try, Success, Failure を通して)モナドでエラーをハンドルしてアクションをチェーンしやすくすることができます。しかしながら、我々の経験上この方法をとると逆にネストが増えて読み難くなる傾向があります。さらに、期待されるエラー(expected error)と例外とで何が違うのかということがはっきりしておらず、 Try にも述べられていません。よって、 Try をエラーハンドルに使うことを推奨しません。具体的には:

わざとらしい例ですが:

  class UserService {
    /** データベースからユーザの情報を取得 */
    def get(userId: Int): Try[User]
  }

上の例よりは下記のような書き方を推奨します

  class UserService {
    /**
     * データベースからユーザの情報を取得
     * @return ユーザ情報が見つからない場合は何も返しません
     * @throws DatabaseConnectionException データベースに接続できない場合/
     */
    @throws(DatabaseConnectionException)
    def get(userId: Int): Option[User]
  }

2つ目の方が呼び出し側がどのようにエラーをハンドルすべきか明解です。

Option

原文へのリンク

  • 値が空になる可能性がある場合は Option を使います。 null と比較すると Option は明示的に値が None になり得ることを示唆します。
  • Option を作るときには値が null である可能性を踏まえて Some ではなく Option を使います。
  def myMethod1(input: String): Option[String] = Option(transform(input))

  // transformはnullを返す可能性があるのでmyMethod2はSome(null)を返す可能性があります。
  // よって、上の例が推奨されます。
  def myMethod2(input: String): Option[String] = Some(transform(input))
  • Noneを例外の代わりに使ってはいけません。例外は明示的にスローします。
  • Option の中に必ず値が入っていると確信できる場合を除いて getOption に対して使ってはいけません。

モナドによるメソッドチェーン(Monadic Chaining)

原文へのリンク

Scala言語の強みの一つとしてモナドによるメソッドチェーンがあります。ほとんど全てのもの(e.g. collection, Option, Future, Try)がモナドであり、それぞれの処理は繋げる(チェーンする)ことができます。これはとてつもなく強力なコンセプトなのですが、メソッドチェーンはほどほどに抑えておくべきです。具体的には:

  • 4つ以上の処理をチェーン(あるいはネスト)するのは避けましょう
  • ロジックを理解するのに5秒を超えるようであれば、どうしたらメソッドチェーンを使わずに実現できるかよく考えてください。一般的にflatMapやfoldには気をつけましょう。
  • 必ずと言っていいほどflatMapのあとは一度メソッドチェーンを区切るべきです。(型が変わるので)

メソッドチェーンは大抵の場合中間結果を変数に格納して、明示的に型情報を記載し、より手続き型なスタイルで書くことでわかりやすくなります。大げさな例で示すと:

class Person(val data: Map[String, String])
val database = Map[String, Person]
// "address"に"null"を設定することがあります

// モナドによるメソッドチェーンな書き方
def getAddress(name: String): Option[String] = {
  database.get(name).flatMap { elem =>
    elem.data.get("address")
      .flatMap(Option.apply)  // nullを扱う
  }
}

// 行数は増えますが、より可読性の高い書き方
def getAddress(name: String): Option[String] = {
  if (!database.contains(name)) {
    return None
  }

  database(name).data.get("address") match {
    case Some(null) => None  // nullを扱う
    case Some(addr) => Option(addr)
    case None => None
  }
}

同時並行性(Concurrency)

Scala concurrent.Map

原文へのリンク

scala.collection.concurrent.Map よりも java.util.concurrent.ConcurrentHashMap を使うようにしてください。具体的には scala.collection.concurrent.MapgetOrElseUpdate はアトミックではありません (ただしScala 2.11.6でfix済み SI-7943)。我々の手がけているプロジェクトは全てScala 2.10とScala2.11向けにクロスビルドするので scala.collection.concurrent.Map は避けます。

明示的な同期(Synchronization) vs 並列コレクション(Concurrent Collections)

原文へのリンク

状態を安全に並列アクセスするためには以下の3つの方法を推奨します。 これらを混ぜて使ってはいけません。最悪の場合デッドロックに繋がる可能性があります。

  1. java.util.concurrent.ConcurrentHashMap: 状態が全てmap内にキャプチャされていて競合する頻度が高い場合に使います
  private[this] val map = new java.util.concurrent.ConcurrentHashMap[String, String]
  1. java.util.Collections.synchronizedMap: 状態が全てmap内にキャプチャされていて、競合が発生しないはずだが安全なコードにしておきたい場合に使います。競合が発生しない場合はJVM JITコンパイラはsynchronizeのオーバーヘッドをBiased Lockingで取り除きます。
  private[this] val map = java.util.Collections.synchronizedMap(new java.util.HashMap[String, String])
  1. synchronizedを用いた明示的な同期:複数の変数をガードしたいときに使います。2と同様、JVM JITコンパイラはBiased Lockingでsynchronizeのオーバーヘッドを取り除きます。
  class Manager {
    private[this] var count = 0
    private[this] val map = new java.util.HashMap[String, String]
    def update(key: String, value: String): Unit = synchronized {
      map.put(key, value)
      count += 1
    }
    def getCount: Int = synchronized { count }
  }

1.、 2.においてはビューやコレクションのイテレータが同期されている範囲を抜けださないように注意が必要です。これは Map.keySetMap.values をreturnする場合などに発生し得ます。ビューや値を受け渡す必要がある場合はコピーを作るようにしてください。

  val map = java.util.Collections.synchronizedMap(new java.util.HashMap[String, String])

  // 悪い例!
  def values: Iterable[String] = map.values

  // 要素をコピーするようにしてください
  def values: Iterable[String] = map.synchronized { Seq(map.values: _*) }

Explicit Synchronization vs Atomic Variables vs @volatile

原文へのリンク

The java.util.concurrent.atomic package provides primitives for lock-free access to primitive types, such as AtomicBoolean, AtomicInteger, and AtomicReference.

Always prefer Atomic variables over @volatile. They have a strict superset of the functionality and are more visible in code. Atomic variables are implemented using @volatile under the hood.

Prefer Atomic variables over explicit synchronization when: (1) all critical updates for an object are confined to a single variable and contention is expected. Atomic variables are lock-free and permit more efficient contention. Or (2) synchronization is clearly expressed as a getAndSet operation. For example:

  // good: clearly and efficiently express only-once execution of concurrent code
  val initialized = new AtomicBoolean(false)
  ...
  if (!initialized.getAndSet(true)) {
    ...
  }

  // poor: less clear what is guarded by synchronization, may unnecessarily synchronize
  val initialized = false
  ...
  var wasInitialized = false
  synchronized {
    wasInitialized = initialized
    initialized = true
  }
  if (!wasInitialized) {
    ...
  }

privateフィールド(Private Fields)

原文へのリンク

private フィールドは同じクラスの別のインスタンスであればアクセス可能であることに注意しなければいけません。故に this.synchronized (あるいは単純に synchronized) では競合を防ぐには十分ではありません。代わりに private[this] を使いましょう。

// 以下は十分に安全ではありません
class Foo {
  private var count: Int = 0
  def inc(): Unit = synchronized { count += 1 }
}

// 以下であれば安全です
class Foo {
  private[this] var count: Int = 0
  def inc(): Unit = synchronized { count += 1 }
}

分離性(Isolation)

原文へのリンク

一般的に並行性(concurrency)や同期(synchronization)に関わるロジックは可能な限り分離されているべきです。言い換えると:

  • APIやユーザーが使う可能性のあるメソッド、またはコールバックにおいて同期プリミティブ(synchronization primitive)を表に出すのは避けましょう。
  • 複雑なモジュールに於いては、小さなインナーモジュールを作って、その中で並行性プリミティブ(concurrency primitive)を閉じ込めておくようにしましょう。

パフォーマンス

原文へのリンク

あなたの書くコードのほとんどはパフォーマンスのことをあまり意識しなくても大丈夫なはずです。ただし、パフォーマンスを意識しなければならない場合は以下のTIPSを参考にしてください:

マイクロベンチマーク

原文へのリンク

ScalaコンパイラとJVM JITコンパイラは裏側で様々なことをやっているのでマイクロベンチマークを書くのはとてつもなく難しいです。大抵の場合、書き上げたマイクロベンチマークのコードはあなたが意図したものを正常に計測できていません。

マイクロベンチマークコードを書くならばjmhを使ってください。必ずすべてのマイクロベンチマークのサンプルに目を通してください。デッドコードを排除、定数の畳み込み(constant folding)、ループアンローリング(loop unrolling)した場合の効果について理解が深まります。

走査(traversal)とzipWithIndex

原文へのリンク

for ループや関数型のトランスフォーム関数(e.g. map, foreach)よりも while ループを使いましょう。 for ループや関数型のトランスフォーム関数は処理が遅いです(仮想関数(virtual function)の呼び出しやボックス化が要因)。


val arr = // intの配列
// 添字が偶数の場合に0埋め
val newArr = list.zipWithIndex.map { case (elem, i) =>
  if (i % 2 == 0) 0 else elem
}

// 以下は上の処理の高性能版
val newArr = new Array[Int](arr.length)
var i = 0
val len = newArr.length
while (i < len) {
  newArr(i) = if (i % 2 == 0) 0 else arr(i)
  i += 1
}

Optionとnull

原文へのリンク

パフォーマンスを意識する必要がある場合は Option よりも null を使って仮想関数(virtual method)呼び出しやボックス化が発生するのを防ぎましょう。nullになり得るフィールドに関してはNullableで装飾しておきましょう。

class Foo {
  @javax.annotation.Nullable
  private[this] var nullableField: Bar = _
}

Scalaコレクションライブラリ

原文へのリンク

パフォーマンスを意識する必要がある場合はScalaよりもJavaのコレクションライブラリを使いましょう。Scalaのコレクションライブラリの方がJavaのそれよりパフォーマンスが劣るからです。

private[this]

原文へのリンク

パフォーマンスを意識する必要がある場合は private よりも private[this] を使いましょう。 private[this] はフィールドを生成しますが private はアクセサメソッドを生成してしまうからです。我々の経験上JVM JITコンパイラは必ずしも private フィールドのアクセサメソッドをインライン化するとは限らないので private[this] を使って仮想関数の呼び出しが発生しないように実装する方が安全です。

class MyClass {
  private val field1 = ...
  private[this] val field2 = ...

  def perfSensitiveMethod(): Unit = {
    var i = 0
    while (i < 1000000) {
      field1  // これは仮想関数を呼び出す可能性があります
      field2  // これはただのフィールドアクセスになります
      i += 1
    }
  }
}

Javaとの互換性

原文へのリンク

このセクションではJavaとの互換をもつAPIを作る際のガイドラインについて述べます。Javaとの互換性が必要ない場合はこれらを適用する必要はありません。この内容のほとんどは我々がSparkのJava APIを作った際の経験から築かれたものです。

JavaにはあってScalaには無い機能

原文へのリンク

以下に述べるJavaの機能はScalaにはありません。これらが必要な場合はJavaで定義してください。ただし、Javaのファイルに関してはScalaDocsは生成されないので注意してください。

  • staticフィールド
  • static内部クラス(inner class)
  • Java enum
  • アノテーション

トレイト(trait)と抽象(abstract)クラス

原文へのリンク

For interfaces that can be implemented externally, keep in mind the following:

  • デフォルト実装を持ったトレイトのメソッドはJavaでは使えません。代わりに抽象クラスを使いましょう。
  • 一般的にトレイトを使うのは避けてください。ただし、未来永劫そのインタフェースがデフォルト実装されない確信がある場合は許容します。
// トレイトのデフォルト実装はJavaでは動きません
trait Listener {
  def onTermination(): Unit = { ... }
}

// 以下はJavaでも動きます
abstract class Listener {
  def onTermination(): Unit = { ... }
}

型エイリアス(Type Aliases)

原文へのリンク

型エイリアスは使ってはいけません。バイトコード上(Java含む)でこれらは見えません。

デフォルト引数(Default Parameter Values)

原文へのリンク

デフォルト引数を使ってはいけません。代わりにオーバーロードしましょう。

// 以下はJavaとの互換性を崩します
def sample(ratio: Double, withReplacement: Boolean = false): RDD[T] = { ... }

// 以下2つは問題ありません
def sample(ratio: Double, withReplacement: Boolean): RDD[T] = { ... }
def sample(ratio: Double): RDD[T] = sample(ratio, withReplacement = false)

複数の引数リスト(Multiple Parameter List)

原文へのリンク

複数の引数リストは使ってはいけません。

可変長引数(vararg)

原文へのリンク

  • 可変長引数(vararg)メソッドには @scala.annotation.varargs アノテーションを適用して、Javaからも使えるようにしましょう。ScalaコンパイラはScala用(バイトコード上はパラメータをSeqとして)とJava用(バイトコード上はパラメータを配列として)の2つのメソッドを生成します。
    scala
    @scala.annotation.varargs
    def select(exprs: Expression*): DataFrame = { ... }

  • Scalaコンパイラのバグ(SI-1459, SI-9013)によりabstractな可変長引数(vararg)メソッドはJavaでは動きません。

  • 可変長引数(vararg)メソッドをオーバーロードする場合は注意が必要です。可変長引数メソッドを違う型の可変長引数メソッドでオーバーロードした場合はソース互換性が無くなる可能性があります。

  class Database {
    @scala.annotation.varargs
    def remove(elems: String*): Unit = ...

    // 以下のオーバーロードを定義すると引数無しのremove()に対するソース互換性が無くなります
    @scala.annotation.varargs
    def remove(elems: People*): Unit = ...
  }

  // 以下は曖昧さ故コンパイルできなくなります
  new Database().remove()

対処法として、第一引数を明示的に定義して、第二引数以降を可変長引数(vararg)にします:

  class Database {
    @scala.annotation.varargs
    def remove(elems: String*): Unit = ...

    // 以下はOK
    @scala.annotation.varargs
    def remove(elem: People, elems: People*): Unit = ...
  }

implicit

原文へのリンク

implicitをクラスやメソッドで使ってはいけません。 ClassTagTypeTag も含みます。

class JavaFriendlyAPI {
  // このメソッドはimplicitパラメータ(ClassTag)を含んでいるのでJavaから見た場合に良いメソッドではありません
  def convertTo[T: ClassTag](): T
}

コンパニオンオブジェクト、staticメソッド、フィールド

原文へのリンク

コンパニオンオブジェクト、staticメソッド、フィールドを扱うにいい当たってはいくつかのことに注意しなければいけません。

  • コンパニオンオブジェクトをJavaで扱う場合は不自然な形になります( Foo というコンパニオンオブジェクトがあったらそれは Foo$ クラスの中の Foo$ 型の MODULE$ というstaticフィールドになります)。
  object Foo

  // Javaでは以下のようになります
  public class Foo$ {
    Foo$ MODULE$ = // オブジェクトをインスタンス化
  }

コンパニオンオブジェクトをどうしても使わないといけない場合はJavaのstaticフィールドの別のクラスで用意してください

  • 残念ながらScalaでJVMのstaticフィールドを定義する方法はありません。Javaのファイルを作って定義するしかありません。
  • コンパニオンオブジェクトのメソッドは自動的にコンパニオンクラスのstaticメソッドに変換されます。ただし、メソッド名で競合が起きた場合はこの限りではありません。staticメソッドの生成を(将来も見据えて)保証したい場合はJavaで書いたテストファイルを作ってその中でstaticメソッドを呼ぶことです。
  class Foo {
    def method2(): Unit = { ... }
  }

  object Foo {
    def method1(): Unit = { ... }  // staticメソッド Foo.method1がバイトコード上で生成されます
    def method2(): Unit = { ... }  // staticメソッド Foo.method2はバイトコード上で生成されません
  }

  // FooJavaTest.java (test/scala/com/databricks/...で作ったと想定)
  public class FooJavaTest {
    public static compileTest() {
      Foo.method1();  // コンパイルは問題なく通ります
      Foo.method2();  // これはmethod2が生成されていないので失敗するはずです
    }
  }
  • caseオブジェクト(あるいはただのコンパニオンオブジェクト)MyClassというものは実際はMyClass型ではありません。
  case object MyClass

  // Test.java
  if (MyClass$.MODULE instanceof MyClass) {
    // 上の式は必ずfalseになります
  }

型の階層を正しく実装したい場合はコンパニオンクラスを定義して、caseオブジェクトにそのクラスを継承させましょう:

  class MyClass
  case object MyClass extends MyClass

その他

currentTimeMillisよりもnanoTime推奨

原文へのリンク

期間 の演算や タイムアウト のチェックをする場合は System.currentTimeMillis() を使うのは控えましょう。サブミリ秒精度を必要としない場合でも System.nanoTime() を使いましょう。

System.currentTimeMillis() は現在時刻を返し、システム時計の時刻が変わった場合には影響を受けます。よって、システム時計の時間を戻した場合にはタイムアウトが長時間(時間が元々の値に戻るまで)「ハング」してしまう可能性があります 。この現象はネットワークが切れてしまった後にntpdが step を実行した場合に発生し得ます。よくある例として、システムのブート時にDHCPが普段よりも時間がかかるというものがあります。これは異常処理を誘発し、意味を理解するのも困難であれば再現性も低い問題となります。 System.nanoTime() の値はシステム時間に関係なく増えることしか無いことが保証されています。

注意:
- 絶対に nanoTime() の絶対値はシリアライズしてはいけませんし、他のシステムに渡してもいけません。絶対値はシステム特有の値であり、リブート時にはリセットされるので意味がありません。
- nanoTime() の絶対値は正数であることは保証されていません(ただし、 t2 - t1は正しい値を算出していることを保証されています。
- nanoTime() は292年という期間を行き来します。なので、Sparkのジョブが長時間かかる場合は考えなおす必要があります。

URLよりもURI推奨

原文へのリンク

あるサービスのURLを格納するときには URI を使いましょう。

URL等価性チェック(equality check) には実際にIPアドレスの解決を行うために(ブロックを引き起こす)ネットワークへの通信が行われます。 URI はフィールドの等価性チェックを行い、機能としては URL のスーパーセットとなります。

37
39
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
37
39