静的型付き言語プログラマから見た動的型付き言語

およそ20年前にAlan Kay の講演をきいたことがある。印象に残ったのは、彼が引き合いに出した McLuhan の言葉だ。

I don't know who discovered water, but it wasn't a fish.
(拙訳)誰が水を発見したかは知らないが、発見者が魚でなかったことは確かだ。

誰しも信念という水の中を泳ぐ魚のような存在だ。思い切って飛び跳ね空気に触れてみなれば、自分が信念という水の中にいることに気付かない。

ある手法の利点を語るには、その手法の欠点や、他の手法の利点や欠点とできるだけ客観的に比較しなければ説得力がない。ただ、これを実践するのは難しい。この記事では、客観的になれているか自問自答しながら、動的型付き言語と静的型付き言語について比較してみようと思う。

僕は静的な C 言語から、動的な Perl、Lisp、JavaScript を経て、現在では静的な Haskell を主に使っている。だから静的型付き言語から動的型付き言語に移った人の気持ちも分かるつもりだし、その逆も分かるつもりだ。(いや、分からないことも多いんだろうけど。)

一口に動的型付き言語、静的型付き言語といっても実にさまざまで、本当は十把一絡げに議論することはできない。しかし、細部にこだわると言いたいことが伝わらなくなると思うので、細かい点には目をつぶって頂けると嬉しい。

以下では僕の立場上、動的型付き言語のプログラマが主張する動的型付き言語の利点や静的型付き言語の欠点が、静的型付き「関数型」言語のプログラマからどういう風に見えるのかという話になる。

すべての動的型付き言語のプログラマがそういう主張をしているとは思っていないし、静的型付き関数型言語を一括りにするのも乱暴なのは承知している。「なんか違うな」と感じたら、建設的なブログ記事をなどを書いて議論して頂けるとありがたい。

スクリプト

「動的型付き言語はコンパイルせずにスクリプトとして走らせることができて楽だ」

静的型付き言語であってもコンパイルして実行するというラッパープログラムを書けば、スクリプトのように実行できる。実際、静的型付き関数型言語では、そいうラッパープログラムを提供しているものが多いし、よく使われている。

以下は Haskell の例:

% runhaskell HelloWorld.hs
Hello, world!

記述量

「動的型付き言語では型注釈を書かなくてもいいので記述量が減って楽だ」

うまく設計された静的型付き言語では、型注釈と定義が分離されている。以下は Haskell の例:

repl :: Int -> Char -> [Char] -- ここが型注釈
repl 0 _ = []                 -- これ以降が関数の定義
repl n c = c : repl (n-1) c

だから、型注釈は省略して書ける。

repl 0 _ = []
repl n c = c : repl (n-1) c

静的型付き関数型言語の多くは、型推論という機能を持ち、定義から型を推測する。そこで必要であればコンパイラが推論した型をプログラミング環境が自動的に挿入してくれる。

逆に、Haskell では型を先に書いて、型レベルで設計することもある(ちょうどオブジェクト指向のプログラマが UML を書くように)。

repl :: Int -> Char -> [Char] -- ここが型注釈
repl = undefined              -- 型検査をごまかす関数定義

僕の場合は

  • 簡単な関数はいちいち型注釈を書きたくないので、定義から書いて型注釈は自動挿入
  • 難しい関数は型注釈を書いて型のレベルで設計し、後から定義を書く (この場合、型が実装を導いてくれる)

のように使い分けている。

対話環境

「動的型付き言語では対話環境があるので開発効率がよい」

対話環境があれば開発効率がよいという主張には完全に同意。でも、静的型付き関数型言語の多くにも対話環境がある。個人的な意見では、対話環境がある言語でもリテラルが充実してないのであれば、対話環境の利点が半減していると思う。

たとえば、Haskell で定義されたデータは、それがそのままリテラルとなり、関数の入力にも使えるし、出力の際もそのままの形で表示される。以下 Haskell で木を定義する例:

data Tree a = Leaf a | Node (Tree a) (Tree a) deriving (Show,Functor)

以下は、定義した木を対話環境 GHCi で使う例:

