1. Qiita
  2. 投稿
  3. Swift

null安全でない言語は、もはやレガシー言語だ

  • 1290
    いいね
  • 28
    コメント

この数年で null 安全 1 は一気に浸透したように思います。ざっと思いつくだけでも、次のプログラミング言語で null 安全 が採用されました。

言語 null 安全 に関する公式の記述 リプレース対象言語 オンラインドキュメント ブラウザ上で試す
Ceylon Typesafe null and flow-sensitive typing Java Tour of Ceylon Ceylon Web IDE
Crystal Null Pointer Exception Ruby Crystal Programming Language play.crystal-lang.org
Flow Maybe Types JavaScript Quick Reference Try Flow
Hack Nullable PHP Hack Documentation -
Kotlin Null Safety Java Reference Try Kotlin
Python Union types - The Python Tutorial Interactive Shell
Rust Module std::option C++ The Rust Programming Language Rust Playground
Swift Optionals Objective-C The Swift Programming Language IBM Swift Sandbox
TypeScript Non-nullable Types JavaScript Handbook Playground

これらは、表中の「リプレース対象言語」に挙げたように、多くのメジャー言語に対する代替手段でもあります。 Java の代わりには Kotlin や Ceylon が、 JavaScript には TypeScript や Flow が、 Objective-C には Swift が、そして PHP には Hack があります。 Python は自身に null 安全 を取り込みました。 Crystal は直接 Ruby と連携して使えるわけではありませんが、 Ruby 風の null 安全 な言語です。 Rust は C++ の代替を目指して開発され、 Firefox の一部で C++ のコードを置き換えるのに使われています 2

null が引き起こしてきた数々の問題を考えると、僕は、 null 安全 は GC (やその他の安全なメモリ管理手法)に匹敵するプログラミング言語の進化だと考えています。僕はこれまで 10 年以上に渡って C や Java, JavaScript, Objective-C, Python などの null 安全 でない 言語を使ってきました。しかし、この 2 年間ちょっとの間、仕事で null 安全 な言語を使うようになって、もはやそれなしのコーディングは考えられないと感じています。もし、まだ null 安全 を体験したことがないなら、それは体験する価値のあるものだと強くオススメします。

null 安全 自体は特に新しい考え方ではありません。しかし、どれだけ優れた仕組みが存在しても、実務で使える言語に採用されなければ多くの人がその恩恵を受けることはできません。 null 安全 が様々な言語に採用されることで、今、ようやく null 安全 な言語を選択肢として考えられる状況になってきました。

かつて、「テストのないコードはレガシーコードだ」 3 という言葉で、テストの必要性が訴えられました。今では、テストが大事だという考えはプログラマの間でほぼ共通認識となっています。同じように、少し気が早くても、あえて null 安全 でない言語はレガシー言語だ と言い切ることで、 null 安全 の普及を加速させることができるんじゃないでしょうか。

本投稿では、 null 安全 の普及を目的とし、 null 安全 について 言語横断的に まとめました。

  • null 安全とは何か
  • null 安全だと何がうれしいか
  • nullable を快適に扱うための仕組み
  • nullable の種類と特徴
  • 言語別雑感
  • null 安全な言語の始め方
  • FAQ

null 安全とは何か

null 安全 とは、一言で言えば、 nullnil, None など)が原因で実行時エラーにならない仕組みです。

null に対してメソッドコールやメンバアクセスすると多くの言語では実行時エラーとなります。

// Java
String s = null;
s.length(); // java.lang.NullPointerException
// JavaScript
let s = null;
s.length; // TypeError: Cannot read property 'length' of null
# Python
s = None
len(s) # TypeError: object of type 'NoneType' has no len()

一方、 null 安全 な言語では実行時ではなくコンパイル時(動的型付言語では静的解析時)にエラーとなります。プログラムが実行できないので、当然ながら null が原因で実行時エラーになることはありません。実行するためにはコードを修正する必要があります。

// Kotlin
val s: String? = null
s.length // error: only safe (?.) or non-null asserted (!!.) calls are allow
// TypeScript
let s: string|null = null;
s.length; // Object is possibly 'null'.
# Python
s = None # type: Optional[str]
len(s) # error: Argument 1 to "len" has incompatible type "Optional[str]"; expected "Sized"

上記の String?string|null, Optional[str] が重要な点です。この ?|null, Optional はこの変数 snull かもしれない( nullable である)ことを表しています。 null には length というメンバはないので、 s.length はコンパイルエラーを起こし、コードを修正するよう要求されているわけです。

では、どうやって s の長さを取得すればいいのでしょうか。 snull でないことをチェックすれば OK です。

// Kotlin
val s: String? = null
if (s != null) {
    s.length // OK
}
// TypeScript
let s: string|null = null;
if (s != null) {
    s.length; // OK
}
# Python
s = None # type: Optional[str]
if s is not None:
    len(s) # OK

この if{} の中では snull でないことが確定しており、そのため s.length がエラーになりません。コンパイラが制御フローを解析し、 {} の中では sString? ではなく String だと判断してくれるのです。

では、変数宣言時に ?|null, Optional を付けると null チェックが必須になるのなら、それらを付けずにいきなり null を代入してみたらどうなるでしょうか。

// Kotlin
val s: String = null // error: null can not be a value of a non-null type kotlin.String
s.length
// TypeScript
let s: string = null; // Type 'null' is not assignable to type 'string'.
s.length;
# Python
s = None # type: str
# error: Incompatible types in assignment (expression has type None, variable has type "str")
len(s)

この場合は代入がコンパイルエラーになります。 null 安全 な言語では、 ?|null, Optional が付いていない型( non-null 4 )の変数に null を代入することはできません。そのため、 non-null な変数の値は null でないことが保証されています。上記の s.lengthnull が原因で実行時エラーになることはありません。

このように nullablenon-null を型で区別し、 null による実行時エラーを防止する仕組みが null 安全 です。

null 安全だと何がうれしいか

プログラマはコードを書き、それをコンパイル(または静的解析)し、実行します。実行できるプログラムはテストされ、出荷され、ユーザーが利用します。この工程の中で、 問題は早めに発見されることが好ましい です。もし、実行時エラーをテストで検出できなければ、それはそのまま潜在バグとして製品に組み込まれ、出荷されてしまいます。コンパイルエラーであれば開発時に 必ず 気付くことができます。

