93
71

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

第14章:ScalaのOption型とnullを語る

Posted at

Option型って知っているかい?これマジ凄いよ!

NullPointerException、通称 ヌルポ
少し込み入ったJavaアプリを作って動かすと、大抵発生するよね。

nullチェックを入れてたり、規約で縛ったりして対応することになると思います。
だけど、コンパイル時に見つけることができれば、、、と思ったことない?

Option型を上手く使えれば、コンパイル時にnullチェックみたいなことができてしまうんだ。
とても素敵だよね!

では、Option型について語ってみます。

Optionとは

値があるかないかを表す型です。箱って言っても良いかもですね。
Optionは更に2種類のサブクラスを持っている。

  • Some
  • None

Some

値があることを表す型。値を持っている。

None

値がないことを表す型。

Optionを使ってみる

淡白に説明してみたので、早速使ってみよう!

Optionを取得する

Mapを作って、getしてみるよ!
getメソッドはOptionを返すんだ。

scala> val map = Map(1 -> "Moses", 2 -> "Lucas", 3 -> "Henderson", 5 -> null)
map: scala.collection.immutable.Map[Int,String] = Map(1 -> Moses, 2 -> Lucas, 3 -> Henderson, 5 -> null)

scala> map get 1
res0: Option[String] = Some(Moses)

scala> map get 3
res1: Option[String] = Some(Henderson)

scala> map get 4
res2: Option[String] = None

getメソッドに13を渡すとSomeが返って、値を持っていることがわかるね。
一方4を渡すと、該当する値は存在しないからNoneが返ってるね。

値がnull

では、getメソッドに5を渡すとどうなるかな?
試してみよう!

scala> map get 5
res3: Option[String] = Some(null)

どう?
予想した通りになったかな?

Someが返ってきているから、値が存在するということだね。
ただ、その値自体がnullってことなんだ。

つまり、

  • Mapにキーが存在している場合はSomeを返し、
  • Mapにキーが存在しない場合はNoneを返す

ってことだね。

そして、キーに該当する値がnullの場合も、もちろんあるってこと。

nullを扱うのが嫌だという場合は、例えば以下を意識してみよう。

  • 可能な限り変数をイミュータブルにする
  • テーブルのカラムはNot null制約使う

とは言ってもJavaとは切れない関係だから、Javaのライブラリを使ってしまうと
どうしてもnullが混入してしまうね。

Optionから値を取り出す

値があることがわかれば、実際の値を欲しくなるよね?
というか取り出さないと意味ないよね。

取り出すのは簡単だよ。Optionのgetメソッドを使うだけだ。

さっきはMapのgetメソッドを使ったんだけど、ここら辺は
ごっちゃにならないように気をつけてね!

scala> val person = map get 2
person: Option[String] = Some(Lucas)

scala> person.get
res3: String = Lucas

Noneの場合もやってみよう。

scala> val nonePerson = map get 4
nonePerson: Option[String] = None

scala> nonePerson.get
java.util.NoSuchElementException: None.get
	at scala.None$.get(Option.scala:313)
	at scala.None$.get(Option.scala:311)
	at .<init>(<console>:10)
	at .<clinit>(<console>)
	at .<init>(<console>:7)
	at .<clinit>(<console>)
	at $print(<console>)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:606)
	at scala.tools.nsc.interpreter.IMain$ReadEvalPrint.call(IMain.scala:734)
	at scala.tools.nsc.interpreter.IMain$Request.loadAndRun(IMain.scala:983)
	at scala.tools.nsc.interpreter.IMain.loadAndRunReq$1(IMain.scala:573)
	at scala.tools.nsc.interpreter.IMain.interpret(IMain.scala:604)
	at scala.tools.nsc.interpreter.IMain.interpret(IMain.scala:568)
	at scala.tools.nsc.interpreter.ILoop.reallyInterpret$1(ILoop.scala:756)
	at scala.tools.nsc.interpreter.ILoop.interpretStartingWith(ILoop.scala:801)
	at scala.tools.nsc.interpreter.ILoop.command(ILoop.scala:713)
	at scala.tools.nsc.interpreter.ILoop.processLine$1(ILoop.scala:577)
	at scala.tools.nsc.interpreter.ILoop.innerLoop$1(ILoop.scala:584)
	at scala.tools.nsc.interpreter.ILoop.loop(ILoop.scala:587)
	at scala.tools.nsc.interpreter.ILoop$$anonfun$process$1.apply$mcZ$sp(ILoop.scala:878)
	at scala.tools.nsc.interpreter.ILoop$$anonfun$process$1.apply(ILoop.scala:833)
	at scala.tools.nsc.interpreter.ILoop$$anonfun$process$1.apply(ILoop.scala:833)
	at scala.tools.nsc.util.ScalaClassLoader$.savingContextLoader(ScalaClassLoader.scala:135)
	at scala.tools.nsc.interpreter.ILoop.process(ILoop.scala:833)
	at scala.tools.nsc.MainGenericRunner.runTarget$1(MainGenericRunner.scala:83)
	at scala.tools.nsc.MainGenericRunner.process(MainGenericRunner.scala:96)
	at scala.tools.nsc.MainGenericRunner$.main(MainGenericRunner.scala:105)
	at scala.tools.nsc.MainGenericRunner.main(MainGenericRunner.scala)

