pexels-photo-3441746.jpeg

[初級-中級向け]Scala基本APIを完全に理解するシリーズ① -Option編-

 
0
このエントリーをはてなブックマークに追加
Kazuki Moriyama
Kazuki Moriyama (森山 和樹)

はじめに

Scalaの基本ライブラリをユースケース付きで解説します。
まずはOptionです。

使用頻度・重要度

メソッドが多すぎするとどれが重要なのか分かりづらいので各メソッドの横にランク付けをしておきます。

☆☆☆: 非常によく使う。Scalaを書くなら必須レベル
☆☆: 使いどこでは威力を発揮する。これを使いこなすかどうかで、コードの綺麗さが変わる。
☆: あまり使わない。使いたいときにどうぞ。場合によっては使用しないほうがいいことも。

Option objectメソッド

apply ☆☆☆

コンストラクタです。
引数がnullの時はNone、null以外でSomeが返ります。

scala> Option("a")
res0: Option[String] = Some(a)

scala> Option(null)
res1: Option[Null] = None

scala> Some(null)
res2: Some[Null] = Some(null)

scala> Some(null).isEmpty
res3: Boolean = false
// nullなのに空じゃないというやばい挙動なので、こんなやばいことはしてはいけない

empty ☆

Noneを作るためのコンストラクタです。
Listのから配列作成メソッドemptyと同じシグネチャであることが意識されています。
別に普通にNoneをベタ書きすればいいと思います。

scala> Option.empty
res0: Option[Nothing] = None

// 似てる
scala> List.empty
res1: List[Nothing] = List()

// これでいい
scala> None
res2: None.type = None

Option classメソッド

map ☆☆☆

みんな大好きmap。
Someのときだけ中身を変換したいときによく使います。
mapとどれだけ仲良くなれるかが勝負!
実はfor式でも同じことが書けます。

scala> Some("1").map(_.toInt)
res0: Option[Int] = Some(1)

// 上は下のfor式と同じ
scala> for {
     |   o <- Some("1")
     | } yield o.toInt
res1: Option[Int] = Some(1)

scala> val a: Option[Int] = None
a: Option[Int] = None

scala> a.map(_.toInt)
res2: Option[Int] = None

flatten ☆☆

ネストしたOptionを一剥がしてくれます。
Optionが返るような処理がネストしてしまった場合に役に立つことがあります。
またSomeとNoneでネストしていた場合にはNoneとなります。
flatMapが使える状況ではflatMapを使うようにしましょう。

scala> Some(Some("a")).flatten
res0: Option[String] = Some(a)

scala> Some(Some(Some("a"))).flatten
res1: Option[Some[String]] = Some(Some(a))
// 一つしか剥がれない

scala> Some(None).flatten
res2: Option[Nothing] = None

// これよりも
scala> Some(List(1)).map(l => l.headOption).flatten
res3: Option[Int] = Some(1)

// こっちのほうがいい
scala> Some(List(1)).flatMap(l => l.headOption)
res4: Option[Int] = Some(1)

flatMap ☆☆☆

flatten+map。
mapの中でOptionが返る場合にはそれをflattenでまとめてくれます。
更に、flatMapの糖衣構文であるfor式を使うことで非常に見通しの良いコードを書くことができます。

scala> Some(Seq(1)).flatMap(_.headOption.map(_ * 2))
res0: Option[Int] = Some(2)

// 上のやつはforを使うと下のように書ける
// Optionが一つづつ剥がれて中の値を直接いじることができる
// `<-`はflatMapとmapの別名
scala> for {
     |   seq <- Some(Seq(1)) // ここの<-はflatMap
     |   v <- seq.headOption // ここの<-はmap
     | } yield v * 2
res5: Option[Int] = Some(2)

foreach ☆

Someのときだけなにかしたいときに生やします。
ただしmapと違って返る値はUnitなので戻り値を再利用などはできません。
Scalaは副作用(簡単に言えばUnitが返るような処理)を嫌うため、あまり活躍する場面はありません。

scala> Some("1").foreach(println)
1

scala> None.foreach(println)
// 何も表示されない

scala> Some("1").map(_.toInt).map(_ * 2)
res2: Option[Int] = Some(2)
// mapは返ってきた値を再利用できるが...

