まえがき
- NimでOption型を使ってnull安全に実装する方法を書きます
Nimで参照型を扱うときnull安全でない
Nimでは値型のデータはプリミティブ型もObject型もゼロ値で初期化されます。
以下のコードは初期化していませんが、ゼロ値で初期化されているため、NullPointerExceptionは起きません。
var n: int
echo n
var s: string
echo s
type
Obj = object
n: int
var o: Obj
echo o
$ nim c -r a_1.nim
0
(n: 0)
ですが、Objectを参照型として宣言したとき、NullPointerExceptionが起こりえます。
以下のコードはNullPointerExceptionになります。
type
Obj = ref object
n: int
var o: Obj
echo o.n
$ nim c -r a_2.nim
Traceback (most recent call last)
/tmp/a_2.nim(6) a_2
SIGSEGV: Illegal storage access. (Attempt to read from nil?)
Error: execution of an external program failed: '/tmp/a_2 '
よって、参照型のデータを扱う際は、そのデータがnullか否かを意識する必要があります。
Option型でnullを意識させる
JavaではNullになる型表現するときに、Optinal (java.util.Optional)型を使用します。
嬉しいことに、Nimにもこれと同様のOption型が存在します。
Option型とは
Option型がラップしている型が、値を保つ場合と持たない場合を表現する型です。
nilになり得る型を表現するのによく使います。
ライブラリのドキュメントは以下です。
Option型でラップされることで、ラップされている型に直接アクセスできなくなり、値を取り出すためのプロシージャ呼び出しを強制されます。
このことにより、「Option型でラップされているから、この型はnilチェックが必須なんだな」ということを開発者に伝えることができます。
また、内部的にnilチェックを必ず行ってくれるプロシージャを介してデータにアクセスできるため、
nilチェック忘れによる意図しないNullPointerExceptionを未然に防ぐことが可能です。
実際に使ってみます。
天気API呼び出しを例にした実装例
API呼び出し時点の天気情報を返すAPIが存在するとします。
このAPI呼び出しをラップした外部ライブラリのプロシージャが存在し、戻り値としてWeather Objectを返すとします。
プログラムでは、このプロシージャを使用し、天気情報を整えて標準出力するものとします。
以下のような実装です。
type
Weather = ref object
temperature: int # 気温
humidity: int # 湿度
weather: string # 天気
proc fetchWeather(): Weather =
## APIを利用して天気情報を取得する。
return Weather(temperature: 20, humidity: 30, weather: "晴れ")
proc print(w: Weather) =
echo "今日の天気は " & $w.weather & " です"
echo "気温は " & $w.temperature & "度 です"
echo "湿度は " & $w.humidity & "% です"
let w = fetchWeather()
print(w)
上記プログラムを実行すると、以下の結果が得られます。
今日の天気は 晴れ です
気温は 20度 です
湿度は 30% です
ですが、このプロシージャはイケてなくて、
API呼び出しに失敗してもエラーを返さず、プロシージャ内でログ出力だけして、戻り値にnilを返すような作りになっているものとします。
type
Weather = ref object
temperature: int # 気温
humidity: int # 湿度
weather: string # 天気
proc fetchWeather(): Weather =
## APIを利用して天気情報を取得する。
discard # 参照型はnilのままで初期化されない
proc print(w: Weather) =
echo "今日の天気は " & $w.weather & " です"
echo "気温は " & $w.temperature & "度 です"
echo "湿度は " & $w.humidity & "% です"
let w = fetchWeather()
print(w)
このプログラムを実行すると、以下のようにnilアクセスでクラッシュします。
Traceback (most recent call last)
/tmp/opt_2.nim(19) opt_2
/tmp/opt_2.nim(14) print
SIGSEGV: Illegal storage access. (Attempt to read from nil?)
プロシージャは外部ライブラリで、実装に手を入れることはできません。どうするべきか?
以下のように修正することで、安全になります。
import options
type
Weather = ref object
temperature: int # 気温
humidity: int # 湿度
weather: string # 天気
proc fetchWeather(): Weather =
## APIを利用して天気情報を取得する。
discard
proc print(w: Weather) =
echo "今日の天気は " & $w.weather & " です"
echo "気温は " & $w.temperature & "度 です"
echo "湿度は " & $w.humidity & "% です"
proc safeFetchWeather(): Option[Weather] =
## APIがnilを返すのでラップする。
let w = fetchWeather()
return option(w)
let w = safeFetchWeather()
w.map(print)
fetchWeatherをラップしたsafeFetchWeatherというプロシージャを新しく追加しました。
このプロシージャ内でもとのAPIを呼び出して、Optionでラップしています。
開発者はsafeFetchWeatherのみを使用することで、直接Weatherオブジェクトにアクセスできなくなりました。
Optionでラップされているオブジェクトにアクセスするにはoptionsモジュールが提供する専用のプロシージャを介する必要があります。
上記のコードではoptionsモジュールが提供するmapプロシージャを介してデータにアクセスしています。
Option型にアクセスするプロシージャ
map
引数にプロシージャを渡すことで、ラップされているデータがnilでない時だけプロシージャを実行してくれます。
import options
type
Weather = ref object
temperature: int # 気温
humidity: int # 湿度
weather: string # 天気
proc print(w: Weather) =
echo "今日の天気は " & $w.weather & " です"
var w = option(Weather(weather: "曇り"))
echo "== nilでないケース =="
w.map(print)
w = none(Weather)
echo "== nilなケース =="
w.map(print)
上記プログラムを実行すると、以下の結果を出力します。
nilのケースでprintプロシージャが実行されなくなりました。
== nilでないケース ==
今日の天気は 曇り です
== nilなケース ==
また、無名プロシージャを定義することで、以下のように実装することも可能です。
import options
type
Weather = ref object
temperature: int # 気温
humidity: int # 湿度
weather: string # 天気
var w = option(Weather(weather: "曇り"))
echo "== nilでないケース =="
w.map(proc(v: Weather) =
echo "今日の天気は " & $v.weather & "です"
)
== nilでないケース ==
今日の天気は 曇りです
mapと聞くと関数適用のmap, filter, reduceのmapを想像する方もいると思います。
Nimではsequtilsが、map, filterなどのプロシージャを実装しています。
これらは引数を別の値や型に変換する用途で使われるものですが、
optionsモジュールのmapも値や型の変更に使用できます。
import options
type
Weather = ref object
temperature: int # 気温
humidity: int # 湿度
weather: string # 天気
proc isCloudy(w: Weather): bool =
w.weather == "曇り"
var w = option(Weather(weather: "曇り"))
echo "== nilでないケース =="
echo w.map(isCloudy)
== nilでないケース ==
Some(true)
同じOption型なのでメソッドチェーンも安全に行えます。
import options
type
Weather = ref object
temperature: int # 気温
humidity: int # 湿度
weather: string # 天気
proc isCloudy(w: Weather): bool =
w.weather == "曇り"
var w = option(Weather(weather: "曇り"))
echo "== nilでないケース =="
w.map(isCloudy).map(proc(b: bool) =
echo "b = " & $b
)
== nilでないケース ==
b = true
filter
引数に、戻り値がboolのプロシージャを渡すことで、ラップされているデータがtrueの時にSome型に、falseの時にNone型に変換します。None型のときはラップされているデータにアクセスされなくなります。
import options
type
Weather = ref object
temperature: int # 気温
humidity: int # 湿度
weather: string # 天気
proc isCloudy(w: Weather): bool =
w.weather == "曇り"
var w = option(Weather(weather: "曇り"))
echo "== nilでないケース =="
w.filter(isCloudy).map(proc(v: Weather) =
echo "今日の天気は " & v.weather & "です"
)
echo "== nilなケース =="
w = option(Weather(weather: "晴れ"))
w.filter(isCloudy).map(proc(v: Weather) =
echo "今日の天気は " & v.weather & "です"
)
== nilでないケース ==
今日の天気は 曇りです
== nilなケース ==
晴れの時はisCloudyがtrueにならないため、プロシージャが実行されなくなりました。
get と isSome, isNone ※
最後に、一番原始的なgetと、getを呼び出しても良いかを判別するisSomeとisNoneです。
前述のmapやfilterも内部ではisSomeとgetを使用しています。
以下の様に使用します。
import options
type
Weather = ref object
temperature: int # 気温
humidity: int # 湿度
weather: string # 天気
proc isCloudy(w: Weather): bool =
w.weather == "曇り"
var w = option(Weather(weather: "曇り"))
echo "== nilでないケース =="
if w.isSome():
echo w.get().weather
echo "== nilなケース =="
w = none(Weather)
if w.isSome():
echo w.get().weather
echo "== nilなケース =="
if w.isNone():
echo "データが空です"
== nilでないケース ==
曇り
== nilなケース ==
== nilなケース ==
データが空です
nilな場合はisSomeがfalseになるため、echoが実行されていないことがわかります。
逆に、isNoneの方がtrueになります。
ですが、こちらの方法ではなるべく実装しないほうが良いでしょう。
以下のように、nilチェックを忘れた場合は普通にクラッシュします。
import options
type
Weather = ref object
temperature: int # 気温
humidity: int # 湿度
weather: string # 天気
proc isCloudy(w: Weather): bool =
w.weather == "曇り"
var w = option(Weather(weather: "曇り"))
echo "== nilでないケース =="
if w.isSome():
echo w.get().weather
echo "== nilなケース =="
w = none(Weather)
# if w.isSome():
echo w.get().weather # <-- ※
== nilでないケース ==
曇り
== nilなケース ==
/usercode/in.nim(21) in
/playground/nim/lib/pure/options.nim(215) get
Error: unhandled exception: Can't obtain a value from anone[UnpackDefect]
他にもflattenやflatMap, ==というプロシージャがあるのですが、良いサンプルが思いつかなかったので割愛します。
なにはともあれOption型とそのプロシージャのおかげで安全にデータにアクセスできるようになりました。
めでたしめでたし。
エラーを握りつぶすプロシージャがそもそも悪いが
まとめ
- NimでOption型の実装例を書いた
- ちゃんとNilチェックしよう
- Optionライブラリのドキュメントは options - Nim を参照
以上です