Hatena::ブログ(Diary)

あどけない話

2013-03-08

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

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

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

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

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

僕は静的な C 言語から、動的な PerlLispJavaScript を経て、現在では静的な 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 を使っている。

おまけ

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

とおりがかりとおりがかり 2013/03/08 17:28 「柔軟なインターフェイスの変更」について, 論点がズレているように思いました.
・動的型付け言語の方では, 型を判別して処理を"足した".
・一方静的片付け言語では, インターフェイスを変えるべきだ.
言語の向き不向きを考えるときに, そのユーザーの設計思想を比較するのはフェアじゃないと思います.
やろうと思えば後者も「新しいインターフェイスをつくる」方法が残っています.
その方針で実装をした場合の手間は, 動的型付け言語の場合と大差ありません.
どちらも結果的に, 同じ関数名で違う型のデータを与え, 違う処理を行います.
JavaやC++では関数のオーバーロードで実現できますし,
Haskellの場合は…Moduleを使うんでしょうか?よくわかりません.

逆に動的型付け言語の場合においても,
即座にインターフェイスを変更する, という決定をすることもできます.
そういうわけで, 特段どちらが"柔軟"なのかは, 利用者の意志決定によるもので,
言語としての"柔軟性"はこの例では生きてこないものだと思います.

kazu-yamamotokazu-yamamoto 2013/03/08 17:32 この議論は、shiro さんのブログを呼んで頂けると、詳しく分かると思います。

http://blog.practical-scheme.net/shiro/20130227-equibillium

groundground 2013/03/08 23:51 ## 記述量

Haskellで型注釈を省略して書けるのは良いのですが、基本推奨されないように思います。
それよりも大規模なJSやRubyのアプリケーションなら、JSDocやRDocを書くから型を書くのと変わらないという方が個人的には正しいと思っています。

## テスト

関数型で言及されているテストは純粋な関数に対して言及されてることがほとんどで、データを永続化するような副作用のある関数に対して本当に簡単に書けるのか気になってます。

仕事ではRuby on Railsを利用していますが、正直、型のある言語が使いたいです。

kazu-yamamotokazu-yamamoto 2013/03/09 08:04 できあがった Haskell のコードには、トップレベルの型注釈を付けることが推奨されています。しかし、どのようにコードを書くかに特にルールがある訳ではありません。

関数型の副作用のある関数のテストが、他のパラダイムと比べて、テストを書きやすということはないと思います。いわゆる xUnit でテストケースを書いて行きます。(副作用のあるコードに QuickCheck を応用して race を 5 つ発見したというすごい事例もあるんですが、僕はまねできません。)

これは将来の話ですが、最近 Free Monad というアイディアが広まっており、これは純粋データから IO を生成できます。そこで、IO に変換される前の純粋データを QuickCheck などでテストできる可能性がでていました。もうやっている人はいるかもしれませんが、Haskell の多くがやっている状況ではありません。

通りすがり通りすがり 2013/03/09 20:38 > インターフェイスを変更できない状況で、柔軟に対応できる

これはアスペクト指向などで解決できますし、
それ以外については例えばMesaなどで実現しています

通りすがり通りすがり 2013/03/10 06:26 動的型言語vs静的型言語というのはそもそも誤った出発点です
実用言語は両方のよいところを取り入れるべきですから
静的に型付けできない、あるいは難しい型理論の世話にならざるをえないのは一体どういう場合なのかを問題にすべきです

kazu-yamamotokazu-yamamoto 2013/03/10 07:49 > これはアスペクト指向などで解決できますし、
> それ以外については例えばMesaなどで実現しています

ここに挙げた簡単な例では、例えば Java のオーバーロードなどで対応できるのは知っています。この記事では、僕の静的型付き言語からくる偏見をなるべく排除するために、動的型付き言語の利点はなるべく認めて、静的型付き言語の利点はなるべく認めない方向で書いています。

動的型付き言語 A では X ができて、B では Y ができるという場合、X と Y は認めています。静的型付き言語の方は、Haskell でできなければ認めていません。OCaml ではできるという話も聞きますが、この辺りはよく理解していません。OCamler の方が記事を書いてくれることを期待しています。

ちなみに、Haskell にもアスペクト指向の研究が2つぐらいありますが、実用化されてはいません。

kazu-yamamotokazu-yamamoto 2013/03/10 07:52 > 動的型言語vs静的型言語というのはそもそも誤った出発点です
> 実用言語は両方のよいところを取り入れるべきですから
> 静的に型付けできない、あるいは難しい型理論の世話にならざるをえないのは一体どういう場合なのかを問題にすべきです

記事を書いて下さい。期待しています!

通りすがり通りすがり 2013/03/25 10:16 考えがまとまらないので羅列になってすみません

・Javaのようにダウンキャストがないと使い物にならない言語は静的言語とは呼びたくない
・適切な型を考えるのは難しいしさぼりたい。型理論をちゃんと理解していないとコンパイルエラーメッセージが理解できない。
・型理論難しすぎ。ランクN多相とか依存型とか…
・Phantom型は動的型言語では実行時エラーにするにしてもうまくいかないのではないか
・Erlangのホットスワップを可能にしているは、データに型情報などがついているからではないか。変数の片付けとはさしあたって独立しているような気がする。

kazu-yamamotokazu-yamamoto 2013/03/25 10:24 型理論を理解しなくても静的型付き関数型言語の型システムは使えます。ただ、エラーメッセージで警告される場所が、実際に問題がある場所とかけ離れていたりして、難しいことはあります。この辺りは、研究課題のようです。