経験上、コードのロジック自体が間違えていたというケースを除けば、 null は最もよくあるバグの原因の一つだと思います。その問題をコンパイル時に排除できるのは素晴らしいことです。

null の生みの親である Tony Hoare は次のように述べています 5

それは10億ドルにも相当する私の誤りだ。null参照を発明したのは1965年のことだった。(中略)これは後に数え切れない過ち、脆弱性、システムクラッシュを引き起こし、過去40年間で10億ドル相当の苦痛と損害を引き起こしたとみられる。

null の発明から半世紀を経て、ようやく僕らは null 安全 によって null の苦しみから解放されようとしているのです。

null 安全は生産性も高める

いくら安全になるとはいえ、 null チェックを強制する null 安全 は一見煩わしく、生産性を下げるものに見えるかもしれません。しかし、 null 安全 は安全性を高めると同時に生産性も高めます

null 安全 では、 Foo? などで表される nullable が目立ちますが non-null も重要です。 null が代入されることのない変数や null を許容しない引数、 null を返さない関数の戻り値などはすべて non-null にできます。 null 安全 でない 言語では値が null でないことを assert やテストで確認しなければなりません。 non-null を使えば、それらがなくてもコンパイラが null でないことを保証してくれます。しかも実行時ではなくコンパイル時にです。

nullable に対して書かなければならない null チェックは、 null 安全 でない 言語でも必要なものです。余分な null チェック が増えているわけではありません。

つまり、 null 安全

  • non-null によって不要なコードを省略できる
  • nullable によってエラーを早め(コンパイル時)に検出できる

ことによって、安全性だけでなく生産性も高める仕組みなのです。

nullable を快適に扱うための仕組み

null 安全 なコードを快適に書くためには、 nullable を快適に扱える必要があります。基本的に nullable な値は null チェックによって処理されるわけですが、 null チェックだけで対処するには面倒なケースもあります。 null 安全 な言語の多くには、そのようなケースで nullable な値を扱うための便利な仕組みが備わっています。本節ではそのような仕組みの内、次のものを取り上げ説明します。

  • null チェックに伴うキャスト
  • !, !!
  • ?., ?->
  • ?:, ??
  • map
  • flatMap
  • do 記法
  • ?
  • 型推論

null チェックが煩わしいと感じるなら、道具を上手く使えていないのかもしれません。本節を読むことでそのような問題が解決する可能性があります。もしくは、あなたの言語に「快適に書くための仕組み」が足りていないのかもしれません。「快適に書くための仕組み」の充実度は言語によって大きく異なります。次の表は、どの言語にどの道具があるかをまとめた表です。「快適に書くための仕組み」の中には関数で実装できるようなものもあるので、他の言語の「仕組み」を眺めて自分の言語に取り入れられないか考えてみて下さい。

言語 !, !! ?., ?-> ?:, ?? map flatMap do 記法 ? 型推論
Ceylon
Crystal
Flow
Hack
Haskell
Java
Kotlin
Python
Rust
Scala
Swift
TypeScript
  • Java と Scala は null 安全 な言語ではありませんが、 null を使わずに Optional, Option を使って null 安全 なコードを書くことができます。そのため、 Optional, Option でどの道具を使えるかを示すために表に加えています。
  • Python の ? が △ なのは、 Union[Foo, None]Optional[Foo] と表すシンタックスシュガーはあるけれども、 Foo? ほど簡潔でないからです。
  • Java の型推論が △ なのは、ラムダ式の型は推論できるけれども変数の型は推論できないからです。

null チェックに伴うキャスト

これまでに見てきたように、 if 文で null チェックをすると、その if 文の {} の中では String?String として扱われる(キャストされる)という仕組みがありました。この仕組みは スマートキャスト (Smart cast) (Kotlin), 制御フロー解析 (Control flow analysis) (TypeScript), Flow-sensitive typing (Ceylon) などと呼ばれています。以下、本投稿では スマートキャスト と呼びます。

スマートキャスト がないと、 null チェックをしても sString? のままです。 sString? のままだと、 null チェックをしても結局 s.length でコンパイルエラーになってしまいます。 length にアクセスするには s を強制的に String にキャストしなければなりません。

// Kotlin
val s: String? = null
if (s != null) {
  (s as String).length // String にキャストしてから s を使う
}

しかし、これだともし null チェックを忘れてもコンパイルが通ってしまいます。

// Kotlin
val s: String? = null
(s as String).length // 実行時エラー

snull なので、上記のコードは String にキャストしようとしたときに実行時エラーになります。 nullablenon-null によって、せっかくコンパイル時に null によるエラーを検出できるようになったのに、これでは意味がありません。

このように、 null チェックと non-null へのキャストが独立していると、キャストの失敗によるエラーが実行時に発生します。 null チェックと non-null へのキャストをセットで行う仕組みは null 安全 を実現する上で欠かせまぜん

似たような仕組みとして、 Swift や Rust には if let (Optional binding) が、 Java の Optional には ifPresent メソッドがあります。 Haskell や Scala では パターンマッチング を使います。どれも スマートキャスト とは異なる仕組みですが、やりたいことは null チェックの条件分岐と non-null へのキャストという意味では同じです。

!, !!

どうしても null である可能性を無視して nullablenon-null に変換したいこともあります。

例として、テキストフィールドに入力された文字列を整数に変換する処理を考えましょう。一般的に、文字列から整数への変換は失敗する可能性があります。 "123"123 に変換できますが "ABC" は整数に変換できません。変換に失敗した場合には null を返すとすると、文字列を整数に変換する処理は String から Int? への変換だと考えられます。

// Swift
let number: Int? = Int(text)

しかし、テキストフィールドに制約が加えられていて数字しか入力できないこともあります。そのようなケースでは文字列から整数への変換に失敗することはありません。それでも、テキストフィールドから得られるのは文字列なので、型の上では整数への変換が必要となります。

// Swift
if let number: Int = number { // null チェック
  // ここでは number を Int として使える
}

必ず null でないとわかっている場合にも、 null チェックを書かなければならないのは手間です。 型は万能ではありません。ロジックの上では null にならないけれど、型の上では nullable になってしまうことがしばしばあります。このままでは、 null 安全 でない 言語では書かなくて良かった無駄な null チェックを書かなくてはならないことになり、生産性が下がりかねません。 null である可能性を明示的に無視して nullablenon-null に変換する簡単な方法が必要です。

Swift では nullable な値の後ろに ! という後置演算子を付けることで、強制的に non-null に変換することができます。これを Forced unwrapping と呼びます。

