LoginSignup
14
6

More than 3 years have passed since last update.

NimでOption型を使ってnull安全に実装する

Last updated at Posted at 2020-05-23

まえがき

  • NimでOption型を使ってnull安全に実装する方法を書きます

Nimで参照型を扱うときnull安全でない

Nimでは値型のデータはプリミティブ型もObject型もゼロ値で初期化されます。

以下のコードは初期化していませんが、ゼロ値で初期化されているため、NullPointerExceptionは起きません。

a_1.nim
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になります。

a_2.nim
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を返すとします。

プログラムでは、このプロシージャを使用し、天気情報を整えて標準出力するものとします。

以下のような実装です。

opt_1.nim
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を返すような作りになっているものとします。

opt_2.nim
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?)

プロシージャは外部ライブラリで、実装に手を入れることはできません。どうするべきか?

以下のように修正することで、安全になります。

opt_3.nim
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なケース ==

晴れの時はisCloudytrueにならないため、プロシージャが実行されなくなりました。

get と isSome, isNone ※

最後に、一番原始的なgetと、getを呼び出しても良いかを判別するisSomeisNoneです。

前述のmapfilter内部では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な場合はisSomefalseになるため、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 a none [UnpackDefect]

他にもflattenflatMap, ==というプロシージャがあるのですが、良いサンプルが思いつかなかったので割愛します。

なにはともあれOption型とそのプロシージャのおかげで安全にデータにアクセスできるようになりました。

めでたしめでたし。
エラーを握りつぶすプロシージャがそもそも悪いが

まとめ

  • NimでOption型の実装例を書いた
  • ちゃんとNilチェックしよう
  • Optionライブラリのドキュメントは options - Nim を参照

以上です

14
6
2

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
14
6