Hatena::ブログ(Diary)

mizchi log

@mizchiの雑記帳

2013-03-03

動的型とか静的型の話の前に「作者の気持ち」を考えろ


自分の思考を整理する意味でも、件のアレについて考えたことを書いてみる。

この件に触れることはプログラマとしての中二病である。恥ずかしい。マジレス乙だ。
でも気づいたら5000文字も書いてしまったし、公開して酒のんで寝る。

型のフローは機械のためだけでなく、人間に対するものでもある


最近TypeScriptを書いている。こいつを使って、二次元座標上で二点間を求める関数、getDistanceを定義してみよう。

interface IPoint {
  x: Number;
  y: Number;
}

var getDistance = (a:IPoint, b:IPoint): Number =>
  Math.sqrt(Math.pow((a.x-b.x), 2)+Math.pow((a.y-b.y), 2));

このコードには、「getDistanceというメソッドが二点間の距離を求める」という挙動が型によって説明されている。
getDistanceの中身が圧縮されて読みづらいのは、半分ぐらいわざとだ。知らない定理ならともかく、義務教育を受けたなら知っているはずの三平方の定理を、ごちゃごちゃと説明的に書きたくない。このコードの意図自体は型定義に圧縮されている。


そもそも、TypeScriptはコンパイルされるとただのJSになるので、実行速度の最適化には繋がない。それでも型情報をもってるのは、開発効率のために型があったほうがいい、という観点からだ。

良い静的型のコードは、良いユーザーモデルを発想させるような、良い型の流れのシナリオを持っている。

型はコメントよりよほど雄弁である -- Haskell


上記のコードをHaskellで書いてみよう。
自分がHaskellをあまり書けないが好きな理由の1つに、美しい型機構があり、そしてカリー化で手続きを意味的に分割できるという点がある。
ややこしいモナドも出てこないので、簡単なコードだ。

-- Point型を定義する
data Point = Point {x :: Float,
                    y :: Float}

-- 関数の型を宣言する
getDistance :: Point -> Point -> Float
getDistance a b =
  sqrt $ ((x a) - (x b)) ** 2 + ((y b) - (y b)) ** 2

-- 第一引数を(Point 0 0)で部分適用。常に原点からの距離を求める関数として定義する
getDistanceFromOrigin = getDistance (Point 0 0)

main = do
  let a = Point 3 2
  let b = Point 5 5
  print $ getDistance a b
  print $ getDistanceFromOrigin a

(Haskell Wayというものを理解していないので、もしかしたら酷いコードかもしれない。が、サンプルのために許してほしい)

型定義と実装が別の箇所で行われている。意味を損なわない名前が正しく定義されている場合、関数名と型定義がドキュメントの役割を果たす。
このコードをよむとき、mainから読み始め、そして getDistance を目にした時に、 Point -> Point -> Float という型の流れだけを見ればいい。
この例を見る限り、型はコメントよりよほど雄弁だ。

Roy という言語


最近個人的に期待している RoyというHaskell系 Altjs(JSにコンパイルできる言語)は、次のように記述できる。

type Point = {x: Number, y: Number}
let getDistance (a:Point) (b:Point) =
  Math.sqrt (Math.pow (a.x-b.x) 2)+(Math.pow (a.y-b.y) 2)

let a : Point = {x: 3, y: 2}
let b : Point = {x: 5, y: 5}
console.log (getDistance a b)

Haskellよりはゆるふわだが、JSらしい記述を邪魔せず、型制約を取り入れることに成功している。

なお、この節はただのRoyの紹介である。
pufuwozu/roy · GitHub https://github.com/pufuwozu/roy

静的型が嫌われる例 -- Java


Javaは全般的に手続きがダサいというのはある程度共有された事実だろう。
もちろん、歴史的経緯があるのはわかるが、それにしてもダサい。
僕が初見でダサすぎてびっくりした、PrintWriterを使いたい場合こうなる。

File file = new File("/path/to/file");
FileWriter filewriter = new FileWriter(file);
BufferedWriter bw = new BufferedWriter(filewriter);
PrintWriter pw = new PrintWriter(bw);
pw.println("content");
pw.close();

このコードで、僕らがやりたいことは1つ。「ファイル名を指定して文字列を対話的に書き込みたい」というpw.printlnだ。
そこに至るまでのデータを作る過程は、やりたいことに対して本質的ではない。勿論、組み込み型なので抽象レベルは低いほうがいいが、JavaはLLのパッケージ群のように外部ライブラリを気軽に呼び出してラッパーを作るという文化がないので、この手続きは覚えておかないといけない。