// Swift
let number: Int = Int(text)!

number の型が Int? ではなく Int であることに注意して下さい。これは、 ! によって Int? 型である Int(text) の戻り値が Int 型に強制変換されたからです。

もし、値が null のときに !non-null に変換しようとすると実行時エラーとなります。 ! を乱用してはいけません。上記の例のように 絶対に null にならないけれども型の上では nullable になってしまうケースや、使い捨てのスクリプトで null ならクラッシュしても良いケースなど、 ! が問題を引き起こさないよう常に意識的でなければなりません

安全でない操作には長い名前を付けるべきだという意見もありますが、僕はその意見には反対です。補完があれば長くても簡単に入力できます。そして、 ! はたった 1 文字ですが、プログラマが明示的に書く必要があります。 ! を書くときに立ち止まって、本当に問題ないケースか確認するという作業を怠らなければ null 安全 を破壊することはありません。

TypeScript の ! や Kotlin の !! も同じように nullablenon-null 化する後置演算子です。 Java の Optional クラスや Scala の Option クラスには同じ目的の get メソッドが、 Rust の Option には unwrap メソッドがあります。それらを適切に使うことで、快適に null 安全 なコードを書くことができます。

余談ですが、 Swift では Forced unwrapping 以外にも ! という記号は危険な処理に用いられることが一貫されており、 ! を書くときに常に意識的でいやすいです。対して、例えば Java では get というメソッドが安全な処理( Mapget メソッド等)でも使われており、 Swift と比較すると危険な処理であることを意識しづらいのではないかと思います。ロジカルには二つのやっていることは同じですが、視覚的・感覚的に訴えることのできる Swift のような言語デザインは素晴らしいと思います。

?., ?->

nullable な値のメンバやメソッドにアクセスしたい、ただし、値が null の場合は結果も null でいいというのもよくあるケースです。愚直に書くと次のようになります 6

// Kotlin
val s: String? = null
val length: Int?
if (s != null) {
  length = s.length
} else {
  length = null
}

length を取得するためだけにこの分岐を書くのは大変です。 Swift や Kotlin, Ceylon, Hack などの ?. ( Swift では Optional chaining と呼びます)を使えばこれを簡単に実現できます( Hack では . の代わりに -> を使うので ?-> です)。

// Kotlin
val s: String? = null
val length: Int? = s?.length // s が null なら結果も null

特に、 ?.null になるかもしれない操作をチェーンするときに重宝します。

// Kotlin
val baz: Baz? = foo?.bar?.baz

これを null チェックと条件分岐で書こうとすると次のようになります。

// Kotlin
val baz: Baz?
if (foo != null) {
  val bar: Bar? = foo.bar
  if (bar != null) {
    baz = bar.baz
  } else {
    baz = null
  }
} else {
  baz = null
}

?. によって、 null チェックによる複雑な条件分岐を書かかずに簡潔なコードで済ませられます。

?:, ??

null の場合にデフォルト値を与えたいというのもよくあるケースです。

// Kotlin
val s: String? = null
val length: Int
if (s != null) {
  length = s.length
} else {
  length = 0 // デフォルト値 0 を与える
}

Kotlin の ?: 演算子や Swift の ?? 演算子を使えばこれを簡単に書けます。

// Kotlin
val s: String? = null
val length: Int = s?.length ?: 0 // デフォルト値 0 を与える

Java の OptionalorElse メソッドも同様の働きをします。

map

?. は強力な構文ですが、 nullable な値を演算子のオペランドにしたり、関数やメソッドの引数に渡したいときには役に立ちません。

たとえば、 Int? を二乗することを考えてみましょう。 ?. と同じように元の値が null のときは結果として null を得たいとします。

もし、 square (二乗する)というメソッドがあれば次のように書けます。

// Swift
let number: Int? = nil
let squared: Int? = number?.square()

しかし * 演算子で number * number のように書こうとすると ?. ではうまく書けません。

// Swift
let number: Int? = nil
let squared: Int? = number?. * number?. // コンパイルエラー

これを null チェックと条件分岐で書くと次のようになります。

// Swift
let number: Int? = nil
let squared: Int?
if let number: Int = number { // null (nil) チェック
  squared = number * number
} else {
  squared = null
}

とても面倒です。こういう場合には、 map という高階メソッドが役立ちます。

// Swift
let number: Int? = nil
let squared: Int? = number.map { $0 * $0 }

Swift を知らないと number.map { $0 * $0 } という表記が何を表しているのかわかりづらいと思いますが、これは JavaScript の number.map(function(x) { return x * x; }) または number.map(x => x * x) 、 Java の number.map(x -> x * x) に相当します。

map メソッドに渡されたラムダ式は、 Optional の値が nil でない場合だけ実行され、 map の結果として返されます。ラムダ式の引数に渡されるのは non-null 化された値です。もし Optional の値が nil であれば結果も nil になります。

Java の Optional や Scala, Rust の Optionmap メソッド、 Haskell の Maybefmap も同様の挙動をします。 Kotlin では nullablemap メソッドはないですが、 ?.let メソッドを組み合わせることで number?.let { it * it } と書くことができます。

flatMap

前述の文字列から整数への変換

// Swift
let number: Int? = Int(text)

textString ではなく String?だったらどうでしょうか。整数への変換が Int? を返すので、 map で書くと Optional がネストして戻り値の型が Int?? になってしまいます。

// Swift
let text: String? = nil
let number: Int?? = text.map { Int($0) }

Int?? がネストしている部分です。今ほしいのは Int? なのでこのままではダメです。

flatMapmap と似ていますが、ネストした Optional を一重に潰して(フラットにして)返してくれます。

// Swift
let text: String? = null
let number: Int? = text.flatMap { Int($0) }

Java の Optional や Scala の OptionflatMap メソッド、 Rust の Optionand_then 、 Haskell の Maybe>>= 演算子も同様の挙動をします。

実は mapflatMap があれば ?. でできることはすべてできます。 ?.mapflatMap をより簡単に書くための表記だと言えるでしょう。 mapflatMap?. ではカバーできないケースに対応可能なより柔軟な仕組みとして重要です。

なお、 Kotlin や Ceylon, TypeScript, Python では nullable をネストすることができません。詳しくは後述しますが、たとえば Kotlin の Int??Int? と等価になります。そのため、これらの言語では mapflatMap が等価になります。 Kotlin では flatMap 相当のことをするのにも、 map と同じく ?.let で代替できます。

