まえがき
- 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 を参照
以上です