Scala初心者向け Optionの扱い方ステップアップ!


自分の経験に基づいてOptionを扱う様々な方法を書いてみます。

Scala初心者の人は参考になるかも?(筆者は執筆時点でScala歴半年程度です。なので初心者向け)

サンプルコードとして「str変数の中身または”なし”という文字列を画面上に出力する」ということを考えてみます。 ※一部のコードは”なし”という文字列は出力しません。

 

Level 0 「Optionの存在を知らない」

JavaプログラマがScalaを始めてすぐにやるかもしれません。ちなみに自分は見たことあります。初めてJavaコードをScalaに移植したときに書いたこともあります。

object Main extends App {
  val str = null

  if (str != null) {
    println(str)
  } else {
    println("なし")
  }
}

Level 1 「Optionの存在を知るが、まだ良くわかっていない」

「お、isDefinedで判定できるらしいぞ」

間違ってはいないんですけどね。

object Main extends App {
  val str: Option[String] = None

  if (str.isDefined) {
    println(str)
  } else {
    println("なし")
  }
}

 Level 2 「matchを知る」

「なんかScalaはmatchとかいうのを使うらしい。isDefiendとはいったい」「matchの方が柔軟性があるらしい?」

サンプルでよくありますね。しょっぱなLevel 2になる人は多いかな。

object Main extends App {
  val str: Option[String] = None

  str match {
    case Some(x) => println(x)
    case None => println("なし")
  }
}

 Level 3 「getOrElseを知る」

「すごい、getOrElseで良いじゃん!超簡潔だよね!」

今回のサンプルコードだとこれが最適解と言っても過言ではないかも。けどまだ先はあります。

object Main extends App {
  val str: Option[String] = None

  println(str.getOrElse("なし"))
}

Level 4 「Option型のクラス構造を知る」

Optionが親クラスでSomeとNoneが子クラスです。

Optionはabstractなので、Optionのインスタンスを作ることは出来ません。ただし、Optionのコンパニオンオブジェクトにapplyが用意されているのでOption(“foo”)などとすることでSomeを作ることができます。※Optionを作ってる訳じゃないよ

Someを作るならばSome(“foo”)でも良いので、「Option()とSome()は何が違うんだろう?」と思いますが、これはコードを見るとすぐに分かります。

https://github.com/scala/scala/blob/2.11.x/src/library/scala/Option.scala

Option()の方は引数がnullの場合はNoneに変換してくれますが、Some()の場合は何もしません。つまりSome(null)という状態で保持します。

argSome(arg)の場合の結果Option(arg)の場合の結果
"foo"Some("foo")Some("foo")
nullSome(null)None

じゃあどう使い分ければいいの?となりますが、ほとんどのScalaコードではnullは登場しないので、基本Some()で良いと思います。ただし、Java libraryをScalaでラップする場合は頻繁にnullを扱うと思います。そういうシーンでは(nullだったら勝手にNoneにして欲しいと思うことが多々あるので)Option()を使えば良いかと思います。

(細かいところなのでどっちでも良いですが、この仕様を知っている人はOption()と書かれていた場合、引数にnullが来る可能性を考慮するかもしれませんね。なので個人的にはnullが来ない普通のScalaコードならSome推奨です)

どうやら単純にJavaライブラリのラップならOption、それ以外はSomeという分けには行かないようです。コメントで以下のように教えて頂きました。

@xuwei_k「subtypingによる問題を避けるために、nullがこない箇所だとしてもSome(_)ではなくOption(_)使うのはよくあります」

どういうことだ・・・?調べよう!だけどわかんない・・・と思ってたら丁寧にgakuzzzzさんが教えてくれました。

なるほど・・・!

foldLeftは第一引数に初期値を、第二引数に関数を取るものですが、第一引数がSomeの場合、型推論によって型パラメータは「Someだ」と判断される訳だけど、このとき第二引数に指定している関数はOptionを取るので「型が合ってないよ!」となると・・・。

一見、OptionはSomeの親クラスだから良いじゃん!とも思えますが、このケースはOptionを指定しているのではなくOptionを引数に取る関数なので親子関係にはない・・・と。

難しいですね・・・、ほんとにLevel 4かこれ。。。

Level 4をまとめると、Option(_)とSome(_)は挙動が違っている。どちらを使うかはケース次第。JavaライブラリをラップするシーンでもOption(_)することは普通にあるよ、という感じでしょうか。基本Someで上手く行かない部分とJavaライブラリ周辺だけOptionという感じでも良いかも?

Level 5 「mapやforeachを知る」

「まじかよ!mapとかforeachってリスト専用の関数じゃなかったの?Optionでも使えたんだ・・・」

Optionでもコレクション型と同じ関数を色々と使えることを知って感心する人は多いですね。自分も驚き、そして感心しました。

以下のforeachだとあまり嬉しくありませんが、Optionに対するmapとかはよく使います。

object Main extends App {
  val str: Option[String] = None

  str.foreach(x => println(x))
}

Level 5.1(番外編) 「不要な変数バインドに気づく」

「そういうのもあるのか」

多くのサンプルがそうなっているからなのか必ずバインド用の変数を書いちゃう人が居ますが(上記の場合 x =>となっている部分)、実は不要です。_で良いです。

str.foreach(println(_))

もっと言うとforeachとかこれ系の関数は実引数として関数オブジェクトを求めているのでこれで良いです。

str.foreach(println)

Level 6 「for式を知る」

「forってループの固定観念があったんだけど、こう書けるとは!」「for式とか不要でmapやforeachで良いと思ってたんだけど、for式便利じゃん」

今回のサンプルだとあんまり恩恵を感じられませんね。とりあえずOptionでもforで回せるということを覚えて、後は複数Optionがあったときにどう書くかを考えれば良いのではないでしょうか。

object Main extends App {
  val str: Option[String] = None

  for (
    s <- str
  ) {
    println(s)
  }

}

Optionとforを組み合わせたときにどう恩恵があるかはこちらにまとめました。

宜しければ参考にしてください。

mapやforeachだけじゃなくてfor式も使おう

いかがでしたでしょうか。

まだLevel 7や8がありそうな気もしますが、自分自身がScala上級者ではないのでたどり着けていません。何かアイデアある方はコメントか@s_tsukaまで教えてください!