do 記法

a: Int?b: Int? があったときに、 ab を足すにはどうすればいいでしょうか。例によって、 ab のどちらかが null だった場合は結果として null を得たいとします。

null チェックをして条件分岐は面倒なので、 flatMapmap を使って書いてみます。

// Swift
let a: Int? = 2
let b: Int? = 3
let sum: Int? = a.flatMap { x in b.map { y in x + y } }

a.flatMap { x in b.map { y in x + y } } は、 JavaScript なら a.flatMap(x => b.map(y => x + y)) 、 Java なら a.flatMap(x -> b.map(y -> x + y)) という意味です。

書けないことはないですが、ラムダ式をネストするのは少々読みづらいです。 Haskell では do 記法を使い、より簡潔に記述できます。

do 記法は flatMap のネストを簡潔に書くための記法です。 map が混ざっていてはいけないので、まずは mapflatMap で書き変えてみましょう。

// Swift
let sum: Int? = a.flatMap { x in b.flatMap { y in .some(x + y) } }

.some(x + y) は、 Int 型である x + y の結果を Optional で包んで Int? 型にするための処理です。これを Haskell で書くと次のようになります。

-- Haskell
sum = a >>= (\x -> b >>= (\y -> Just (x + y)))

>>= は Swift の flatMap に、 Just.some に当たります。 do 記法を使うと、このコードを次のように書き変えられます。 do 記法は >>= のシンタックスシュガーなので、この二つのコードは等価です。

-- Haskell
sum = do
  x <- a
  y <- b
  return (x + y)

<-do 記法で用いられ、上記のコードでは nullablea, bnon-nullx, y に変換しています。 non-null に変換さえできれば最後に x + y を計算するだけです。 >>=flatMap )をネストするよりずっとわかりやすいと思います。

Scala にも同様の仕組みがあります。

// Scala
val a = Option(2)
val b = Option(3)

val sum = for {
  x <- a
  y <- b
} yield x + y

これまでの関係をまとめると、

  • 多くのケースで簡潔な ?. を使う
  • 一部のケースでは ?. では書けないので、上位互換の mapflatMap を使う
  • flatMap がネストするとわかりづらいので do 記法を使う

となります。ただし、これは言語横断的な話で、 Haskell や Scala に ?. はないですし、 Swift や Kotlin には do 記法がありません。実用上はその言語で欠けている部分を諦めて別の方法で妥協する必要があります。

?

Swift や Kotlin, Ceylon, Hack, Flow などの言語では、型名に ? を付けて nullable な型を表します。 nullable な型を書く機会は多いので、簡潔な記法が用意されているのはうれしいところです。

Foo? は、 Swift では Optional<Foo> の、 Ceylon では Foo|Null のシンタックスシュガーです。 Kotlin のように ? が直接的に nullable であることを表す言語仕様なら ? の存在は必須ですが、そうでなくても ? がシンタックスシュガーとして用意されていれば便利です。

TypeScript には、残念ながら 2.0 時点でこのようなシンタックスシュガーはありません。 Crystal にもありませんが、 Crystal のように極力コード中に型を書かず推論させる言語であれば不要だと思います。

型推論

直接的に null 安全 に関わっているわけではないですが、僕は 型推論null 安全 を快適にするために重要だと考えています。

null 安全 は、 null に関するエラーを型を使ってコンパイル時(または静的解析時)に検出する仕組みです。そのため、言語に静的型付けであることを強制します。しかし、 null 安全 のために動的型付けの書き心地が損なわれてしまっては、 null 安全 を取るのか、動的型付けの書き心地を取るのかという話になってしまいます。

静的型付けでも、 型推論 で変数や引数、戻り値の型を書かなくてよいのであれば、動的型付けに近い書き心地を実現できます。

// Swift (型推論なし)
let numbers: [Int] = [2, 3, 5]
let squares: [Int] = numbers.map { (x: Int) -> Int in x * x }
// Swift (型推論あり)
let numbers = [2, 3, 5]
let squares = numbers.map { $0 * $0 }
// JavaScript
let numbers = [2, 3, 5];
let squares = numbers.map(x => x * x);

静的型付けの Swift でも型推論があれば、動的型付けの JavaScript と同じような簡潔さでコードが書けているのがわかると思います。

nullable の種類と特徴

一口に nullable と言ってもその実現方法は様々です。しかし、大きく分けると二つに分けられます。それは nullable な型をネストできるかできないかです。

ネストできる

  • Haskell
  • Java
  • Rust
  • Scala
  • Swift

ネストできない

  • Ceylon
  • Crystal
  • Hack
  • Kotlin
  • Python
  • TypeScript

nullable な型をネストした Foo?? という型は何を表すのでしょうか。もし Foo? を単に Foonull を入れられる型だと考えると、 Foo?? であっても null を入れられるという性質を二重に記述しているだけになり Foo? と変わらないように思えます。

Foo?? が何の役に立つのかは例を見てみるのが一番です。最も身近な例の一つは、 MapDictionary 、ハッシュを使うときです。

Kotlin や Java の Map<K, V>K 型のキーで V 型の値を引くためのクラスです。キーで値を引くと、キーに対応した値が存在する場合としない場合があります。キーが存在しない場合は null を返したいので、戻り値の型は V? になります。例えば、 Map<String, Int> なら String 型のキーで値を引き、戻り値として Int? を得ます。

// Kotlin
val map: Map<String, Int> = mapOf("a" to 2, "b" to 3, "d" to 5)
map["a"] // 2
map["e"] // null (キー "e" に対応した値が存在しない)

では、値として null も格納したい場合はどうなるでしょうか。つまり、 Int?V とし、 Map<String, Int?> としたいということです。このとき、キーで値を引いた戻り値の型は、 V?VInt? なので Int?? になります。しかし、 Kotlin では ?null を代入できるかどうかを表すだけなので、 Int??Intnullnull か、という意味になり Int? と同一です。その結果、

// Kotlin
val map: Map<String, Int?> = mapOf("a" to 2, "b" to 3, "c" to null, "d" to 5)
map["a"] // 2
map["c"] // null (キー "c" に対応した値 null )
map["e"] // null (キー "e" に対応した値が存在しない)

となり、 null という値が返されたのか、値が存在しなかったのかを区別することができません。

もし nullable な型をネストできれば話は違います。 Swift では Int??Int? が区別され、次のようになります。