% ghci Tree.hs
> fmap (+1) (Node (Leaf 1) (Leaf 2))
Node (Leaf 2) (Leaf 3)

入力も出力も定義がそのままリテラルになっていることが分かるだろう。

テスト

「動的型付き言語はテストしやすい」

これは意味が分からなかったのだが、たとえば「呼び出すメソッドを実行時に決定する機能を使ってモックを差し込むことがやりやすい」ことなど言っているのではないかと教えて頂いた。

たしかに静的型付き言語のオブジェクトをテストするのに比べれば、動的型付き言語のオブジェクトをテストする方がやりやすいだろう。でも、それはオブジェクト指向で考えているからではないか(十把一絡げにしてすいません)?

関数プログラミングの場合は、差し替えたい部分があるなら、そこは引数にしておく。だから、テストのために「実行時に呼び出すメソッドを変えたい」とは思わない。引数を変えればいいからだ。

少し話がそれるけれど、関数型言語では副作用のある関数とない関数を分けて書く習慣を身につけているプログラマが多い。副作用のない関数は、引数だけから結果が決まるのでテストしやすい。副作用のある関数でも、上記のように動作を変えたい部分は、引数にしてテストしやすいように工夫する。

世の中のコードはうまく設計されているものばかりじゃない。うまく設計されてないコードを手渡されたときに、実行時に呼び出すメソッドが変えられるのでテストしやすい。そういう意見には一理あると思うけど、僕だと(変更が許されるなら)リファクタリングしてテストしやすく変更すると思う。その方が保守しやすくなるからね。

ちなみに、副作用のない関数に関しては、テストケースを自動的に生成するというテスト手法があって、静的型付き関数型言語ではよく使われている。この種のテストでは、関数が持つべき「性質」を記述する。以下、ある方法で符号化して復号化すれば、元に戻るという性質の例:

prop_encodeDecode :: String -> Bool
prop_encodeDecode xs = decode (encode xs) == xs

対話的にテストしてみる:

> quickCheck prop_encodeDecode
OK, passed 100 tests.

100個の乱数が生成されて、すべてテストを通過したことが分かる。値の生成方法にも、乱数的に生成する方法の他に、ある大きさを網羅する方法などがある。

テストの世界は広くて深い。ある局面で「テストしやすい」と言われても「あなたの場合だとそうなんでしょうね」という感想になることが多い。

メタプログラミング (追記)

「動的型付き言語は、実行時に環境に応じたメタプログラミングができる」

これは、たとえば実行時に DB のスキーマを取ってきて、必要なメソッドをメタプログラミングで自動生成することなどを言っている。しかし、静的型付き言語でも、コンパイル時に DB のスキーマを取ってきて必要な関数/メソッドを自動生成できる。

スキーマが変わる度にコンパイルするのは面倒と感じるか、コンパルしてある程度の品質を保証したいと思うかは、プログラマ次第だ。

不完全

「動的型付き言語は不完全なコードを実行できる」

おそらく動的型付き言語の利点は、この言葉に集約されると思う。ただ、これはかなり大雑把な表現なので、もう少し厳密に議論したい。

まず、静的型付き言語でも不完全なコードは実行できる。たとえば、この C 言語のコード;

#include <stdio.h>

void main () {
}

何も実装してないから(仕様を満たさないという意味で)不完全だけど、コンパイルもできるし、実行もできる。別の例として undefined を使った不完全でも実行できる Haskell コードを最初の方で示した。

という訳で、言いたいことは分かるんだけれど、もう少し的確な表現を使う方がいいと思う。以下では、このテーマを細分化して議論していく。

少ない手間で不完全

「動的型付き関数型言語では、少ない手間で不完全なコードを記述できる」

「少ない手間」とは、ある部分であるメソッドを呼び出しているのに、そのメソッドの定義は一文字も書かれていないなどを言っている。

ただ、静的型付き言語では、定義されてないメソッドや関数があれば、プログラミング環境が関数定義の雛形を自動挿入できるので、動的型付き言語の利点とは言えないと思う。

インターフェイスの柔軟な変更

「動的型付き関数型言語では、公開しているインターフェイスの互換性を保ちながら、新しい仕様に変更できる」

