null安全の話

技術,プログラミング言語

最近、何の気なしにGo言語を学んでみて、これ結構好きかもしれないと感じています。このGo言語は新しめのプログラミング言語なのに、同じような新しめのモダンなプログラミング言語には当然のようにあるものがなかったりします。それが例えばnull安全です。

ないことで欠けているかのように捉えられがちですが、ちゃんとポリシーを持って割り切っています。他にもクラスがなかったりとか、言語仕様を最低限に絞ったシンプルさが好みに合っているようです。(個人の意見です)

null絶許言語

null絶許とは、nullなんか絶対許さない、殺すという思想のことです。まあ、Javaプログラマなら誰でも遭遇するヌルポ=ガッとか、C言語ならSEGVとか、nullに纏わるトラブルや苦労話は枚挙にいとまないところです。そういうあたりから、ツンデレではなくツンドラなnull絶許思想が生まれたとかなんとか(おそらく違う)。

nullかnilか

どうでもよいですが、言語によってnullだったりnilだったりするのはなんでなのか。最近の言語はnil が多いという勝手なイメージがあります。単に言い換えだけじゃなくて、nullとnilが同居する言語もありますし。

あと、nullについてはヌルなのかナルなのかという問題もあったような。日本ではヌルポ=ガッの流れからしてヌルが多いのでしょうか。それを含めると読み方としては、ヌル、ナル、ニルが併存するというカオス。でも、null安全とかnull許容という文脈ではほとんどnullですね。

入力した文字列を出力

null安全な言語はいくつかあると思いますが、これも最近学んだのでSwiftを引き合いに出します。

Swiftにおけるnull安全の詳細をここで語っていると長くなるので、別途、解説記事を探して見てください(丸投げ)。

ポイントだけ書くと、例えば文字列を格納するString 型の変数を定義した場合、そこにnullを入れることはできません。nullを入れたい場合、Optional型というのを使い、それを表すために「String?」の様に型名の後ろに?をつけます。ただ、null安全な言語はnullを絶許なので、そのままではOptional型の変数を扱えないので、アンラップという処理が必要です。

例えば、標準入力から1行読み取り、それを"Hello,XXX!"みたいに表示する簡単なプログラムを考えます。

サンプルプログラムなんかではこういうのが平気で使われていますが、この3行目の最後の!が強制アンラップというものです。ここはnullが来ないから、そういうつもりで扱ってね、という記述になります。なぜアンラプかと言うと、Optional型を出力してみれば分かるのですが、Optionalであるとラッピングされているので、それを剥くからアンラップかな、と考えております。

readLine()という関数は標準入力から読み取った文字列を返す関数ですが、エラーが発生した場合にはnil を返します。

サンプルだとエラーチェックなんてあまり気にしないので上のような気持ち悪いコードが出てきますが、これを実行してCTRL-Dとかで入力を閉じてしまうとreadLineはエラーを返すので以下のように実行時例外が発生します。

nullpo/nullpo.swift:3: Fatal error: Unexpectedly found nil while unwrapping an Optional value

意訳すると「nil来ないって言ってたのに、nil来たじゃねーか、ふざけんな」というエラーを吐いています。

まあ、サンプルなんでエラーチェックに力入れてないし、仕方ないよねってところではあります。が、null安全、null絶許って言っても、結局は書く人が手を抜けば危険なことには変わりありません。

Swift使いじゃないのでどう書くべきかは自信がないですが、こんな感じでしょうか。

2つの数値の和を出力

競技プログラミングなんかでよくあるスペース区切りで2つの数字を入力させて、その計算結果を出力するものだと、こんなサンプルが出てきます。

!がいっぱいで、びっくりです。これは数値ではなくて文字を入れたり、数値を1つしか入れない時点でエラーで落ちます。競技プログラミングは数値がくるというところでは絶対数値がくる性善説の世界なのでこれでよいのですが、職業プログラマは人(ユーザー)を見たら泥棒と思え…とは言いませんが、何するかわからない危ない人と思うので、性悪説が必須です。

これを直すとこんな感じでしょうか。ちょっと冗長過ぎますが、すみません。ちなみに、型名はいちいち書かないでもいい感じで処理してくれますが、一応書いています。

書き方が悪いという指摘を受けそうですが、null絶許言語でアンラップを書くとこんなことになってしまうわけです。もっとシンプルに書けたらすみません。なお、関数にするともう少しシンプルに書けます。ついでに足し算の部分はループにしました。これだと3個以上数値を書いても全部足してしまいますけど。

競技プログラミングのお約束の世界では、使用で数値が空白区切りで2つとなっていれば、絶対そういうデータが与えられるので、何も考えなくてよいのですが。

Go言語では

入力した文字列を出力

では、Go言語では。

サンプルにありがちなコードだとこんな感じでしょうか。なお、入力を閉じてもGo言語版はエラーにはなりません。nameを宣言した時点で値は初期化されているので、文字列なら空文字列になっています。ですので、何もしないで入力を閉じてもエラーにならなかったりします。

なりませんが、ちゃんとエラー処理をします。Go言語の場合、関数は多値を返すことができます。Scan関数の場合、第1引数が読み取った数、第2引数がエラーを示すerror型の変数です。これがnilならエラーは発生していないし、nil以外ならエラーです。

Swift版と違ってif … else … の形式にしていないのは、Go言語のお作法として、エラーチェックしたらすぐにリターンしてしまうのを推奨しているからです。

