型の安全性とnull安全について(スライド板)
アジェンダ
型安全性とはなにか
null安全とはなにか
TypeScriptではどうか
Haskellではどうか
Goではどうか
型安全でない言語
注意
最後の方、少しHaskellの話をします
(嬉しいことに)Swift, Elm, Rustを触ってる、触ろうとしている方がオーディエンスにいるので意味はあるかなー、という気持ちですmrsekut.icon
事実と意見が混在しています。間違いがあればご指摘いただけると嬉しいです。
型安全性とはなにか
「型検査を通過したなら、ある種のバグが存在しない」という性質
こういう性質のことを「健全性」と言う(後述)
検査済みのバグは実行時には絶対に起こらないということを数学的に保証
ある種のバグってなんや?
バグの定義は言語によって異なる
例えば
メモリ安全
null安全 (後述)
など
健全性と完全性
型システムの目指すべき性質
健全性は、型エラーがなければ、絶対にバグはない、という性質
完全性は、型エラーがあれば、絶対にバグはある、という性質
健全性
型エラーがなければ、絶対にバグはない
絶対に!!
型エラーがある時の話はしていない
実際にはバグがなくても型エラーがあるなら怒る
下図の灰色は「コンパイルエラーが出てるけど正しく動く」やつ
https://gyazo.com/c0935a8be4ab128867e06c3db455cca0
例えばこんなコードは型エラーはあるが正しく動く
code:ts
// この関数の戻り地はstring
const returnString = (): string => {
if (true) {
return 'string'; // 条件がtrueなので100%こちらが評価される
} else {
return 42; // error: Type '42' is not assignable to type 'string'.
}
};
完全性とは
型エラーがあれば、絶対にバグはある
図で言えば白を広げて黒との境界値を一致させる
https://gyazo.com/fa52465194b90d452b633a5b51ae14fb
完全性は満たされていない言語も多い
そもそも大前提にあるもの
実行時エラーを起こしたくない
実行時エラーとはプログラムが未定義な状態に到達すること
未定義なのでどうなるかはわからない
例えばメモリリークとか、プログラムが強制終了するとか
例外とは異なる
Pythonインタプリタとかでエラーメッセージが表示されるやつは「例外」
実行時エラーを防ぐ機構の一つが型システム
実行時エラーが起こる例
値が未定義な変数を参照することによるエラー
ぬるぽ
1965年に考案したnull参照の概念は、10億ドル単位の過ちと呼ぶべきものであろう。
「操作」と「値」の種類の不整合によるエラー
1 + "hello"
swich文で該当するパターンが存在しないことによるエラー
プログラムが停止しないことによるエラー
無限ループ
null安全とは
型でぬるぽを防ぐ機構
null安全の要件を満たすもの
条件
nullable
non-null
片方だけのものはnull安全ではない
「型を見るだけで」nullになりうるかどうかを判断できるのも地味に強い
C言語などではint hogeな値が実はnullかもしれない
nullableとは
nullになりうる型
tsではhoge: string | nullのやつ
型を見るだけでnullになり得ることがわかる
nullbaleな型を持つ値はそのままでは使えない(後述)
non-nullとは
nullableでない型にnullを代入できないという性質
TypeScriptなら、stringやnumberの値にnullを代入できない
code:ts
let hoge: string = "こんちわ"
hoge = null // Type 'null' is not assignable to type 'string'.
普通に考えて、nullableでない型にnullが代入できちゃったらnullableのうれしみを享受できない
null安全の何が嬉しいのか
null安全でないプログラムを書いたらコンパイラが怒ってくれる
nullableなものはnullでないことを確認しないと怒られる
code:ts
const s: string | null = null;
if (s != null) {
s.length; // OK
}
null安全でない言語の場合どうなるか
ぬるぽにならないようにプログラマが常に気にしないといけない
APIでデータを取得するときはもちろん、それ以降のデータを使い回す間もずーっと
もしnullなものに対してメソッドを呼んだりしたら即NullPointerExceptionで死ぬ
code:js
// JavaScript
let s = null;
s.length; // TypeError: Cannot read property 'length' of null
interfaceでの型付けに、nullが入りうるものとそうでないものの区別がつかない
実際は「-1のときは、エラーにする」ので、型を見るだけじゃわからない
だからまじで一生気にしないといけない
絶対にnullにならないものに対しても、本当にnullになりうるものに対しても
nullableであることを型で表現できない場合どうするか
null安全でない言語での話
命名規則で「この変数はnullになり得ますよ」を表現したり
裁量は完全にプログラマに委ねられる
命名規則をミスったら死ぬ
正しく命名していても使い方ミスったら気付けない
コンパイラは怒ってくれない
簡単に各言語のnull安全機能の紹介
TypeScriptでは
Union型などを使ってnullableを表現する
type A = string | null
利用するときはいちいちnullチェックをしないといけない
code:ts
const s: string | null = null;
if (s != null) {
s.length; // OK
}
チェックする箇所を多少減らせる
nullチェックの記述が簡潔になる
code:ts
const x = foo?.bar?.baz() ?? 'hoge' // fooやbarがnull/undefinedなら'hoge'を返す
穴もある
anyを使うとnull安全機能は局所的に無になる
漸進的型付けが出来るためにanyがあるので、本来0からTypeScriptで書くならanyは一切不要、な気もする
外部ライブラリとのアレはあるが
code:ts
const s: any = null;
s.length() // コンパイル時は怒られない。ただし実行時エラー
tsconfigでstrict: trueにしてないとチェックをしてくれない
Haskellでは
Maybe a型やEither a b型を使う
Maybe a型
Maybe a = Just a | Nothingという型
aは型引数で実際に具体型を入れてMaybe Intのようにして使う
genericsみたいなやつ
ちゃんと値がある場合はJust a、なければNothingを返す
ただしTSのときと異なりJust 42みたいなやつはそのままでは演算はできない
なのでJust 42から42を取り出して、演算してまたJustの中に入れて、みたいにする
code:hs
add = do
n1 <- Just 42 -- 42を取り出す
n2 <- Just 100 -- 100を取り出す
return $ n1 + n2 -- 足してJustにくるんで返す
main = print add -- Just 142 ←Justにくるまれている
いずれかがNothingなら結果もNothingになる
code:hs
add = do
n1 <- Just 42
n2 <- Nothing -- 片方がNothingだった
return $ n1 + n2
main = print add -- Nothing
他の言語でのMaybeに似てるやつ
SwiftのOptional
ElmのMaybe
Rust, ScalaのOption
KotlinのNull Safety
NimにもしょぼいOption型ライブラリがあった
Either a b型
Either a b = Left a | Right bという型
Maybeのちょっと強い版
正しい(Right)ときはRight aを返す
MaybeのときのJustと同じ
値がなかった時にLeft aを返す
例えばLeft Stringを返す
例えばエラーメッセージを表現できる
Nothingのときはなにもできなかった
他の言語のEitherに似てるやつ
RustのResult型
Elm, ScalaのEither型
null安全な言語にはnullableを便利に使う機構がある
Optional Chainingだったり
モナドだったり
いろいろ
型安全でない言語
Javaはnull安全ではない
Optionalクラスを自作して?表現したりする
参照型のみNullの代入が出来る
non-nullの性質はない
Objective-C
Go
動的型付けの言語の多く
null安全ではないが、実行時エラーは起きず、例外として捕捉される
例えばGoでは
関数の返り値に、「結果」と「いけたかどうか」の2値タプルを返す
code:go
// fが本来欲しい値。errはいけたかどうかのフラグ
f, err := os.Open("/tmp/hogehoge.txt")
if err != nil {
log.Fatal(err) // errorのときの処理
}
... // エラーが無かったので、なんらかのfの処理をする
デメリット
↓この慣用句が頻発する
code:go
if err != nil {
log.Fatal(err)
}
↑は書かなくてもコンパイラは怒らないので忘れたら死ぬ
終わり
null安全な言語では、コンパイルが通った時点でヌルポにはなり得ない
できるだけコンパイラに頼って
安全な記述をしたい