かなり悲惨なことになってしまった。。。
こうなると、SomeかNoneの判定は必須だね。

パターンマッチ

Noneのgetメソッドを使うとExceptionが発生することがわかったので、
判定が必須になりました。
判定はどうする?ifキーワード使っちゃう?

まだ語っていないんだけど、 パターンマッチ が便利だ。
パターンマッチを使うと読みやすいし、強力な機能も使えるんだけど
詳しい説明は別の章で。

今回は紹介がてらソースコードを載せるね。

Option1.scala
object Option1 {

  def main(args: Array[String]): Unit = {
    val map = Map(1 -> "Moses", 2 -> "Lucas", 3 -> "Henderson")

    def check(o: Option[String]) {
      o match {
        case Some(s) => println(s)
        case None => println("Not exist.")
      }
    }

    val some = map get (2)
    val none = map get (4)

    check(some)
    check(none)
  }

}

実行してみよう。

$ scalac Option1.scala
$ scala Option1
Lucas
Not exist.

となりました。

コンパイル時のチェック

冒頭でちょろっと述べたけど、 コンパイル時のnullチェックみたいなものを説明します。

ここまで読んでくれていれば、勘の鋭い人なら気づいているかもしれない。
そうです、nullチェックとは言いつつNoneチェックであり、
パターンマッチで実現できるのです。

もちろんSomeチェックでもあるんだけど、
大抵の場合忘れるのは、何かが存在しない場合の方なので。

早速見てみましょう。

Option2.scala
object Option2 {

  def main(args: Array[String]): Unit = {
    val map = Map(1 -> "Moses", 2 -> "Lucas", 3 -> "Henderson")

      def check(o: Option[String]) {
        o match {
          case Some(s) => println(s)
        }
      }

    val some = map get (2)

    check(some)
  }

}

コンパイルしてみると

$ scalac Option2.scala
Option2.scala:7: warning: match may not be exhaustive.
It would fail on the following input: None
        o match {
        ^
one warning found

こんなの出ちゃいました。

これでNone忘れを回避できるようになるね!!

ちなみにEclipseで試してみたところ、
ソースコードの左に黄色のwarningを出してくれていました。

Nullオブジェクトパターン

ここは蛇足ではあるんだけど。

Nullオブジェクトパターンって知っているかな?
Option型を知った時、このパターンが頭に浮かんできました。

Nullオブジェクトパターンとは

あるインタフェースに対して、空実装をした具象クラス。
つまりどのメソッドも何も処理をしない。

正常系のロジックを通すことが可能となる。

なので以下が可能となる。

  • ヌルポ撲滅
  • nullチェックの分岐撲滅

OptionもNullオブジェクトパターンに近い考え方なのではないかと
感じております。

まとめ

Option型はどうだった?

ヌルポをコンパイル時にチェックできるようになる仕組みって
凄くないですか?

あと
Optionのgetは使わないで、パターンマッチを使おう!

今回も
体で感じてくれたかな?

93
71
1

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
93
71

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?