たとえば、第一引数が整数で第二引数が文字列である関数を公開しているとしよう。利用して行く過程で、順番は逆の方がいいと気付いた。

動的型付き言語であれば、実行時に第一引数の型を検査し、整数なら古い仕様で、文字列なら新しい仕様で動くということが可能だ。もし、自分が提供するライブラリの顧客が、顧客側のコードの変更を認めてくれないのであれば、こういう対策が必要になる。こういった状況では、動的型付き言語の方が有利。

ただ、静的型付き言語のプログラマは、このような変更がなされたら、速やかに新しいインターフェイスに移行するべきだと考えている。コンパイラは、変更すべき箇所をすべて見つけてくれるので、顧客が変更を認めてくれないという状況でない限り、変更は可能だし、変更の手間もかからない(ただ必要な人への連絡は手間だ)。

これは一長一短である。

静的型付き言語のプログラマにとっては、変更すべき箇所を指摘するツールがないのは不安に感じるだろう。(静的解析ツールを提供している動的型付き言語もあるが、言語処理系に組み込まれていないと、言語処理系の変化に取り残されてることがよくある。)

動的型付き言語のプログラマにとっては、変更に対する柔軟性がないのは不安に感じるだろう。

ユーザインターフェイスとしての言語

「動的型付き言語は、あるシステムを制御するユーザインターフェイス言語に向いている」

あるシステムを制御するユーザインターフェイス言語とは、たとえば Emacs を制御する Emacs Lisp やブラウザを制御する JavaScript が挙げられる。これらの言語は、たとえコードが不完全でも動き続けなければいけない。だから、あるシステムを制御するユーザインターフェイス言語の設計者は、自然と動的型付き言語を選択するだろう。

ただし、動的型付き言語は品質の面に不安があるので、コード生成を利用しているプログラマもいる。最近では、コードを静的型付き言語で記述しておき、コンパイルである程度の品質を保証してから、JavaScript を生成するという方法が一部で流行っている。こういう使い方をするプログラマにとっては、JavaScript とはブラウザを制御するためのアセンブリ言語である。

ホットスワップ

「動的型付き言語では、コードのホットスワップができる」

動き続けなければならないサーバのコードの一部をバージョンアップしたい。動的型付き言語の Erlang で書かれたサーバでは、コードのホットスワップが当たり前のように実践されている。

静的型付き言語でもやってやれないことはないと思うけど、動的型付き言語の方が圧倒的にやりやすいのは間違いない。

デバッガでのプログラミング

「動的型付き言語では、変わりゆく世界を動的に捕まえてプログラミングできる」

コードを書いたときには想定してなかったか、気付いてはいたがどう対処すればよいのか分からずにそのままにしていた箇所があるとしよう。優れた動的型付き言語では、そこを踏むとデバッガが起動する。そして、デバッガの中で対処するコードを書いて再実行すれば、あたかも前からコードがあったかのように動いてくれる。

優れた動的型付き言語の優れたプログラマは、もう再現できないかもしれない千載一遇のチャンスを無駄にしたくない。だから、このようなプログラマにとっては、デバッガなどのプログラミング環境がプログラミング言語だ。

このようなプログラミングを実践しているプログラマは、本当に尊敬する。残念ながら、静的型付き言語では、このようなプログラミングはできないか、相当無理がある。

まとめ

僕の考える動的型付き言語の利点をまとめると以下のようになる。

  • インターフェイスを変更できない状況で、柔軟に対応できる
  • 不完全な場合でも動き続けなければならない状況で動き続けられる
  • コードのホットスワップがやりやすい
  • 変わりゆく世界を動的に捕まえてプログラミングできる

欠点は、以下のような点:

  • たくさんテストを書かないと品質が保証できない
  • 不整合な部分を発見する手段が提供されてないことが多い

僕の場合は、動的型付き言語の欠点の方が大きいので、静的型付き言語 Haskell を使っている。

おまけ

動的型付き言語のプログラマが知らないかもしれない世界として、静的型付き関数型言語では「型が実装を導いてくれる」ということが挙げられる。もし知らないのであれば、実際に体験してみると静的型付き言語に対しての印象も変わるかもしれない。