タオルケット体操

サツバツいんたーねっとでゲームとかガジェットのレビューとかをします

型はみんなの心の中に存在しているんじゃない?

Sponsored link

はじめに

まずインターネットのお気持ち記事で「型がある」とか「型がない」とか言い始めたら、書き手がどういった意図でそのような言葉を選んだのか警戒しなければならない。

全部を語るとそれだけで5記事分くらいの分量になりそう*1なので割愛するが、世間的には「静的な型検査を行える」ことを指して「型がある」と呼称しており、その強弱とかランタイムの挙動については基本的に無視するという姿勢をとっているようだ。きちんと研究している層にとってはなんとも歯痒い定義なのかもしれないけれど、雑に語るにはとても便利な切り分けなので本記事も基本的にこれを採用する。

余談だけど、世間一般が語る「強|弱い型付け」の定義って恣意的すぎて意味不明だわとおもってたんだけど、いまWikiで確認してもやっぱりどういう基準で決めてるのか意味不明。たぶん時代遅れの概念なんだとおもう。

変数の型が静的なのは我々にとってごく自然なこと

例えばノベルの主人公がいるとする:

const mainCharacter = Person(/* */);

この主人公が定義された瞬間から最後まで Person のインスタンス(あるいはPersonの型定義を満たす存在)であるのはまさに自明だとおもう。
ちなみに今はあくまで例え話をしているのでここで叙述トリックとか前衛小説の話題を持ち出して話をまぜっかえすような腰抜けは突然キッチンから飛び出してきたセガールの投げつけたスパゲッティコードが頭に突き刺さって死ぬことになる。

最初Personが代入された変数は死ぬまでPersonのままで、いきなり数字になったり三角関数になったりはしないわけだ。
もちろん変数を再利用するようなコードを書くやつもいるが、そういうスタイルで書かれたコードはゴミと呼ばれる。
小説のようなエンターテイメントは驚きが必要とされるが、厳密なドキュメントというのは退屈でなくてはならない。つまりまともなプログラマーにとって、変数は生まれてから死ぬまで同じ型、同じ意味を保持する。

関数の存在は人類には早いので型は静的なほうが優しい

変数の型に一貫性があるのが人間にとって自然であるように、関数の振る舞いにも一貫性があるべきだ。
ここでタイトルに「ごく自然なこと」と書かなかったのは、多くのプログラマーが引数の違いによって返す型が変わるような関数を作って「便利」だと称するのを見てきたからだ。僕には理解できないんが、どうやら関数に一貫性があるべきだという感性は人類が生得しているものではないらしい。

ただし、振る舞いに一貫性のない関数が沢山あるプロジェクトは一つの例外もなくメンテコストの増大によって崩壊するのをみてきた。
なので振る舞いに一貫性のない関数は「人類に優しくない」ということが言えるだろう。

例によっていかにも恣意的なサンプル擬似コードを書くならば:

/**
  * destinationが座標だったらpersonの持つ位置座標を動かします。返り値はvoidです。
  * destinationが地名だった場合、移動先のマップ情報をロードします。返り値はMapです。
  */
function travel(person: Person, destination: string | number): unknown {
  // blah blah blah
}

のような関数だろうか。
ここまで酷いのはなかなか見ないが、引数のパターン*2によって動作や返り値の振る舞いが変わるような関数は毒だ。

ちなみに型の表現力が高い言語だと「引数のパターンによって振る舞いが変わる関数」を静的に記述できたりするが、その場合でも抽象的なレベルでの一貫性が担保されている状態で行うのが基本となる。

補足:ジェネリクスを知らないという枷

僕はプログラマーとしてのキャリアをC#で開始した。
その時にはすでにLINQがリリースされてから数年が経っていたし、サンプルコードにも普通に出てくるものだったので無意識的にジェネリクスはただの基本的な便利機能の一つだと考えていたのだが、どうも世間には複数の立場が存在しているようだ。
というかJavaやC#を10年以上書いている人ですら「よくわかんない」「別にfor文で十分だし」……のようにアレルギー持ちの人が存在しているらしい*3

すごく乱暴な説明をしてしまえば、データ型とアルゴリズムをより疎結合に記述できるような仕組みで、要はただの便利グッズだ。コンパイラの作者は大変になるんだけど、コードを書く側からすればメリットしかない。