// Swift
let map: [String: Int?] = ["a": 2, "b": 3, "c": nil, "d": 5]
map["a"] // Optional(Optional(2))
map["c"] // Optional(nil)
map["e"] // nil

これなら、 nil という値が存在したのか、それとも値が存在しなかったのかで分岐することができます。

// Swift
if let c: Int? = map["c"] {
  if let c: Int = c {
    // nil でない値が存在した場合
  } else {
    // 値 nil が存在した場合
  }
} else {
  // キーに対応した値が存在しなかった場合
}

nullable な型がネストできるということはそれだけ表現力が高いということです。ですので、僕は一般論としてはネストできる方が望ましいと考えています。ただ、パフォーマンスや言語仕様、実行環境との相性などを考慮すると、何がベストかは一概には言えません。たとえ nullable な型をネストすることができなくても null 安全 の魅力を大きく損ねるわけではありません。たまに不便なケースがある程度です。

本節ではさらに詳しく、それぞれの言語で nullable な型がどのように実現されているのか見ていきます。仕組みを知れば、なぜ nullable な型をネストできる言語とできない言語があるのかも理解できると思います。

nullable か non-null かだけを区別

一番単純な方法で、型に ? をつけると nullable に、そうでなければ non-null になるという方法です。 Kotlin や Hack が採用しています。ある型 Foo? をつけて Foo? とすると( Hack では ?Foo )、 Foo? 型の変数には Foo インスタンスの他に null も代入できます。

// Kotlin
val s1: String = null // コンパイルエラー
val s2: String? = null // OK

この場合、 Foo?? のように nullable な型をネストしても、 Foonullnull ということを表すだけであり、 Foonull 、つまり Foo? と言っているのと同じです。そのため、 Foo??Foo? は同じ型を表します。

nullable な型をネストできないのは不便なときもありますが、 Kotlin においては妥当な選択だったんじゃないかと思います。 Kotlin は "100% interoperable with Java" を謳う Java 仮想マシン上で動作する言語です。 Java には null が存在し、その Java との連携を考えると、この実装は自然で理にかなっていると思います。

Union type

Ceylon や TypeScript, Python, Crystal が採用しているのがこの方式です。これらの言語の型システムには Union type が存在しており、それを使って nullable を実現しています。 Union Type とは複数の型のいずれかを表す型です。たとえば、 Foo|Bar のようにして Foo または Bar を代入可能な型を作ります( Python では Union[Foo, Bar])。

Ceylon では Foo|Null, TypeScript では Foo|null, Python では Union[Foo, None] のようにして nullable な型を表します。 Ceylon の nullNull 型の値、 TypeScript の nullnull 型の値、 Python の NoneNone 型の値として扱われます。

// Ceylon
Foo|Null foo = null;
// TypeScript
let foo: Foo|null = null;
# Python
foo = None # type: Union[Foo, None]

Foo|NullFoo 型の値か null かを代入できるというのは自然な表現です。言語仕様として Union type を持つなら、 Union typenullable を表現するのは自然だと思います。

なお、 Ceylon や Python には、 Foo|NullUnion[Foo, None] をより簡潔に記述するためのシンタックスシュガーが用意されています。 Ceylon では Foo?Foo|Null と、 Python では Optional[Foo]Union[Foo, None] と等価です。上記のコードは次のようにも書けます。

// Ceylon
Foo? foo = null;
# Python
foo = None # type: Optional[Foo]

TypeScript にはこのようなシンタックスシュガーはないので単純に string|null のようにして nullable であることを表します。 Crystal にもシンタックスシュガーがないですが、前述のように、 Crystal では型を明記することがほとんどないので問題にならないと思います。

Union typenullable を表す場合もネストすることはできません。 Kotlin のときと同じで、 Foo|Null|Null (Foo??) は FooNullNull かなので、 Foo|Null (Foo?) 、つまり FooNull かと同一です。式変換で書くと Foo?? == (Foo?)? == (Foo|Null)|Null == Foo|Null|Null == Foo|Null == Foo? と書けます。

Tagged union

Swift や Rust, Haskell が採用しているのがこの方法です。 Swift でも Foo? のようにして nullable な型を表しますが、値を持つときは some 、持たないときは none というタグが付けられています。 Union type 風に書くと、 Foo?some(Foo)|none と表されるようなイメージです。タグ付きの Union なので Tagged union と呼ばれます。

nullable を表す Swift の Optional と Haskell の Maybe は次のように宣言されています。

// Swift
enum Optional<T> {
  case none
  case some(T)
}
-- haskell
data Maybe a = Nothing | Just a

Swift では nonesome が、 Haskell では NothingJust がタグです。

タグがあることで nullable な型をネストできるようになります。 Foo?? == (Foo?)? == some(some(Foo)|none)|none なので、明確に外側の ?none なのか、内側の ?none なのかを区別することができます。

Union type 同様、 Tagged unionnullable な型を表すのも、直接的に値があるかないかを表しているので自然です。言語仕様として Tagged union を持つなら、ネストでき、自然な表現である Tagged unionnullable な型を表すのが望ましいと思います。

クラスや構造体

Java の Optional や Scala の Option はクラスで実現されています。

Java の Optional は単純にフィールドに値を保持し、それが null かどうかでメソッドの挙動が変化するようになっています。 Scala はおもしろくて、抽象クラス Option を継承した具象クラス SomeNone として実装され、ポリモーフィズムで挙動が切り替わるようになっています。オブジェクト指向的には Scala の方がキレイな実装なように感じます。

Java の OptionalisPresent メソッドは次のように実装されています。

// Java
public boolean isPresent() {
  return value != null;
}

しかし、 Scala の OptionisEmptySomeNone でそれぞれ実装されています。

// Scala
final case class Some[+A](x: A) extends Option[A] {
  def isEmpty = false
}
case object None extends Option[Nothing] {
  def isEmpty = true
}

false, true を返しているだけで条件分岐はありません。ただし、 Scala がポリモーフィズムで切り分けているのは isEmpty だけです。 mapflatMap など、その他のメソッドは条件分岐で記述されています 7 。やろうと思えばすべてのメソッドの挙動をポリモーフィズムで切り替えられます 8 が、おそらくパフォーマンス上の(条件分岐があっても静的ディスパッチした方が速いという)理由で条件分岐で書かれているのだと思います。

