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メソッドに1
、3
を渡すと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
キーワード使っちゃう?
まだ語っていないんだけど、 パターンマッチ が便利だ。
パターンマッチを使うと読みやすいし、強力な機能も使えるんだけど
詳しい説明は別の章で。
今回は紹介がてらソースコードを載せるね。
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チェックでもあるんだけど、
大抵の場合忘れるのは、何かが存在しない場合の方なので。
早速見てみましょう。
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は使わないで、パターンマッチを使おう!
今回も
体で感じてくれたかな?