実際には一口にジェネリック・プログラミングと言っても単なる型変数の導入だけではなく、varianceであったり、ランタイムへの落とし方*4であったり、悪名高いC++のテンプレートであったり……と様々なアプローチがあり、その辺の事情について説明する文書を目にしたせいでややこしく捉えてしまっているかもしれない。
しかしプログラミングを始めて半年くらいの人間でも扱えた程度には枯れているし、日常の90%では先に述べたような事柄を意識する必要はない。
メジャーどころの言語では概ね互換性のある挙動をしており、いまとなっては使えない言語のほうが珍しい*5ので習得のコスパも良い。

さて、静的な型検査を行う言語の経験が浅い人が ObjectAny などの宣言で関数の型を握り潰す痛ましい事件が後を絶たないが、その99%はジェネリクスを知らないことに起因することが僕の調査によって明らかになっている。

99%というのは冗談にしても、やはりジェネリクスなしで型の安全性と抽象度の高い操作を両立させるのは不可能なので、動的言語圏出身の人というのはそういう場合に抽象度をとって安全性を捨てる傾向にある。
呼吸をするくらいの自然度でジェネリクスを扱えるようになれば静的に型検査されるからといって実装速度が落ちることはなくなるし、動的時代と同じくらいの汎用性をもったアルゴリズムを書ける。
むしろ動的言語時代は単なる美的センスでしか担保されなかったデータの抽象性を、generic typeという適切な縛りの範疇に留めて置けるようになるので型なし言語に出戻りしたときのコードのクオリティが向上すること間違いなしだ*6

さて、適当なサンプルコードを作ろうかとおもったが、ここまで広く世間で使われており、その応用の幅も広い概念をたった一つのくそみたいなサンプルコードで表現するのは不適切なのでやめた。
というか2021年にもなって、避けて生きる方が難しい程度に一般化した機能についてひと段落以上の文章をさいているのはおかしな話である。

ちなみに我々使う側にとってはただの便利機能だが、言語を設計する側としてはかなりめんどくさめの機能なので、その辺の取捨選択が生じることもあるとはおもう。TypeScriptにも最初はついてなかったし。

型検査は動作チェックの自動化

いまどきどんな言語でもlintツールなしで開発することはないだろう。
lintツールを使うことでスタイルの統一性だったり、危険なコードが存在しないかどうかのチェックをかなりの部分自動化できる。

プログラマーにとって重要な概念のひとつに「自動化」がある、ということに反対する人はまずいないだろう。
同様に、静的な型検査というのはある種、動作チェックの自動化を兼ねているという言い方が出来る。

例えばプロトタイピングの過程で筆が乗ってきて、気がついたら動作チェックすることもなく何時間もぶっ続けでコーディングしてしまうことはあるとおもう。
そういうときでも、一定以上の強さの型を持つ言語であればコンパイルが通った時点で正常系は一発動作するのが普通だ。

コンパイラが面倒をみてくれるので思い違いやしょうもないタイポで動作が止まったりすることはないし、試行錯誤のために書き換えるときも型の支援が生きる。また静的型付け言語のほうが一般的にIDEのリファクタリング機能も強力だ。

少なくともこの辺の生産性の高さに異論を唱える人はいないだろう。

じゃあなんすか、型なし言語にはなんの優位性もないっていうんですか

1. メタプログラミングがやりやすい

はい。
メタプログラミングって言葉も割と曖昧な用語なんですが、生成的なコーディングを行う行為だとしておきましょうか。

「型なし」言語であればメタプログラミングは実に簡単。
型の辻褄合わせは当然不要、オブジェクト周りへのアクセスもプリミティブな仕組みが用意されていることが多い。
代表的な言語だとLispとかだろうか。もちろんPythonのようなメジャーどころもinspectとかastみたいなものが標準ライブラリから普通に呼べて、それらを組み合わせればかなりダーティなことまで行える。import hookとかを組み合わせるとマクロの展開も出来る。
この辺の言語でメタプロが簡単に行えるのはインタプリタ方式の特性でもあるんだけど、カジュアルにそれが行えるのはやはり型の整合性を気にしなくてもいいというのが大きい。