なお、 Java の Optional を Scala の Option のように継承でサブクラスを作れなかったのは、 Java に sealed class がないからだと思われます。もし、 Java で Optional を継承して SomeNone を作ろうとすると、 Optionalfinal クラスにすることができないので SomeNone の他にも Optional のサブクラスを作れてしまいます。 Scala の sealed class は継承を限定できるので SomeNone 以外に Option のサブクラスを作ることはできません。

この方法であれば、 Optional はただのジェネリックなクラスなので Optional<Optional<Foo>> として問題なくネストできます。

追記: コメントで @kmizu さんにご指摘いただいた通り、 Scala や Kotlin の sealed classTagged union を作る手段なので、 Scala の OptionTagged union とも言えます。詳しくはコメント欄を御覧下さい。

言語別雑感

今度は言語別に null 安全 に関連した雑感を述べます。

  • C#
  • C++
  • Crystal
  • Go
  • Java
  • JavaScript
  • Kotlin
  • Objective-C
  • Python
  • Ruby
  • Scala
  • Swift
  • TypeScript

C#

C# の T? (Nullable<T>) は本投稿で見てきた nullable とよく似ているように見えますが、 T として値型しかとれないという問題があります。また、 nullable な値のメンバにアクセスしてもコンパイルエラーとなりません。これが参照型に拡張され、静的に型チェックされ、参照型の Tnon-null 化されれば null 安全 な言語となります。

現状では中途半端で残念な仕様ですが、 C# はこれまでも先進的な仕様を取り入れてきた言語であり、また null 安全 になった TypeScript と同じ開発者が関わっているので、今後 null 安全 になることを期待しています。

追記: @gorn708 さんから補足コメントをいただきました。

次のC#のバージョンだとこの辺が本件の関連ですかね。
https://github.com/dotnet/roslyn/blob/features/NullableReferenceTypes/docs/features/NullableReferenceTypes/Nullable%20reference%20types.md

この Proposal が採用されて、 C# が null 安全 に一歩近づくといいですね。

C++

C++17 で std::optional が導入されます。しかし、それだけで null 安全 になるわけではありません。既存のコードベースが巨大なので、すべてが std::optional に対応して、 C++ が null 安全 になるというのは現実的ではないでしょう。そもそも、そういうことを目指している言語でもないと思うので、それでいいのかもしれませんが。

Crystal

基本的に型を書かずに推論させるという方針は Ruby 的な言語仕様と相性が良さそうです。しかし、 Ruby と連携せずにどこまで Ruby プログラマを取り込めるかが普及するかの分かれ目になってきそうです。まだ 1.0 がリリースされていないので、今後の展開に注目です。

Go

Go は過去 10 年で登場した人気言語の中ではめずらしく null 安全 でない 言語です。

僕は Go に詳しくないですが、 Go はこれまでのプログラミング言語の進化に逆らって、大胆に仕様を削ぎ落としてシンプルであることを目指した言語だと理解してます。ジェネリクスがなくても型付けされた配列やマップがあれば大体問題ないでしょ的な。

しかし、 Kotlin のように単に nullablenon-null を区別するだけであれば、言語仕様も大して複雑化しないと思うので、是非取り入れて欲しいところです。身近に Go に精通した人がいないので、 Go プログラマがどう感じているのか聞いてみたいです。

Java

Java 8 で Optional が追加されましたが、 Java で nullable をすべて Optional にしようというのは無理があります。標準ライブラリレベルで null を返すものがたくさんあるので、結局混ざってしまいます。例えば、 Mapget メソッドはキーに対応する値がなければ null を返します。 null 安全 な言語では nullable になっているところです。また、 Java の Optional はフィールドに使わない方がいい 9 という話もあり、そもそもが null を代替できるものではありません。

別のアプローチとして、 JSR 305 10 が進めばアノテーションで non-null チェックできるようになるかもしれません。標準ライブラリに片っ端から @NonNull をつけてくれれば大分楽になりそうです。ただ、しばらくは JSR 305 の標準化も期待できないので、サードパーティの @NonNull 的なものでがんばるしかないのでしょうか。たとえ @NonNull が標準になっても、本投稿で挙げた「快適に書くための仕組み」があるわけではないですし、辛い状況は続きそうです。

JavaScript

型アノテーションに関する話もある 11 12 13 ようですが、近いうちに採用される見込みはほぼなさそうです。 null 安全 は更にその先の話になるので、 JavaScript を null 安全 に書きたいなら、しばらくは TypeScript か Flow を使うのが現実的だと思います。

Kotlin

言語自体の問題ではないですが、 Swift が Objective-C の言語仕様に手を入れることで Objective-C との連携を改善して、 Objective-C との境界でも null 安全 なコードを書けるようになってきているのに対して、 Kotlin と Java の境界では null 安全 が破壊されてしまう状況が辛いです。このあたりは、同じ組織が両言語を開発しているかどうかの差が大きそうです。前述のように Java が @NonNull を採用してくれれば解決するかもしれません。

Objective-C

Objective-C はめずらしく、 nil に対してメソッドコールしたり、 nil のプロパティにアクセスしたりしてもエラーにはならない言語です。

Foo *foo = nil;
NSString *bar = foo.bar; // OK: bar == nil

しかし、これは null 安全 ということではありません。いつでも nil が実行時エラーを起こさないわけではなく、この bar を次のように使うと実行時エラーとなります。

NSURL *url = [[NSURL alloc] initWithString: bar]; // 'NSInvalidArgumentException', reason: '*** -[NSURL initWithString:relativeToURL:]: nil string parameter'

Objective-C の nil の扱いは一見安全なように見えて、問題の発覚を遅らせ、解決をより難しくしているだけです。潜在バグを生みやすいという点で、僕は Objective-C 方式は最悪だと考えています。かつて、 Objective-C で書いたプログラムを Java に移植したときに、 Java で NullPointerException が発生して初めて Objective-C 側の nil ハンドリングが不適切だったことに気付くということが何度もありました。

Swift が安定してきた今、もはや Swift を使わずに Objective-C を使う理由はほぼないように思います。 C++ 連携したいのでもない限り Swift を使いましょう。

Python

Python の言語仕様に型ヒント 14 が取り入れられるのは 3.5 からですが、コメント構文を使うことで Python 2 でも型ヒントを記述し null 安全 なコードを書くことができます。

# Python 3
def to_int(s: str) -> Optional[int]:
# Python 2
def to_int(s): # type: (str) -> Optional[int]

