Why Type Systems Matter

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

大規模なコードベースでは、FileStatus.OPENのほうが0よりずっと検索しやすくなります。

注:Python本来の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が魔法の核心です。これはトレイトで、インターフェースのようなものです。コレクション内のすべての要素T半順序付け可能でなければならない、という意味になります。

こうした情報がすべて、コンパイラーが私たち自身の足元をすくうようなミスを防ぐのに役立ちます。また、ほかの場所を調べなくても、関数の入力と出力を理解できます。

まとめ

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

このルールに従っていると、自分の考えを最も洗練された形で表現できるよう、魔法のように導かれていくことに気づきました。コードも、ずっと慣用的なものになりました。

原文は Matthias Endler により に公開されました。

この記事は「gpt-5.6-luna」を使用して翻訳されました。