ぬるぬるした物を噛み砕く

せいんと(@773_Arihara)です。

新年度がいよいよ始まりますね。

1年目新人プログラマーの成果発表みたいなノリで書いてみました。

nullnullした物を噛み砕く

キーワード

  • null安全
  • null許容型
  • Optional

null安全な言語でのnullの扱い方について、ざっくり噛み砕いて記します。

この記事では、こういうパターンの時に使えそう、という例を挙げてみます。

Swiftに馴染みが無い方に向けて色々な事をつらつら書き並べた所為で、前半は本題から大きく外れています。ゴメンなさい。

もっと詳しく知りたい方は、諸先輩方の素晴らしくまとめられた記事を後半に載せていますので、そちらをご覧ください。

※この記事では、PythonNone、Swiftのnilをnullと呼称しています。

前座

普通の動きが分かって頂ければOKです。

要件

以下に示す、CHARACTER表があります。

id name
1 モニカ
2 サヨリ
3 ナツキ
4 ユリ

任意の整数を与えると、CHARACTER表から該当するidnameを返却するような関数を作成してください。

ただし、与えた値がCHARACTER表のidに存在しない場合、「モニカ」を返却してください。

上記関数を用いて、それぞれのキャラクター名(name)を以下の形式で表示してください。

nameが取得されました。

SQLなら

CHARACTER表からの取得は下記のようなSQL文で定義出来そうです。

SELECT name FROM CHARACTER WHERE id = 1

引数でidを渡したいのであれば、最後の1をプレースホルダに変えれば良いと思います。

ただし、これだけだと該当するidが無い場合の要件が満たせていないので、なんとかしなければいけません。

下記サイトを参考に作成しました。

SELECT name FROM CHARACTER WHERE id = 1
UNION ALL
SELECT 'モニカ' WHERE NOT EXISTSSELECT name FROM CHARACTER WHERE id = 1

PostgleSQLで動作確認を行いましたが、問題ないようです。

プレースホルダーの件は上記同様。

もっと上手い方法があるかもしれませんが、これ以上は脱線するのでやめておきます。思いついた方はコメントください。

実装

DB接続は面倒なので、今回はやりません。ごめんなさい。

Pythonの例

#idからnameを取得する関数
def get_name(id: int) -> str:
    if id == 1:
        return "モニカ"
    elif id == 2:
        return "サヨリ"
    elif id == 3:
        return "ナツキ"
    elif id == 4:
        return "ユリ"
    else:
        return "モニカ"

#関数を呼び出して名前を表示
print(get_name(1) + "が取得されました。")
print(get_name(2) + "が取得されました。")
print(get_name(3) + "が取得されました。")
print(get_name(4) + "が取得されました。")
print(get_name(5) + "が取得されました。")

get_name関数は、DB接続を行って該当idnameを取得する物だと考えて頂けるとありがたいです。

関数の引数にはint型、戻り値にはstr型とするように明示しています。

ただし、これはIDE上の補完機能が効くのみで、型を制限する物ではないそうです。

試しに、

print(get_name(2.5) + "が取得されました。")

と変えても、コンパイルエラーとはならず、「モニカ」が取得されます。

引数を整数のみに制限するには、何かしら工夫が必要だと思われます。

ちなみに本プログラムの、実行結果は以下の通りです。

モニカが取得されました。
サヨリが取得されました。
ナツキが取得されました。
ユリが取得されました。
モニカが取得されました。

上手く動いているようですね。

Swiftの例

//idからnameを取得する関数
func getName(id: Int) -> String {
    switch id {
    case 1:
        return "モニカ"
    case 2:
        return "サヨリ"
    case 3:
        return "ナツキ"
    case 4:
        return "ユリ"
    default:
        return "モニカ"
    }
}

//関数を呼び出して名前を表示
print(getName(id: 1) + "が取得されました。")
print(getName(id: 2) + "が取得されました。")
print(getName(id: 3) + "が取得されました。")
print(getName(id: 4) + "が取得されました。")
print(getName(id: 5) + "が取得されました。")

まず関数名がPythonと異なっていますが、命名規則に基づいた物です。

こっちのキャメルケースの方が個人的には馴染み深いです。

そしてPythonとは対照的に、関数の引数及び戻り値の型制限が掛かります。

Pythonの場合と同様に、

print(getName(id: 2.5) + "が取得されました。")

と変更すると、こちらはコンパイルエラーとなります。

Cannot convert value of type 'Double' to expected argument type 'Int'

噛み砕くと、小数値(Double)は整数値(Int)に変換出来ませんよ、というエラーです。

また、Pythonでは使用出来なかった、switch文がSwiftでは使用可能です。(くどい等価演算子が避けられるくらいのメリットしかないかも。)

そしてそして、関数の呼び方も少し違っています。

getName(id: 1) 

引数にラベル名が必須となっています。

個人的には、視認性の向上に繋がり、良いと思います。

ただ、いちいちラベル名を残したくないという意見もあります。(そもそも、Swift2.xまでは省略出来ていた)