Pythonの組み込み関数を使って同等のことをやるとすると、こうなる。

file = open('/path/to/file')
file.writelines(['content'])
file.close()

openがグローバル名前空間に属しているという気持ち悪さはあるとしても、その意図は明確だ。
writeleinsを呼びたい。ユーザーがやりたい意図を、APIが可能な限り阻害しない。

この例から考えるに、ライブラリ実装者は可能な限りユーザーのメンタルモデルを阻害しないAPI体系をつくることを意図すべきだ。


このJavaのような、本質と関係ないデータフローばかり見せられた人が、静的型は糞だ!って結論に至るのは痛いほど理解できる。僕も最初に学んだ言語はJavaで、「プログラミングの楽しさ」に目覚めたのはPythonだった。

コードがその使用者に対する説明ではなく、あくまで計算機上のデータ本位でしか見てないというケースで、この問題が発生する。

僕らは機械に対してだけではなく、人間に対してコードを書かないといけない。それを忘れた瞬間、コードは邪悪な熱量を持ち始める。

マインドモデルをコードに書くか、文化に従うか、自己完結するか


静的型定義のメリットは、上に述べたようにデータフローを明示化することだった。
じゃあ、全部静的型にすべきかというと、そういうわけでもない。


昨日はjQueryのコードリーディングの勉強会に出ていたのだが、src/core.js に次のようなヘルパ関数があった。

