Why Type Systems Matter

Matthias Endler

なぜ型システムは重要なのか

原文は Matthias Endler により に公開されました。 このブログを購読する

私はこれまでPythonやPHPのような動的型付け言語でコードのほとんどを書いてきましたが、Rustを触ってみて以来、静的型システムに情熱を抱くようになりました。
それは私にとってとても自然に感じられるようになりました。まったく新しい自己表現の方法のように。

型は助けてくれる

型があれば、保証することや期待することを伝えることができます。機械に対しても、他の開発者に対しても。型は意図を表現するのです。

プログラマとして、あなたも型について何となくの直感を持っているはずです。

sentence = "hello world"

sentenceは文字列だと推測できるでしょう。なにしろクォートで囲まれていますから。型が別の場所から推論される場合は、少し厄介になります。

sentence = x

sentenceはまだ文字列でしょうか?うーん……わかりません。xの型次第です。もしかしたらxは数値で、だからsentenceも数値なのかもしれません。あるいはxは以前は文字列だったのに、リファクタリングの過程で今はバイト配列になっているのかもしれません。みんなで楽しい時間を過ごせますね。🎉

では、これはどうでしょう?

filesize = "5000" # Size in bytes

ここでは、ファイルサイズを文字列で表現しています。

これでも動くかもしれませんが、どこか落ち着かない考えです。
単純な計算でさえ、予期せぬ結果を招く可能性があります。

file1 = "5000"
file2 = "3000"
total = file1 + file2
print(total) # prints '50003000'

どうすれば直せるのか?

ファイルサイズは常に数値だと安全に想定できます。より正確に言えば、正の自然数でなければなりません。負のファイルサイズなど存在し得ないし、メモリの最小単位は1バイトです(ごく一部の特殊なシステムを除いて)。そして離散的な機械を扱っている以上、コンピュータが扱えるファイルサイズでしかあり得ないこともわかっています。もしこれらすべてを正確な方法で表現できたら……?

ここで型システムの出番です。
Rustなら、sizeというフィールドを持つFile型を次のように定義できます。

struct File {
  name: String,
  size: usize,
}

usizeは、メモリへのポインタを常に格納できるだけの大きさを保証します(64ビットコンピュータではusize = u64)。これでsizeの型についての曖昧さはなくなります。不正なファイルオブジェクトを作ることすらできません。

// Error: `size` can't be a string.
let weird_file = File { name: 123, size: "hello" };

型システムは不正な状態を防いでくれます。自分で決めたルールを破ることを、単純に許してくれないのです。設計上の選択に責任を持たせてくれます。あえて言えば、それは脳の延長になります。しばらくすると、型チェッカーを頼りにするようになります。「コンパイルが通れば、動く」は強力なマントラです。

型は可読性を高め、文脈を与える

次のPythonのスニペットを考えてみましょう。

def filter_files(files):
  matches = []
  for file in files:
    if file.status == 0:
      matches.append(file)
  return matches

0は何を表しているのでしょうか?わかりません。文脈が欠けているのです!

次のようにenum型を定義すると、話は少し明確になります。

from enum import Enum

class FileStatus(Enum):
  OPEN = 0
  CLOSED = 1

上の例は次のようになります。

def filter_files(files):
  matches = []
  for file in files:
    if file.status == FileStatus.OPEN:
      matches.append(file)
  return matches

大規模なコードベースでは、0よりもFileStatus.OPENの方がはるかに検索しやすくなります。

注:ネイティブなenum型はPythonの歴史の中でもかなり遅い段階で導入されました。これは、型システムを強化することが可読性の向上にどう役立つかを示す良い例です。

異なる型を組み合わせると、魔法が起こる

型を賢く選べば、すべてのピースが突然ぴたりとはまります。どこからともなく、コンパイラがあなたの設計上の決定や、すべての型がうまく連携しているかをチェックし始めるのです。メンタルモデルの欠陥を指摘してくれます。これにより、リファクタリングの際に大きな安心感が得られます。

例えば、ソートについて考えてみましょう。ソートと聞いて、私がまず思い浮かべるのは数値のリストです。

sorted([1,5,4,3,2]) # [1,2,3,4,5]

これがハッピーパスです。では、これはどうでしょう?

sorted(1)

おっと。これは動きません。1は単一の数値であって、コレクションではないからです!sortedに渡す前に型のチェックを忘れると、プログラムの実行中にエラーが発生します。

sorted([1, "fish"])

Python 2では、これは[1, 'fish']という結果になります(文字列は長さで比較されるため

追記:Redditユーザーのjcdyer3が指摘したように、理由は比較不可能な型同士を比較した際に型でソートされるため、すべての整数がすべての文字列より前に来るからです。これはCPythonの実装詳細です)。

Python 2によれば1 < fish
Python 2によれば1 < fish
出典:Freepik提供のイラスト

Python 3以降では、これは例外をスローします。

TypeError: '<' not supported between instances of 'str' and 'int'

ずっと良いですね!エラーの原因が一つ減りました。ただ問題なのは、これが実行時に起こるということです。Pythonの動的型付けが原因です。静的型付け言語なら、これを回避できたはずです。

fn sorted<T>(collection: &mut [T]) where T: PartialOrd {
  // TODO: Sort the collection here.
}

一見怖そうに見えますが、実際はそうでもありません。

sortedという名前の関数を定義しており、collectionという一つの入力パラメータを取ります。

collectionの型は4つの部分から成り立っています。

  1. &は、コレクションを「借用」することを意味し、所有はしないということです。関数がリターンした後も、それは存在し続けます。クリーンアップされることはありません。
  2. mutは、コレクションが可変であることを意味します。変更することが許されています。
  3. [T]は、入力としてリスト/スライス/ベクタを期待することを示します。それ以外はすべてコンパイル時(プログラムが実行される前)に拒否されます。
  4. PartialOrdが魔法の隠し味です。これはtraitで、いわばインターフェースのようなものです。コレクション内のすべての要素T半順序でなければならないことを意味します。

これらすべての情報が、コンパイラが私たち自身の足を撃ち抜くのを防ぐのに役立ちます。そして、他を見ることなく関数の入出力を理解できるのです。

まとめ

  • 型は開発者に宿題をさせ、コードの保証や制限について考えさせます。
  • 型を制約と考えるのではなく、欠陥のあるメンタルモデルから自分を守ってくれる安全網と考えてください。
  • 常に、自分の意図を最も正確に表現する型を選んでください。
  • 標準ライブラリに完璧な型がなければ、より単純な型から自分で作りましょう。

これらのルールに従うことで、私は魔法のように自分のアイデアの最もエレガントな表現へと導かれていることに気づきました。コードはよりイディオマティックになりました。

この記事は「muse-spark-1.2-contributor」を使用して翻訳されました。

コメント