ラベル名を省略させる為には、関数定義の際に以下のようにアンダースコア(_)を付けて、明示する必要があります。

func getName(_ id: Int) -> String {

このようにすれば、以下のように関数呼び出しが行えます。

getName(1) 

ちなみに、明示を行わないまま、上記の形式で関数呼び出しを行うと、コンパイルエラーとなります。

Missing argument label 'id:' in call

大分脱線しましたが、実行結果はPythonの時と同じです。

モニカが取得されました。
サヨリが取得されました。
ナツキが取得されました。
ユリが取得されました。
モニカが取得されました。

本題

やっとnullが出てきます。

要件その2

CHARACTER表から サヨリ」を消去します

ただし、消すのはレコードではなく、nameのみです。

多分忘れてるだけなので。

id name
1 モニカ
2 null
3 ナツキ
4 ユリ

この状態で、以前と同様の要件を満たすように実装してください。

実装

何も変えないでも行けるやろ〜と思うと痛い目に。

Pythonの例

CHARACTER表の状態をコードに反映させます。

#idからnameを取得する関数
def get_name(id: int) -> str:
    if id == 1:
        return "モニカ"
    elif id == 2:
        return None
    elif id == 3:
        return "ナツキ"
    elif id == 4:
        return "ユリ"
    else:
        return "モニカ"

#関数を呼び出して名前を表示
print(get_name(1) + "が取得されました。")
print(get_name(2) + "が取得されました。")
print(get_name(3) + "が取得されました。")
print(get_name(4) + "が取得されました。")
print(get_name(5) + "が取得されました。")

サヨリNone(≒null)に変更しただけで、処理は一切変更していません。

実行してみましょう。

モニカが取得されました。
Traceback (most recent call last): File "test.py", line 14, in print(get_name(2) + "が取得されました。") TypeError: unsupported operand type(s) for +: 'NoneType' and 'str'

id=1のモニカは取得出来ているようですが、nullに変更したid=2の取得でエラーとなっているようです。

それ以降のnameについても取得されず、強制終了してしまっています。

エラー内容を噛み砕くと、nullstr+で結べません、といった感じです。

+は文字列同士を連結させる為に使用していましたが、nullが入ってしまうとNGという事です。

つまり、nullでない場合のみ処理を実行するように制限すれば良いのです。

if  get_name(2) is not None:
    print(get_name(2) + "が取得されました。")

プログラミング経験者の方なら、そんなの当たり前じゃないか!と思う方もいるかも知れません。

ただ、このnullチェックを忘れてしまうケースが多々あるかと思います。

例に示したコードでは、コンパイルエラーとはならず、エラーとなる箇所を実行して初めて、過ちに気がつきます

このような小規模なプログラムならすぐにエラーが発生する事がわかりますが、大規模なプログラムになると、リリースしてから数ヶ月後、数年後に発覚する事なんかもあったりします。

なんとも悲しい。

Swiftの例

Pythonの場合と同様に、サヨリnil(≒null)に変更します。

//idからnameを取得する関数
func getName(id: Int) -> String {
    switch id {
    case 1:
        return "モニカ"
    case 2:
        return nil
    case 3:
        return "ナツキ"
    case 4:
        return "ユリ"
    default:
        return "モニカ"
    }
}

//関数を呼び出して名前を表示
print(getName(id: 1) + "が取得されました。")
print(getName(id: 2) + "が取得されました。")
print(getName(id: 3) + "が取得されました。")
print(getName(id: 4) + "が取得されました。")
print(getName(id: 5) + "が取得されました。")

これはなんと、コンパイルエラーになります

'nil' is incompatible with return type 'String'

噛み砕くと、nullは定義した返却値の型Stringと矛盾してますよ、という意味です。

一体どういう事なのか、見ていきましょう。

そもそも、SwiftではString型にnullを代入する事が出来ません。

もっと言うと、特別な事をしない限り、全ての型にnullを代入する事が出来ません。

以下は、いずれもコンパイルエラーとなる例です。

var str: String = nil
var char: Character = nil
var arr: Array<String> = nil
var some = nil

最後は型を指定せずに、型推論での変数定義を試みていますが、エラーとなります。

このように、特別な事をしていない型の事を非Optional型と呼びます。

非Optional型にはnullを代入する事が出来ません。

どうしてもnullを代入する必要がある場合は、Optional型で定義する必要があります。

定義方法は以下の通りです。

var str: String? = nil
var char: Character? = nil
var arr: Array<String>? = nil

型の後ろに?を付けるとOptional型となり、コンパイルエラーになりません。

なお、型推論の記法でOptional型を定義する事は出来ません。

さて、本題の関数ですが、戻り値に非Optional型(String)を指定しているのがエラーの原因のようです。

関数の定義を以下のように変更してみます。

func getName(id: Int) -> String? {

すると、こうなります。

f:id:Arihara:20200331200041p:plain

エラーが大量発生しました。

Value of optional type 'String?' must be unwrapped to a value of type 'String'

直訳すると、Optional型String?の値はString型の値にアンラップされる必要があります、といった感じです。

アンラップとは、Optional型から非Optional型に変換する処理のことです。

Optional型は、ラッピングが施された箱のような物と考えると分かりやすいかと思います。

中身を使いたい場合には、ラッピングを取り除く(アンラップする)必要があります。

ただし、箱の中には、何も入っていない可能性があります。

この状態がnullです。

アンラップの方法は様々あります。

  • 強制アンラップ
print(getName(id: 1)! + "が取得されました。")

Optionalな変数の最後に!を付けると強制的にアンラップされます。

ただし、その変数がnullだった場合、ランタイムエラーとなり、アプリが強制終了してしまいます。

基本的には推奨されない方法です。

  • nullチェック
if getName(id: 2) != nil{
print(getName(id: 2)! + "が取得されました。")
}

条件を追加して、nullでない場合のみアンラップします。

!=はアンラップではなく、Not Equalの意味なので注意してください。

  • デフォルト値を与える
print((getName(id: 3) ?? "知らない人") + "が取得されました。" )

nullでなければアンラップした値、nullなら"知らない人"を返します。

演算子??三項演算子の省略型です。

getName(id: 3) != nil ? getName(id: 3)! : "知らない人"

と同一の意味です。

??を用いる方が、断然直感的で分かりやすいかと思います。

if let name4 = getName(id: 4){
    print(name4 + "が取得されました。")
}

Optional変数をアンラップし、変数に代入可能(=非null)であれば、代入を行った後にTRUEが評価されます。

代入を行った変数は、if文の中でのみ利用可能です。

nullの場合は、FALSEが評価され、if文の中は実行されません。

  • guard-let構文
guard let name5 = getName(id: 5) else { exit(1) }
print(name5 + "が取得されました。")

オプショナルバインディングとほぼ同じですが、こちらは変数がguardブロックの外でも利用可能になります。

この特性から、 nullの場合に実行されるelseブロックの処理では、その変数が使用出来なくなるように工夫する必要があります。

上記の例では、プログラムを終了させています。

関数単位では、returnさせるなどの処理が考えられます。

最後に、修正したコード全体をもう一度見てみましょう。

//idからnameを取得する関数(型をOptionalに変更)
func getName(id: Int) -> String? {
    switch id {
    case 1:
        return "モニカ"
    case 2:
        return nil
    case 3:
        return "ナツキ"
    case 4:
        return "ユリ"
    default:
        return "モニカ"
    }
}

//関数を呼び出して名前を表示

//1.強制アンラップ
print(getName(id: 1)! + "が取得されました。")

//2.nullチェックしてアンラップ
if getName(id: 2) != nil{
    print(getName(id: 2)! + "が取得されました。")
}

//3.デフォルト値を与える
print((getName(id: 3) ?? "知らない人") + "が取得されました。" )

//4.オプショナルバインディング
if let name4 = getName(id: 4){
    print(name4 + "が取得されました。")
}

//5.guard-let構文
guard let name5 = getName(id: 5) else { exit(1) }
print(name5 + "が取得されました。")

実行結果は以下の通りです。

モニカが取得されました。
ナツキが取得されました。
ユリが取得されました。
モニカが取得されました。

null安全はいいぞ!

長々書いていきましたが、結局はこれが言いたい事です。

null安全とは、値がnullかどうかのチェックをコンパイラが強制してくれる機能の事です。

Swiftの例で、関数の型をOptionalに変更した際にエラーとなったのは、この機能による物です。

また、非Optional型はnullを全く意識する事なく、使用する事が出来ます。

変数の定義の際は、基本的には非Optional型で定義を行い、nullが入る可能性のある物に限り、Optional型で定義する、と言う方針が良いと思います。

nullはあまりに多くのバグを生み出してきた厄介者なので、null安全の流れには是非とも乗っていきたいところです。

参考にさせて頂いたサイト

もっと深く知りたい方は、こちら。

あとがき

ネタが伝わってれば目標達成です。

ちなみにエアプです。

ナツキ派だと思います。

忠実に再現するのであれば、idに何を与えても「モニカ」を返す方が良かったかしら。

さて、この記事ではSwiftとPythonを対比させて、null安全って凄いでしょ!と示しているところですが、実はPython拡張機能をインポートすることにより、null安全になる事が出来ます。

詳細は以下のサイトに載っています。

ここまで長い記事は書いた事が無かったので、全然まとまりませんでした。

これから精進していきたいと思います。多分。

最後まで読んで頂いた方からは是非ダメ出しをして頂きたいところです。

それにしても、この辺をきちんと理解しているプログラマーはどのくらいの割合で居るのかが気になります。

個人的には知らない人のほうが多数だと思うのですが、どうでしょう。

普及度的に考えるとPythonC#がnull安全普及の先駆けとなって欲しいです。

ただ、この2つは選択的対応なので、そもそも設計者がnull安全の存在を知らないと意味無いですが。

やっぱりネイティブ対応の言語が一番嬉しいんですよね。

私がnull安全の恩恵を受けられるのは何十年先になるやら・・・

誰か良い職場教えてください。

ここまで読んでいただき、ありがとうございました。