scala> Some("1").foreach(println).map(_ * 2)
<console>:12: error: value map is not a member of Unit
       Some("1").foreach(println).map(_ * 2)
                                  ^
// foreachはUnitが返るのでコンパイルエラー

isEmpty/isDefined(nonEmpty) ☆☆

SomeかNoneを判定します。
nonEmptyとisDefinedは等価です。
条件分岐系は殆どmapで対応できますが、これらのメソッドを使用したif文のほうが戻り値の制約がなく

柔軟性が高いです。
使わなくてもいいときは使わないほうがいいです。

scala> val a = Option("1")
a: Option[String] = Some(1)

scala> a.map(_.toInt)
res0: Option[Int] = Some(1)
// mapは変換は得意だけど...

scala> if (a.isDefined) a else println("no")
res1: Any = Some(1)
// こういう操作はできない

get ☆

Someのときに中の値を取り出します。
逆にNoneのときは例外を吐きます。
バグの温床なのでプロダクションコードでは使わないようにしましょう。
めんどくさくてもmapなどで変換し続けるのが実行時エラーも起きずに安全です。
テストなど失敗しても別にいいときにだけ使用しましょう。

scala> Some("a").get
res0: String = a

scala> None.get
java.util.NoSuchElementException: None.get
  at scala.None$.get(Option.scala:366)
  ... 36 elided
// 例外が出てやばい

getOrElse ☆☆

Someのときに中の値を取り出します。
Noneのときは設定したデフォルト値が返り、例外が起きないのでgetよりは安全です。
ただ、Optionであるべき部分にまで使用するような乱用はOptionの意味を失わせるので乱用は避けましょう。
デフォルト値というものが意味を持つ場合に、理由を持って使用するとコードがきれいになる場合があります。

// こういうのは
scala> Option("a") match {
     |   case Some(v) => v
     |   case None => "b"
     | }
res0: String = a

// こうかける
scala> Some("a").getOrElse("b")
res1: String = a

scala> None.getOrElse("b")
res2: String = b

orElse ☆☆

自分自身がNoneであった場合に設定されたOptionを返却するようにします。
値の有無によって別の値を返却するような時があれば使用できるかも知れません。

// こういうのは
scala> Option(1) match {
     |   case None => Some(2)
     |   case v => v
     | }
res0: Option[Int] = Some(1)

// こうかけちゃう
scala> Option(1).orElse(Some(2))
res0: Option[Int] = Some(1)

scala> None.orElse(Some(2))
res1: Option[Int] = Some(2)

orNull ☆

Someのときに中の値を、Noneのときにnullを返します。
そもそもOptionが存在するScalaにおいてはnullをそのまま扱うのはよろしくないため使うのはやめましょう。

scala> Some("a").orNull
res0: String = a

// nullが表に出てきてよくない
scala> None.orNull
res1: Null = null

forAll ☆☆

Noneまたは引数で設定したテストが成功したときにtrueを返します。
Someかつテストが失敗したときにfalseが返ります。
Noneでtrueは非常に集合論っぽいです。[1]
数学っぽく表現すると、任意のSomeに対してp
活用できるときに使用するとかっちょよく書けます。

scala> Some("a").forall(_ == "a")
res0: Boolean = true

scala> None.forall(_ == "a")
res1: Boolean = true

scala> Some("a").forall(_ == "b")
res2: Boolean = false

exists ☆☆

Someかつテストが成功したときにtrueを返します。
逆にNoneまたはテストが失敗したときにfalseを返します。
数学っぽく言うと、あるpなるSomeがあって

scala> Some("a").exists(_ == "a")
res0: Boolean = true

scala> None.exists(_ == "a")
res1: Boolean = false

scala> Some("a").exists(_ == "b")
res2: Boolean = false

collect ☆☆

中身の値によって処理を振り分けたいときに使用します。
NoneはNoneがそのまま返ります。
caseの羅列(正確にはpartial function)を渡すことによって実現します。
mapなどだとif文の羅列になりそうな部分をcaseでシュッと書けてかっこいいです。

// こういうmapが
scala> Some(1).map { i =>
     |   if (i % 2 == 0)
     |     i * 2
     |   else if ( i % 3 == 0)
     |     i * 3
     |   else
     |     i * 5
     | }
res0: Option[Int] = Some(5)