// Multifunctional method to get and set values of a collection
// The value/s can optionally be executed if it's a function
access: function( elems, fn, key, value, chainable, emptyGet, raw ) {...

もうコメントと引数の数からして嫌な予感がするだろうが、このメソッドの振る舞いを簡単に説明すると、
$('#foo').attr('class', 'bar') を、$('#foo').attr({'class': 'bar'}) というようにハッシュで一括にアクセスできるようにしたり、$('#foo).text('str') は 値を更新するから文字列返すよ、この場合メソッドチェーンできないよ、というのを、この便利関数一個で対応している。

おそらくDOM操作系メソッドが多すぎるせいで、コード量の圧縮のために、このような抽象化をする必要があったのだろう。jQueryの特殊な事情に起因するものだ。(それにしても酷いとは思うが)。
これは jQueryの開発者同士は共有すべきだが、ユーザーに提供するメソッドではない。ユーザーはその文脈を共有する必要がない。あなたがもしjQueryの開発者なら、その文化に対する学習コストが発生するが、それによって開発効率の恩恵を受けることができる。


ひとりプロジェクトで好き勝手にコード書いて、その結果全部動的型でいいんじゃないの?となるのは自分の中にあらゆる実装ケースに対するマインドモデルが自分の中で完結しているからだ。チーム開発ではない場合、それも許される。僕も趣味でPythonしか書いていないときはそうだった。

でも、チームが徐々に大きくなっていくとどうだろう。僕は3つの要素があると思っている。

  • 言語理解
  • ライブラリ理解
  • プロジェクト理解

そもそも実装方法がわからないとき、アルゴリズムやデザインパターンからマインドモデルを新規に学習する必要がある。それをオレオレ手法をとられると、その学習コストが跳ね上がって、属人性が高いものになる。優秀なマインドモデルから作られたライブラリは理解しやすいが、そうでないライブラリの思考に追従するのは悲しいものだ。

一般的に、マイナーな言語コミュニティほど平均的な技術力が高い


僕は Ruby は学習コストが高い言語だと思っている。言語仕様はそれほどでもない。が、その背景、哲学の量が半端ない。僕はRubyを「適当に」使うことはできるけど、「正しく」書くことはできない。僕が書いたRubyにはどんどん斧が飛んでくる。(Rubyistが多い会社なので要求水準が高い)

良い習慣を身につけることは、プログラマとしてあるべき正しい姿だ。だが、あらゆる面でRubyWayなコンテキストを共有していないと、舞台に上がることすらできないのは、はたして初学者に対して優しいと言えるだろうか。敷居が低い言語だと言えるだろうか。

僕は、喧伝されるRubyの生産性の高さみたいなものは、プログラミングの楽しさみたいなものを担保に、個々人に対して要求水準を上げて成り立ってる部分が大きいと思っている。別にRubyがきらいなわけではない。Java, C++, PHP以外のマイナー言語の生産性を喧伝する際に一般的に当てはまることだ。


Rubyの哲学を知らない人が業務としてRubyを使わないといけない時代がきた場合、その担保は崩れた時にどうするんだっていうのはいつも考えておく必要があるのだろう。HackersNewsで、Railsはスタートアップ界のJavaだ、っていう記事もあった。そういう時代は案外近いのかもしれない。

Rails, You Have Turned into Java. Congratulations! | Discursive http://discursive.com/2013/02/19/rails-you-have-turned-into-java-congratulations/


同じ文化を共有していればいるほど、命名には文化があって、それはデータの流れの情報すら内包している。その場合、動的型で型以外のロジックのコアについての生産性を上げることを目標とするのはおかしくない。ただし、全員が共有していれば、の話だ。

まとめ


コードの文量が増えれば増えるほど、なんにしろ破綻リスクは目に見えて大きくなる。言語自体の文化だけではなく、根幹ライブラリの理解、プロジェクト内の抑える文脈が増えていく。僕らはそれを抑える必要がある。

1万行のコードに対して、人間の短期記憶はあまりにも小さい。型情報は定義やIDEを通して僕らに語りかけてくる。データモデリングによって次の実装が創発される。正しい静的型プロジェクトはそうであるべきだ。

動的型で巨大なプロダクトを作る場合、小さいモジュールで適切に分離し、認知負荷を抑える。RubyやNodeが役割1つごとにライブラリに切り出すのは、再利用性ってのもあるし、インターフェースを適切に切ってモジュール間のコミュニケーションを最小にするテクニックだ。連綿と受け継がれてきたUnix文化だとも言える。


と、だいたいこんな感じのことを考えていた。
長々と書いたが、オチが他の記事と一緒でつまらん。それ一番言われるから。
なお、この文章はポール・グラハムみたいな文章を書きたくなり、僕の中のポール・グラハム的成分によって書かれました。

nogitsune413nogitsune413 2013/03/03 13:59 「静的型が嫌われる例 -- Java」に共感しました。Javaでは、ちょっとした処理を書くのにも、いつの間にかコード量が多くなってしまうと思います。

ruiccruicc 2013/03/03 15:25 s/第一引数を(Point 0 0)でカリー化/第一引数に(Point 0 0)を部分適用/

Haskellの関数はデフォルトで全てカリー化されています(curried functions)。
カリー化(currying)は複数引数関数を一引数関数にばらすことであり、
例えば2引数関数(というものがHaskellに無いのでtupleで表す)に以下のcurry関数を適用することです。
curry :: ((a, b) -> c) -> (a -> b -> c)
カリー化の結果、部分適用が出来る様になります。

phperphper 2013/03/03 16:58 一般に静的型の利点を否定するコメントではありませんが、getDistance a b が距離を求める関数であることは、型よりも関数名がより雄弁に語っています。
逆に、関数名も型も、それがユークリッド距離であるかについては何も語っていません。

mizchimizchi 2013/03/03 18:50 > ruicc
部分適用に修正しました

> phper
ユークリッド距離であることは省きましたが、二点間の距離といった場合、普通はユークリッド距離を指すという文脈を前提にしています。マンハッタン距離なら getManhattanDistance になると思います。

jmukjmuk 2013/03/04 06:08 一般に同一のソフトウェア内での距離尺度は単独で、場合によってユークリッド距離になるかマンハッタン距離になるか変わり、混在するような環境はまれだと思いますが、そのような場合にはfloatなどをラップした EuclideanDistance 型と ManhattanDistance 型を定義して、両者は比較不可能とするだけの話でしょう。つまり、必要であれば型に語らせることは当然可能です。

majiangmajiang 2013/03/04 10:49 > sqrt $ ((x a) -(x b)) ** 2 + ((y b) -(y b)) ** 2
sqrt $ ((x a) -(x b)) ** 2 + ((y a) -(y b)) ** 2 ですね。

warufuzaketaichiwarufuzaketaichi 2013/03/04 12:12 Javaに関する部分が余りに酷いのでエントリを書きました。内容の修正を期待します。https://gist.github.com/taichi/5079626

kwatchkwatch 2013/03/05 00:00 Pythonのコードはopen()に'w'をつけないと動かないし、こう書いた方がいいかも。
with open('/path/to/file', 'w') as f:
 f.write('content')

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


画像認証

トラックバック - http://d.hatena.ne.jp/mizchi/20130303/1362286050