TypeScriptのように実行時に型が消滅するような言語だと、生成的で型がつけにくいところだけ全てanyで書いて、入り口と出口のみ型をつけることでギリギリ「いいとこ取り」することは可能だけど、普通の言語は型情報の生成やらなんやらで記述は難解かつ長大なものになっていくし、コンパイルエラーも意味不明なものに陥りやすい。

メタプログラミングに関しては圧倒的な優位性がある……
例えばRuby on Railsのようなフレームワークに静的な型をつけるのはほぼ不可能というか、不毛だというのは概ね同意してくれるとおもう。あれはなんかいい感じに勝手にメソッドが生えてくるのが嬉しいのであって、ルールがかっちりとしていて全てが静的に決定されなければならないフレームワークであればあそこまで流行ってはいないだろう*7
なんとなく察せられる通り、僕はRailsがどちらかと言えば本ッ〜〜当〜〜〜〜〜〜〜〜ッッに大嫌いなので上の意見には大きなバイアスがかかっています。

そしてメタプログラミングはチームで開発している普通のプロジェクトでは使用禁止が普通だし、90%のケースで現代の言語は代替の道具を用意してくれているので、この話はここで終了です。

ていうかこの特性を真に活かせるのはLispくらいなのではないでしょうか(エアプの意見)。

2. プラグイン的なものへの活用

例えばゲームのMOD作成にはLuaやPythonなんかの言語が使われがちだし、エディタのプラグインを書くのにもよく使われる。
そういったグルー的な用途にはすごく向いているというのは直感的に理解できる。

エーアイ界隈でもnumpyとかのCで書かれたコードをPythonから呼び出しまくっているわけで、ほぼほぼプラグインといっても差し支えないですね(?)。

3. その他

お風呂に入りながら本気出して考えてみたんだけど本当に見つけられなかった。なんかある人は教えてください。
正直なところメタプロとかの特徴についてもF#のコンピュテーション式だとかみたいにマイルドだけどパワフルな機能を備えている言語も多いし、どうなんだろう……

ちなみに型のエラーが出ないからーーとか初心者でもーーとかいうのはなしで。そういうレベルの人はむしろ型検査の補助輪つけて勉強した方が良い。IDEも色々と教えてくれるし。
結局メタプロのやりやすさと"スクリプト"言語としての汎用性あたりが動的型の言語のメリットで、そこに大きな価値を見出すタイプのプロジェクトでもない限りは普通に静的な型検査ありきでコード書いた方が生産性高いよね。っていうのが僕の結論です。

まとめ

  • 人間は脳内で勝手に変数の型を静的に決定している

  • 関数という概念は人類には高度すぎるから静的型という枷をつけて優しくしたほうがいい

  • メタプロは危ない

Lintはあったほうがいいよね、程度のノリで型検査してくれたほうがいいよね。
僕の意見はそんな感じ。

まぁ雛鳥のときに最初に目にした言語の違いとか、性格の違いとかで違う意見を持つ人だっているとはおもう。
僕の感性は世間様そのものです、みたいなことはおもってないので異なる世界をもっている人はブログとかQiitaとかで書くともしかしたら有益な知見が発生するかもしれない。


ちなみに静的な型検査とか言っても厳密さとか機能のリッチさとか色々あるわけで……。
例えばnullとかnilを許容するようなショボいシステムだとむしろ記述量増えるばかりでありがたみがないな、とか。
代数的データ型とパターンマッチはないとQoL上がらんくない?とか。厳密すぎると実用が難しくなるとか。
そういうところまで話し始めるとまたややこしくなるし僕もついていけなくなってくるので今日はここまで!


*1:ついでに筆者は十分な知見をもっていない

*2:最低のパターンだとグローバル変数

*3:当時はSIerに所属していたので"LINQ禁止令"とかいうアホな命令と戦う必要があったのを思い出す

*4:例えばJavaとC#でも違う

*5:あの気難し屋のGo言語がとうとうサポートするということは、やはり難しい機能ではないということだ

*6:一度静的検査の世界で生きた後に無検査の世界に戻りたいと感じるかどうかは疑問だが

*7:そもそも無計画にモンキーパッチを濫用する文化とrequireの挙動のせいでRoRに限らずRubyのコードを静的に解析しようとする試み自体が不毛