データに型情報が付いているのは、多くの動的型付き言語に見られる一般的な特徴ですね。

kazu-yamamotokazu-yamamoto 2013/03/25 10:25 ランクN多相、依存型、Phantom型は、僕はあんまり理解していませんし、使いもしません。(人の書いたコードを読まないといけないことはありますが。)

Keita MizuochiKeita Mizuochi 2013/04/28 02:14 今さらという感じがするのですが、テストの段落について感じたことをコメントさせて下さい。

山本さんの書かれている
「関数プログラミングの場合は、差し替えたい部分があるなら、そこは引数にしておく。」
は、関数プログラミングの考え方でもありますがオブジェクト指向プログラミングの考え方でもあると思いました。例えばクラスベースのオブジェクト指向だと、差し変えたい部分を新しいクラスとして定義しそれを使うクラスのコンストラクタやメソッドの引数にするからです。

また、
「「実行時に呼び出すメソッドを変えたい」とは思わない。引数を変えればいいからだ。」
も同じことを言っているように感じました。オブジェクト指向において、コンストラクタやメソッドの引数を変えることは実行時に呼び出すメソッドを変えることにつながるからです。

A-11A-11 2015/02/17 00:45 動的型付け言語の生き残る理由は、殆どの静的型付け言語の型システムがチューリング不完全だからだと私は考えます。

静的型付け言語の評価項目の一つに、その言語の持つ型システムがあります。
型システムが強力であるほど、その言語は便利となります。
例えば、ある静的型付け言語では型に応じて手動展開しないと型チェックの通らないコードが、ランクN多相を導入した静的型付け言語では手動展開を省けたりします。

もし「究極の型システム」が存在するならその型システムは、任意の性質Fを満たすかどうかもチェックできるでしょう。
勿論これは、ゲーデルの不完全性定理を一般化した
ライスの定理 - Wikipedia
http://ja.wikipedia.org/wiki/%E3%83%A9%E3%82%A4%E3%82%B9%E3%81%AE%E5%AE%9A%E7%90%86
そのものであり、「究極の型システム」の型チェッカが相手ならば、無限ループやメモリ食いつぶしに陥らせるコードを書くことが可能です。
これによるコンパイラのクラッシュor応答不能を防ぐため、ほぼ全ての静的型付け言語は、「究極の型システム」の装備を諦め、どこかで型チェックをサボります。

この「型チェックのサボリ」の犠牲となったコードの扱いが、動的型付けと静的型付けで異なります。

静的型付け言語の売りは、漏れなき型チェックなのですから、サボったコードは問答無用で型エラー扱いとなります。
初期の静的型付け言語はランク2多相のコードを型エラーにするし、ランクN多相の言語はランクN+1のコードを型エラーにします。
結果プログラマは、ランクN+1多相の様な高度なチェック技法を身につけていても、その言語のサポートする型システムの範囲内のコードしか書けません。

これに対し動的型付け言語は、コンパイル時にチェックを諦めざるを得なかったコードでも実行を許します。
ランク1多相しかチェックできない動的型付け言語でもランク2多相のコードを動かせるし、そもそも大抵の動的型付け言語は、コンパイル時に何もチェックしないというかコンパイルしなくても、任意の型システムのコードを動かすことが出来ます。
プログラマは自分の使う型システムを、言語に制限されずに済むのです。

勿論、コンパイラによるチェックがないならば、型システムの魅力は殆ど失われます。
しかし、コンパイラの型チェックを行なうのに静的型付け「言語」である必要はありません。
既に多くの静的型付け言語で採用されている「型推論」の技術は、型アノテーションを持てない動的型付け言語にも応用できます。
チェックサボリについては「型エラー」ではなく「型警告」としてプログラマに通知すれば、プログラマ自身がチェックしなければならない範囲を大きく絞り込むことが出来ます。
あるいは、C言語コンパイラgccの「-Werror」オプションの様に、型警告を型エラーとして扱えば、同じコードが動的型付け言語ではなく静的型付け言語に化けます。
「型推論」と、チェックサボリ=「型警告」のない「究極の型システム」とを組み合わせれば、静的型付け言語と動的型付け言語の間に、違いは残りません。

裏を返せば、静的型付け言語と動的型付け言語の本質的な相違は、型チェックの不完全性に対する対応に集約されます。
型システムのヘボが原因のものまでプログラマのエラーと同じ扱いにするのは、動的型付けに対する静的型付けの修正不能な短所です。
現在のところ、静的型付け言語並みのコンパイル時チェックを持つ動的型付け言語を私は知りません。
しかし、その登場は時間の問題だと私は信じます。
ならば、その時が来たら静的型付け言語は存在意義を失うでしょう。

ぺちぱーぺちぱー 2015/10/30 14:33 とても興味あるお話なんですが、レベルの低いPHPerには理解が難しい内容です。
概念論的・抽象的な記述ではなくて、具体的にたとえばPHPのサンプルコード(掲示板とかカートとか具体的なサンプルアプリ)を例示しておいて、
ここがこうだから動的言語の場合はこうで静的言語であればこうである、関数型ならこうしたら利点がある、というような比較をしないと理解しづらいです。

ターバン野郎ターバン野郎 2016/02/23 22:19 pythonのmypyには頑張ってもらいたいものですね

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証

トラックバック - http://d.hatena.ne.jp/kazu-yamamoto/20130308/1362724125