そうすることで、その先の部分で問題がないことが分かりやすくする意図があるそうです。上の様な簡単なソースではあまり関係ありませんが、最初のif文を過ぎた以上はnameには何か意味のあるものが入っているのが保証されるので、以後はチェックは必要ないという考え方です。実際、ifを多重にネストしたコードなんて見たくありません。

2つの数値の和を出力

Go言語のScan関数は最初から空白区切りの入力を分解してくれるので、その分スッキリ書けます。

と言うか、Scan関数が高機能すぎて、他に書く部分がなくなってしまいました。Scan関数の場合は値を2個入れるまで待ってくれるので、"1 2″でも"1″改行、”2”改行でも処理してしまいます。3個目は入らないし、1個目に文字を入れた時点でエラーになります。そういう点ではあまり公平な比較になりませんでした。

というわけなので、SwiftのreadLineに相当するbufioライブラリを使います。

18〜21行目のエラーチェックは無くてもあまり問題ない様です。30〜32行目のチェックは1行に2つの数値しか計算しないために入れているものなので、スペース区切りで書かれたものを全部足して良いのであれば不要です。1個しか数値を指定していない場合でも許すのであれば22〜26行目のチェック部分も不要です。

標準ライブラリの関数は大抵最後の返り値でerror型を返すので、それを受け取ってエラーチェックをするというのを覚えておけば問題ありません。
例えば、29〜33行目の数値変換のチェック部分を以下の様に書いてしまうと、コンパイラがエラーを出します。

エラーが出るからと、第2返り値を受け取って、そのままチェックしない以下の様なコードを書いてもコンパイルがエラーにします。

いちいちコンパイルしないとエラーが分からないのは不便かと思うかもしれませんが、Go言語にはいろいろなlintツールがあるのでそれをかければ良いし、VSCodeで開発していれば随時チェックして赤や黄色の波下線を付けて教えてくれます。そこにマウスカーソルを持っていけば細かいことをポップアップで確認できます。

VSCodeでの警告

結局、null絶許言語でアンラップ処理を加えた場合と大して変わらない?そうなんですよね。

null安全の考え方は素晴らしいと思うし、実際、ソースを書いている時点で未然に問題に気づかせてくれるとは思います。ただ、そのために文法とか複雑になって面倒な様だと本末転倒に感じるので、自分はGo言語のアプローチが気に入っています。

ただ、確かに実行時に落ちてしまうプログラムが書けてしまいます。しかし、それはnull安全のプログラミング言語でも適切に書かなければ同じことです。それを文法で支援するのがnull安全な言語であって、機能で支援するのがGo言語かと思います。アプローチの違いでどちらが正しいとかでもないので、あとは好みの問題でしょうか。

何が落ちて、何が落ちないか

Go言語で何が落ちて何が落ちないのかは色々試してみました。

メソッド

Fooという構造体に対して、似たようなメソッドを3つ定義して呼び出しています。

Hello1はFooという構造体に対してメソッドを定義していますが、mainではFooへのポインタ変数を作成したのみで実体を作っていませんので、その場でランタイムエラーです。

Hello2は1と似ていますが、Foo構造体へのポインタに対してメソッドを定義しています。なので、これ自体は何の問題もなく呼び出せます。実体を作成していない構造体へのポインタに対して定義したメソッドを呼び出すことができるというのも不思議ですが、静的メソッドみたいな感じでしょうか。

Hello3は2と似ていますが、構造体のプロパティにアクセスしています。ですので、呼び出しはできますが、その中でプロパティにアクセスしようとした時点でランタイムエラーです。なお、Nameプロパティを参照する部分が"f.Name"じゃなくて"(*f).Name"じゃないかというツッコミが入りそうですが、Go言語は後者の代わりに前者で書くことが可能です。もちろん、後者で書いても構いません。

golangci-lintを通してみましたが、これらについては特に警告は出ませんでした。プログラマが注意する必要があります。この辺りは、null安全肯定派からはそら見たことかとツッコまれそうなところです。

エラーの返し方

Go言語では関数のエラーを最後の引数がnilかどうかで判断しますが、エラーがない時にこれにアクセスしようとすると、当然ランタイムエラーです。

これもツッコミが入りそうですが、Go言語のお作法としてはとにかくチェックして、以上なら抜けろというのがあるので、こんな書き方をするのが悪いという当然の結果になります。

そうすると以下の様に書きたくなるのですが、これはエラーになります。

sumはifのブロックで定義されているので、ifの中では使えますが、その外では使えません。ですので、以下の様に書かないといけないです。当たり前ですが、ちょっと残念な気もします。もちろん、elseブロックを作れば行けますが、そうするとGo言語のお作法から外れますので。

addがエラー返すことがあるのか?というツッコミは無しでお願いします。

Go言語のnull安全

冒頭部分でGo言語にnull安全の考え方がないと書いてしまっていますが、全くないわけではないので念のため。例えば、変数は全て宣言時に初期化されてます。ここでは、nullは許容する範囲以外での存在を絶対許さないといったnullに対する怨念がないという程度の意味になります。

変更履歴

2022/2/21
ソースコードをGistに置いたものに置き換えました。

特に必要ないとは思いますが、ソースコードはいかに置いてあります。

MyGists/blog/tech-null-safty at main · woinary/MyGists (github.com)

2022-02-21技術null,プログラミング言語

Posted by woinary