型ヒント やコメント構文で記述された型は mypy 15 16 を使ってチェックします。 mypy は Python の静的型チェッカーです。 Python 2 でも使える 17 ということは、すべての Python プログラマが今すぐでも null 安全 なコードを書けるということです。

なお、 mypy は現状ではデフォルトで null 安全 なモードになっていないので、 null 安全 にするには

mypy --strict-optional foo.py

のように --strict-optional オプションを指定する必要があります 18

Python の Optional にはまだ「快適に書くための仕組み」のほとんどありません。関連する Proposal として PEP 505 19 がありますが、当面は「快適な仕組み」をあきらめるか、自力で関数を作って再現する必要があるでしょう。

Ruby

静的型付 + 型推論が流行り、多くの動的型付言語も型アノテーションを取り入れる中、一向にその動きが見えなかった Ruby ですが、 RubyKaigi 2016 の Matz の講演 20 で静的に型推論する話が取り上げられました。これがどういうものになるかはまだわかりませんが、 null 安全 のためには静的型が欠かせないので歓迎です。もし、 Ruby 3 に null 安全 も取り入れられればうれしいですね。

Scala

僕は Scala をほとんど触ったことがないので、 Scala で null がどのように扱われているのか長い間疑問だったんですが、次のツイートで疑問が解けました。

Scala は言語仕様的には null 安全 でないけれども、 null を使わずにすべて Option に統一するという文化によって null 安全 を実現しているようです。

Swift

ここに挙げた言語の中では nullable な型を一番快適に使える言語だと思います。「快適に書くための仕組み」が充実していることに加え、連携先の言語( Swift の場合は Objective-C )側を修正することで、 null 安全 でない Objective-C との境界でも null 安全 なコードを書けるようになっていることが大きいです。両方の言語を Apple が主導しているからこそできる芸当です。

TypeScript

TypeScript 2.0 で null 安全 な言語に進化を遂げました。ただし、今のところデフォルトでは null 安全 なモードになって いない ので、 --strictNullChecks フラグをオンにする必要があります。 6

null 安全 になったとは言うものの、今のところ Python 同様「快適に書くための仕組み」がほぼありません。例えば、 ?. は JavaScript 待ちのようです 21 。将来的な発展に期待です。

null 安全な言語の始め方

null 安全 な言語が良さそうに見えても、既存のコードベースやライブラリもあり、今使っている言語を変更するというのは大変な作業です。しかし、 null 安全 な言語の中には既存の言語とシームレスに連携して、一つのプロジェクトの中でミックスしてコードを書けるものもあります。それなら、新しく書く部分だけを null 安全 な言語で書き始めることもできます。

本節では、次の null 安全 でない 言語を使っている人を対象に、できるだけ簡単に null 安全 な言語を使い始めるための方法を述べます。

言語 null 安全 な言語の導入しやすさ
Java
JavaScript
Objective-C
Python
Ruby

ただし、マークの意味は次の通りです。

マーク 意味
既存コードに対して付加的であり、問題が起こってもすぐに元に戻すことができる。導入のための障害はほぼ存在しない。
既存コードとシームレスに連携することができる。ただし、新しい言語で書いたコードは新しい言語としてメンテする必要はある。新しい言語は言語仕様が変化しがちなので、メンテコストは既存コードより相対的に高くなることが多い。
既存のコードと連携することはできない。使い捨てのスクリプトや試験的なプロジェクトなど利用できるケースが限定的。

Java

Java の既存コードと連携できて null 安全 なコードを書ける言語としては Ceylon, Kotlin, Scala あたりが有名です。僕は特に Kotlin をオススメします。

Kotlin は構文が Java と近く、 Java プログラマにとって違和感がなく使えると思います。 Ceylon はキーワードが特殊なのと、これからあまり流行ると思えないので微妙です。 Scala については関数型言語的な考え方など学ばなければならないことが多いと思います。

Kotlin と Java では、それぞれの言語で書かれたクラスやメソッドを互いにシームレスに利用することが出来ます。 Java で書かれたクラスをそのまま Kotlin 側で使ったり継承したりできるので、まずは新しく書くクラスだけ Kotlin で書いてみるのがいいと思います。

特に、 Android アプリ開発者であれば、 Kotlin は準公式言語のようなものです。 Android Studio は IntelliJ IDEA をベースに作られていますが、 Kotlin は IntelliJ IDEA を開発している JetBrains が作った言語です。 Android Studio 上でも Java と比べても遜色ないレベルのサポートを受けることができます。 22 23

JavaScript

AltJS は色々ありますが、無難に TypeScript か Flow を使うのがいいと思います。 TypeScript と Flow の使い分けは、 @mizchi さんの↓が参考になります 24

流行っているライブラリのみを組み合わせて使う場合や、バックエンドとの連携において型が十分に提供される環境なら、正直、flowtypeよりtypescriptでいいと思っています。flowtypeが力を発揮する環境は、既存のJSが大量に存在する環境や、railsなどの動的な型のフレームワーク環境で、静的な定義が抽出できない環境だと思います。

TypeScript も Flow も JavaScript にトランスパイルされるので、導入後何か問題が起こっても TypeScript や Flow のコードを捨てて、トランスパイル後の JavaScript コードだけ使うことができます。そのため、既存のプロジェクトに対しても導入のハードルは低いです。

なお、 TypeScript 2.0 の段階ではデフォルトで null 安全 なモードになっていないので注意して下さい。 null 安全 なモードにするには、 --strictNullChecks を指定する必要があります。デフォルトではないですが、常に --strictNullChecks をオンにすることが推奨されています。 6

Objective-C

Objective-C と連携できる null 安全 な言語は Swift 一択だと思います。これまでの Swift は破壊的変更が多く、メンテコストがおそろしくて Swift に移行できなかった人も多いかもしれません。しかし、現行の Swift 3 からはできるだけ後方互換が保たれる予定です。僕の勤務先では Swift バージョンアップ時のマイグレーションコストを差し引いても Swift で開発する生産性が勝つという判断でこれまでも Swift で開発してきましたが、今後はマイグレーションコストも小さくなり、ますます Objective-C を使う理由がなくなりそうです。

Swift のコードは Objective-C のコードと互いに連携することができます。 Objective-C で書かれたクラスを Swift 側で継承し、それを Objective-C 側で使うようなこともできます。すでに Objective-C のコードが存在するなら、新しく書く部分だけを Swift で書いてみるのがいいと思います。 Xcode を使っているなら、特に新しくインストールしなければならないものはありません。

