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

# まえがき

* 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
```

```bash
$ 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
```

```bash
$ 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になり得る型を表現するのによく使います。

ライブラリのドキュメントは以下です。

* [options - Nim](https://nim-lang.org/docs/options.html)

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)
```

上記プログラムを実行すると、以下の結果が得られます。

```bash
今日の天気は 晴れ です
気温は 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アクセスでクラッシュします。

```bash
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でない時だけプロシージャを実行してくれます。

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


また、無名プロシージャを定義することで、以下のように実装することも可能です。

```nim
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](https://nim-lang.org/docs/sequtils.html)が、map, filterなどのプロシージャを実装しています。
これらは引数を別の値や型に変換する用途で使われるものですが、
optionsモジュールのmapも[値や型の変更に使用できます](https://nim-lang.org/docs/options.html#map%2COption%5BT%5D%2Cproc%28T%29_2)。

```nim
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型なのでメソッドチェーンも安全に行えます。

```nim
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`型のときはラップされているデータにアクセスされなくなります。

```nim
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を使用しています](https://github.com/nim-lang/Nim/blob/version-1-4/lib/pure/options.nim#L242)。
以下の様に使用します。

```nim
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`チェックを忘れた場合は普通にクラッシュします。

```nim
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]

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

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

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

# まとめ

* NimでOption型の実装例を書いた
* ちゃんとNilチェックしよう
* Optionライブラリのドキュメントは [options - Nim](https://nim-lang.org/docs/options.html) を参照

以上です