// こうなってスッキリ
scala> Some(1).collect {
     |   case i if i % 2 == 0 => i * 2
     |   case i if i % 3 == 0 => i * 3
     |   case i => i * 5
     | }
res1: Option[Int] = Some(5)

contains ☆☆

中の値に対しての検証を行います。
Noneの場合には問答無用でfalseです。

// こういう処理や

scala> Some(1) == Some(1)
res0: Boolean = true

// こういう処理が
scala> Some(1) == Some(2)
res1: Boolean = false

// こう書ける
scala> Some(1).contains(1)
res2: Boolean = true

withFilter ☆☆

Optionにフィルターをかけた状態でmapなどの操作を行えます。
withFilterは連結可能なので複数のフィルターをかけることができます。

scala> Some(1).withFilter(_ == 0).map(_ * 2)
res0: Option[Int] = None

scala> Some(1).withFilter(_ == 1).map(_ * 2)
res1: Option[Int] = Some(2)

// 複数連結可能
scala> Some(1).withFilter(_ == 1).withFilter(_ > 0).map(_ * 2)
res2: Option[Int] = Some(2)

filter/filterNot ☆☆

filterはSomeの中身に条件を適用して、通ったらそのままSome、だめならNoneを返します。
filterNotは条件が通らなかったらSome。
どちらもNoneはNone。

// こういうのが
scala> if (a.get == 1) a else None
res0: Option[Int] = Some(1)

// こうかける
scala> a.filter(_ == 1)
res1: Option[Int] = Some(1)

fold ☆☆☆

SomeかNoneどうかで処理を振り分けるときに使用します。
match caseでこういう事をすることがよくあるが、そういうのが一発で駆逐できます。
個人的には非常によく使用するメソッドの一つです。

// こういうのが
scala> Option(1) match {
     |   case Some(i) => print("some")
     |   case None => print("none")
     | }
some

// こう書ける
scala> Option(1).fold(print("none"))(_ => print("some"))
some

toRight/toLeft ☆☆

OptionをEitherに変換します。
toRightはSomeをRightに変換し、Noneの場合はデフォルト値を使います。
toLeftはNoneをLeftに変換し、Someの場合はデフォルト値を使います。
たまに使う。

scala> Some(1).toRight(2)
res0: Either[Int,Int] = Right(1)

scala> None.toRight(2)
res1: Either[Int,Nothing] = Left(2)

scala> Some(1).toLeft(2)
res2: Either[Int,Int] = Left(1)

scala> None.toLeft(2)
res3: Either[Nothing,Int] = Right(2)

toList ☆☆

Someの場合は中の値一つだけのList、Noneは空Listを返します。
あまり使わないがOption objectに生えてるListとのimplicit conversionメソッドと併用すれば魔術を使うことができます。
普通は同じクラスに対してじゃないとflatMapやfalttenはネストできませんが、OptionはListにコンバージョンされるためネストさせることができます。
しかし一見何が起きているか分かりづらいので下のようなこと早めたほうがいいでしょう。

scala> import scala.util.Try
import scala.util.Try

// 普通は同じクラスじゃないとflattenできない
scala> List(Try(1)).flatten
<console>:13: error: No implicit view available from scala.util.Try[Int] => scala.collection.GenTraversableOnce[B].
       List(Try(1)).flatten
                    ^

// OptionとListならできちゃう
scala> List(Option(1)).flatten
res1: List[Int] = List(1)

// flatMapでもいけるのでこんなことも可能
scala> for {
     |   l <- List(1)
     |   o <- Option(2)
     | } yield l + o
res2: List[Int] = List(3)

終わりに

Optionを完全に理解できたでしょうか。
Scalaは難解というイメージを持たれがちですが、実はシンプルで強力なAPIを備えた言語です。
ただし、APIがどうシンプル・強力なのかは実装者の実力に任せられているため最初はどうしても壁があるように感じます。
この記事がその壁を超える補助になれば幸いです。

個人的には☆☆☆のメソッドは絶対にマスターし、あとは☆☆のメソッドをどれだけ使いこなせるかが肝になる気がします。

これで君も今日からScalaマスター!

脚注
  1. 詳しくは「空集合 常に真」でググってください ↩︎

info-outline

お知らせ

K.DEVは株式会社KDOTにより運営されています。記事の内容や会社でのITに関わる一般的なご相談に専門の社員がお答えしております。ぜひお気軽にご連絡ください。