Python

mypy 15 を導入することで今すぐにでも Python で null 安全 なコードを書くことができます。 Python 2 でも使える 17 ので、今すぐ Python 3 に移行できなくても今すぐ始めることができます。 Python 2 でも使えるコメント構文は実行時にはただのコメントとして解釈され、動作に一切影響を与えないので、既存のプロジェクトにも気軽に導入できます。

mypy と Python の Gradual Typing について学ぶには次の各記事が参考になります。

  • [翻訳] Python の静的型、すごい mypy! 16
  • What is Gradual Typing: 漸進的型付けとは何か 25
  • Mypy syntax cheat sheet 26
  • PEP 484 -- Type Hints 14

前述のように、 mypy では現状デフォルトで null 安全 なモードになっていないので、 --strict-optional オプションを指定する必要があります 18

Ruby

残念ながら、本節で挙げた他の言語ほど、 Ruby の場合はうまくいきません。 Ruby の代替である Crystal は、 Ruby と構文が似ているだけで Ruby のコードと連携することができません。既存のプロジェクトの一部だけを Crystal にするというのは難しいでしょう。

しかし、独立したスクリプトなら Crystal で書いても問題ないはずです。書けるところから Crystal を試してみるのが良いでしょう。 Amethyst 27 という Rails ライクな Web フレームワークもあるので、小規模な新規プロジェクト等で試してみるのも良いかもしれません。

FAQ

はてブコメントや Twitter を眺めているときちんと伝わっていないと思われることがあったので簡単に回答します。なお、僕と完全に意見が同じわけではないですが、 @omochimetaru同じ趣旨でより多様な質問に丁寧に回答しているので、下記で足りなければそちらも御覧下さい。

Q. null 安全がそんなに良いものならとっくに普及しているはずでは?

A. 良いものでもすぐに普及するとは限りません。今では当たり前になっている GC やオブジェクト指向やラムダ式、高階関数が多くの言語に取り入れられるまでにどれだけの時間がかかったか思い出して下さい。

Q. null 安全を導入したらよくわかってない初心者がひどいコードを書いてより悲惨なことになるのでは?

A. この可能性は否定できないですが、この論法では新しいものは何も導入できないことになってしまいます。使いこなすのが著しく難しいものならその指摘も的外れではないですが、 null 安全 なコードを書く上で気を付けなければならないことはそれほど多くなく( nullable のまま値を取り回しすぎない、コンパイラを黙らせるだめに ?: 等でテキトーに値を与えない等)、たとえばオブジェクト指向と比較しても難しくないと思います。

Q. null オブジェクトじゃダメなの?

A. null オブジェクトには致命的な問題があります。それは null 、つまり値が存在しないときに期待される挙動は文脈によって異なるということです。例えば、こちらのコードの 58 行目に渡すべき適切な null オブジェクトを実装することはできません。 Integer が存在しないときに、それが 0 として振る舞って欲しいのか、 1 として振る舞って欲しいのかは文脈によって異なるからです。

Q. 安全性と生産性のトレードオフでは一概に良いものと言えないのでは?

A. null 安全安全性と生産性を同時に高めるもの です。詳しくは本文に追記したのでそちらを御覧下さい

Q. null でエラーを扱うとエラー情報を扱えず辛くない?

A. エラー情報が必要な場合には null でエラーを表すべきではないと思います。また、 null のまま取り回すとどこでエラーが発生したのかわからなくなってしまうので、エラーになる可能性のある関数が nullable を返す場合には、できるだけ早い段階でエラー処理をすべきです。

Q. Option について紹介するならモナドに触れるべきでは?

A. モナドについて詳しい人には改めて説明する必要はないと思いますし、詳しくない人に対してモナドの話まですると混乱するだけかと思い、本投稿では省略しました。

Q. ?. があるから◯◯も null 安全では?

A. ?. やそれに類似した仕組みを持つ言語は多いですが(そして、 null 安全 な言語の ?. も Groovy や CoffeeScript, Dart などの動的型付言語由来だと思いますが)、 null 安全 の肝はコンパイル(静的解析)時に null によるエラーを検出できることなので、 ?. だけあっても null 安全 にはなりません。



  1. null 安全 (または null 安全性 )は null safety の訳語です。 null safety という用語は主に Kotlin コミュニティで使われているもので、言語によって OptionalOption, Maybe, nullable type などの型で実現できる、 null が原因で実行時エラーを起こさない性質のことを表します。そのような性質を表す言葉として null 安全 はぴったりだと思うので、本投稿では null 安全 という呼び方を採用します。 

  2. Shipping Rust in Firefox 

  3. レガシーコード改善ガイドによるレガシーコードの定義 

  4. non-nullable と言うこともありますが( TypeScript など)、本投稿では Kotlin 流に non-null で統一しています。 

  5. アントニー・ホーア - Wikipedia 

  6. Kotlin の if 文は式なのでそのまま戻り値を代入することができますが、他言語のプログラマには馴染みのない使い方なのでここでは文として使用しています。 

  7. Option.scala - scala/scala 

  8. Optional.kt - qoncept/kotopt: Kotlin で sealed class を使ってポリモーフィズムで SomeNone の挙動を切り替えた例 

  9. OptionalがSerializableではない話と使い方まとめ - きしだのはてな 

  10. JSR 305: Annotations for Software Defect Detection 

  11. Proposal for types in Harmony 

  12. Types (Experimental) - google/traceur-compiler 

  13. ES8 Proposal: Optional Static Typing 

  14. PEP 484 -- Type Hints 

  15. mypy 

  16. [翻訳] Python の静的型、すごい mypy! 

  17. mypy: Type checking Python 2 code 

  18. mypy: Experimental strict optional type and None checking 

  19. PEP 505 -- None-aware operators 

  20. RubyKaigi 2016 基調講演レポート Ruby3 Typing 

  21. Suggestion: "safe navigation operator", i.e. x?.y 16 - Microsoft/TypeScript 

  22. Getting started with Android and Kotlin 

  23. Android開発を受注したからKotlinをガッツリ使ってみたら最高だった 

  24. 型なき世界のためのflowtype入門 

  25. What is Gradual Typing: 漸進的型付けとは何か 

  26. Mypy syntax cheat sheet 

  27. Codcore/amethyst: Crystal 版の Rails 

Comments Loading...