浜松Ruby会議01に参加しました。
浜松のPython勉強会、Unagi.py、最高に名前がズルい #hmrk01
— みずぴー (@mzp) March 28, 2015
発表資料『Rubyistのための型入門』
原稿
上記の資料を作る前に書いた下書きです。あくまで下書きなので、一部スライドの内容と構成が違う部分があるので、注意してほしい。
導入
自己紹介
こんにちは、みずぴーです。とりあえず、簡単な自己紹介をします。
GithubやTwitterではmzpと名前でいろいろやってます。
今日は、名古屋から来ました。いやー、浜松って思ったより近いですねー。
お仕事では、スタンドファームという会社でRailsでMisocaというサービスを作っています。
スタンドファーム
宣伝です。
弊社スタンドファームではMisocaという見積・納品・請求書の管理サービスを作っております。請求業務で不便な思いをされている方は是非お試しいただけるとよいかと思います。
またエンジニア募集中だそうです。
Rubyとの出会い
さて、ボクはRubyを仕事で使うようになったのは、去年スタンドファームに転職してからですが、Rubyを使い始めたのはもっと前です。
使いはじめたのは、もう9年も前のことになります。大学に入った年に、先輩に「Perlいいよー、やりなよ」って勧められて、いわれたことをそのままやるのは癪だなぁと思いながら、本屋に行ったのがきっかけでした。
懐しいですね。
LLバトル
懐しいついでに、当時のことを振り替えってみましょう。
10年前といえばメインのOSはWindowsXPですね。...あんまり懐しくないですね。
RubyはLL(Lightweight Laungage)と呼ばれて、PerlやPythonなどとまとめられることが多かったですね。P言語(Perl, Python, PHP, Ruby)なんで言葉もありましたね。
そして、JavaやC++といった静的型付き言語と比較される/することが多く、中には痛烈に非難している人もいました。ただ、仕事で使っている人はほぼおらず、「静的型付き言語は使いにくい。LLで仕事したい(けどできない)」のような話が多かったように思います。
静的型付き言語の復活
さて、それから10年近く時がたち、時代は変わりました。WindowsXPは相変らず利用されています。
Rubyで仕事するのは珍しい話ではなくなりました。また「Rubyは仕事をするための言語」という認識を持つ人も増えてきたそうです。
ただ静的型付き言語が衰退したわけではなく、ScalaやSwift、F#といった新しい静的型付き言語も登場しており、人気を集めています。
当時、あれほどdisられていた静的型付け言語はなぜ滅びなかったのでしょうか。そのdisは間違っていたのでしょうか。それとも新しい機能が追加され、改善されたのでしょうか。
今日はそういった話を、型の話をしたいと思います。
当時の静的型付き言語
こんな言語
当時、仮想敵として設定されていた静的型付き言語はどのようなものだったでしょうか。
こういった言語は、当時、以下のような点を非難されていました。
- コンパイルに時間がかかる
- 文法が冗長
- 型が窮屈
- 型のせいで記述量が増える
1つづつ見ていきましょう。
コンパイルに時間がかかる
「静的型付き言語はコンパイルが終わるのをまたないといけないwww。インタプリタ型であるRubyなら修正してすぐ実行できるwww」といったことを言われていました。
そうです、当時のRubyはコンパイルしてなかったんです。(注釈: 当時のRubyにも"コンパイル"と呼ばれる処理はあったらしい)
文法が冗長
これは主にJavaに向けられた非難だったと思うんですが、「文法が冗長で記述量が多い」というものがありました。
かなり恣意的な例ですが、Javaでの
class Main { public static void main(String args[]) { System.out.println("Hello, world!!"); } }
が、Rubyだと
puts "Hello, world!!"
になります。
型が窮屈
型は基本的に制約を導入するものなので、それが邪魔になることもあります。
例えば、Rubyでは1つの配列に異なる型の値を詰めれますが、これは静的型付け言語では通常はできません。
# Rubyでないと書けない [ 1, 2, "bar", [ 10.0, 20.0 ] ]
これ本当にできないわけではないので、より正確に言うと「ちょっとした工夫をすると実現できるが、通常は避けられる傾向にある」くらいですね。正しいことを言おうとすると、語尾がもやっとした感じになるのよくないですね。
型のせいで記述量が増える
当時のJavaでは型を明示的に書く必要がありました。
例えば1と2と3から成るリストを作る場合、Javaでは
ArrayList xs = new ArrayList(); xs.add((Object)new Integer(1)); xs.add((Object)new Integer(2)); xs.add((Object)new Integer(3));
ですが、Rubyでは
[ 1, 2, 3 ]
と書けます。最高ですね!
ここまでのまとめ: 全体的に重厚
ここまでの流れをまとめると、RubyなどのLLに比べて、静的型付き言語は何をやるにも大変でだ、重い、という非難を受けていました。
最近の静的型付き言語
最近登場した言語
こういった問題がどう変わったかを見る前に、最近登場した静的型付き言語にどのようなものがあるか見てみましょう。
- Scala: JVMで動く言語です。Javaとの互換性が高いのでbetter Javaとして使っている所も多いと聞きます。言語としてはかなり先進的な機能を搭載しています。
- Swift: AppleによるiPhone/Macアプリのための言語です。Objective-Cの反動からか、かなり評判いいですね。言語としてはわりとオーソドックスな機能を搭載しています。
- AltJS: JavaScriptを生成するための言語たちの総称です。シンプルな型しかないやつから、極めて先進的な型を持つ言語まであります。
- F#/Haskell/OCaml: 関数型言語と呼ばれるやつです。実はRubyよりもずっと古い歴史を持ちます。ボクは好きです。
どう変わったか
さて、先程あげた静的型付き言語の非難されていた部分がどう変わったを見ていきましょう。
- コンパイルに時間がかかる
- 文法が冗長
- 型が窮屈
- 型のせいで記述量が増える
コンパイルに時間がかかる
「マシンの性能もあがっていますし、コンパイラ技術もあがったので改善しました!!」みたいなことを言おうとしたんですが、Scalaのコンパイルはやたら遅いのでそんなことはないですね。
まあ、みんな我慢してるんじゃないですかね。
(メモ: sbt-musicalの紹介とかをする)
文法が冗長
次は文法です。最近の言語はリテラルや型推論が導入されて、かなり短くなっています。
[Scalaの場合]
object Main extends App { println("Hello, world") }
[OCamlの場合]
let () = print_endline "Hello, world!!"
まあ、最短とは言わないですが、それなりに許容可能な記述量になっています。 あとHello, worldの長さを競ってもそんなにうれしくないですよね。
型が窮屈・型のせいで記述量が増える
さて、ここからが本題です。当時の静的型付き言語の型と、今の静的型付き言語の型は、位置付けが大きく異なります。
JavaやCなどの当時の静的型付き言語の型は弱い型と呼ばれ、Scalaなどの今の静的型付き言語の型は強い型と呼ばれています。
弱い型付けは「型安全」が保証されない型のことです。 「型安全」という言葉が何を指すかは言語・定義によって違いますが、この発表では「実行時に型エラーが起きない」くらいの意味です。
強い型付けは、逆に「型安全」が保証される型のことです。
弱い型付け
JavaやCではキャストによって型を変えれるので、コンパイルに成功しても型によるエラーを防ぐことはできません。
[C言語の場合]
int *p = (int*)0x42; *p = 0; // ← 死ぬ(たぶん)。
なので、型は何かを保証するためではなく、メモリレイアウトの表現とか、コンパイラでの最適化などに使われます。
(メモ: コードの例が微妙なので変えたほうがいい?)
強い型
強い型、もしくは強い型付けは、逆に「型安全」が保証される型のことです。なので当然、型を強制的に変更するキャストはありません。
型の整合性が取れていることをコンパイラが保証してくれるので、テストの一種に数えられることもあります。型チェックだと粒度が粗いので単体テストに劣るのか、全パスについて検証してくれるので単体テストに勝るのかについては場合によって異なるので、一概には言えませんが。
ただ、単純にキャストをなくしただけだと、制約が厳しいだけでほぼメリットがないので、より強力な型に関する機能が用意されていることが多いです。
最近の型
では、最近の静的型付き言語に用意されている型に関する機能を見ていきましょう。
型推論
まずは型推論です。その名前のとおり型を推論してくれる機能で、型を書くことが減ります。
[OCaml]
(* xsの型はstring listと推論される *) let xs = ["foo"; "bar"; "baz"]
どこまで推論してくれるかは言語ごとによって大きく異なります。
例えばScalaでまメソッド内のローカル変数なのについては型を推論してくれますが、引数や返り値の型は省略できません。
[Scalaの場合]
// メソッドの引数と返り値の型は書く(省略できない) def f(x : Int, y : Int) : Int = { // 変数宣言の型は省略できる val z = x + y z }
いっぽう、ボクの大好きなOCamlならすべての場所の型を推論できます。
[OCamlの場合]
(* メソッドの引数と返り値の型を省略できる *) let f x y = (* 変数宣言の型も省略できる *) let z = x + y in z
あと、こういう話をすると必ず「型を無闇に省略しないほうがわかりやすいのでは」という意見がよくでるんですが、まあそれはそれで。
型が軽い
型推論によって型を省略できるだけでなく、型を作る手段も増えています。
いちいちクラスを定義してコンストラクタを書いて....、といったことをしなくても、新しい型を作れます。そのため、気軽に型を使うことができます。これを「型が軽い」と表現する人もいて、ボクもこの表現は気にいっています。
具体的に見ていきましょう。
直積型
まずは直積型、別名タプル、n項組です。これは2つ以上の値の組です。
例えば、Rubyで座標を返えすメソッドを書きたい場合は、こうしますね。
[Ruby]
def get_point [1, 2] # x座標とy座標 end
これがScalaだと
[Scala]
def getPoint() : (Int, Int) = { (1, 2) }
となります。 そっくりですね。
わざわざ新しい型を定義しなくても、 (Int, Int) と書くだけで、2つのIntの値の組を表す型を書くことができます。また、その値は (1, 2) と書けます。
直和型
(注: 時間の都合により、スライドではだいぶ変えた)
次は直和型です。別名はタグ付きunion、判別共用体などです。
なお型に関係している人たちは、一度はこれを「ちょクワガタwww」と誤変換するのがあるあるネタになってます。 これはRubyに直接対応するものがないのでぎこちない感じになるんですが、まあ無理矢理やってみましょう。
トランプのカードを表す定数を作りたいとします。
まず、種類を示す定数を定義します。
module Card # 数の札 Number = 0 # ジャック J = 1 # クイーン Q = 2 # キング K = 3 end
若干、恣意的なのは我慢してください。
そして、Numberはパラメータを持たないといけないので、使うときはこんな感じになります。
one = [ Card::Number, 1 ] two = [ Card::Number, 2 ] ... jack = [ Card::J, nil ] queen = [ Card::Q, nil ] king = [ Card::K, nil ]
これを使ったメソッドを定義しましょう。対応する数字を取得するメソッドはこんな感じになります。
def getValue(x) case x.first when Card::Number x[1] when Card::J 11 when Card::Q 12 when Card::K 13 end end
そもそもの例が苦しい感じがしますが、これは処理対象のデータをクラスで表現したくない、というシチュエーションを単純化した例です。GoFのデザインパターンではVisitorパターンを使うようなシチュエーションです。
ただ、全体的にだいぶあやうい感じがします。例えば、Numberは引数があるので2番目の要素にアクススしてもいいけど、J,Q,Kではアクセスしちゃダメというな制約がプログラム全体に埋もれてしまっています。
さて直和型を使ってましょう。例はOCamlですが、まあ、ニュアンスで分かると思います。
まず、直和型を使ってNumberとJとQとKを定義します。
type Card = Number of int | J | Q | K
これは「Number 0もJもQもKもCard型だが、それぞれ互いに区別される」という意味になります。また、Numberだけは構築するときにint型の値が必要なことも表現しています。
使う場合は以下のようになります。
let one = Number 1 let two = Number 2 ... let jack = J let queen = Q let king = King
これを使ったメソッド、もとい関数は以下のようになります。
let get_number x = match x with | Numebr n -> n | J -> 11 | Q -> 12 | K -> 13
このmatchはパターンマッチと言われていて、
- Numberを使っているときの条件分岐の値を取り出しをまとめてやってくれる
- xの全パターンを網羅しているかをコンパイラがチェックしてくれる。
といった機能があります。
もっとすごい型の機能
全部話していると時間が足りなくなるので、他の機能については簡単な紹介に留めます。わりとadvancedな機能なので、言語によってはなかったりします。
- レコード型: 直積型の1種です。0番目、1番目という値のかわりに、名前を与えれます。RubyのStructに近いです。
- 構造的部分型: fというメソッドとgというメソッドを持っている型、というのが表現できます。静的duck typeと呼ぶ人もいますが、いくつかの場所ではduck typingと挙動が異なり、議論を起こしがちなので注意してください。
- 型付きprintf: printf "%s" はstringしか受けとらない、ということができます。
何がうれしいか
このように最近の静的型付き言語は型に関する機能を豊富に持っています。
そのため、型で設計意図を表現し、意図に合わないコードはコンパイルエラーにすることができます。コンパイルを通るかどうかを一種のテストにできます。
例えば、サニタイズされていない文字列と、サニタイズされた文字列の型を変えることができます。出力関数が受けとるのはサニタイズされた文字列にしておけば、コンパイラにXSSのチェックさせることができます。ファントトムタイプと呼ばれる技法です。
また型がドキュメントのかわりになります。 例えば慣れたプログラマなら('a -> 'b) -> 'a list -> 'b listという型を見れば、mapに準ずる動作をする関数なんだな、ということが直感的にわかります。型から関数を検索するWebサービスもあるくらいです。 HoogleとかOCaml Scopeとかです。
-閑話休題-
Rubyに型ってつかないの?
さてRuby会議っぽい話題にもどりましょう。Rubyに型をつけることはできないのでしょうか。実はいくつかの実装があります。
例えば、distributedじゃないほうのdruby、diamondback ruby http://www.cs.umd.edu/projects/PL/druby/ が有名です。一時期、Railsに型をつけることにも成功したそうです。
<ここにsnipetをはさむ>
が、この辺はRuby/Railsのバージョンアップがはげしすぎてついていくのにだいぶ苦労している印象を受けます。ぶっちゃけ、いくら型が完璧でもRails2.0とかRuby1.8は使いたくないですよねぇ...。
あとRuby会議に来てこういうのもなんですが、型がないことを前提で設計されている言語に、あとから型を入れるのはかなり無理があると思います。型が欲しいなら、それ用の言語を使ったほうがいいです。
参考文献
今回話したような内容は「型システム入門」に詳しいです。訳者には、Ruby 2.0のリリースマネージャーこと遠藤さんもはいってます。ほら、Ruby会議っぽいですね。
ぶっちゃけ、詳しく知りたくないというか、言語を使えればいいや、というだけなら読む必要はないです。その言語の入門書なりリファレンスなりを読んだほうが絶対いいです。
ただ、自分で型を実装したい人、他の言語の型と自分の愛する言語の型を比べたい人は絶対に読むべきです。先人の組み立てた偉大なるツールがあるので読まないと地雷原につっこんでいくことになります。
どっちがいいの?
case by caseです。
型は制約です。できることを減らすというデメリットがある反面、間違いも減らします。 多少不便になっても安全をとるなら型を使おうべきですし、高速に作りたいなら型を使わないのもありです。
また、型システムにもいろいろな種類があるので、状況にあった型システムを選ぶべきです。
なので、静的型付き言語は銀の弾丸ではないですが、有用な道具なので、みなさまの道具箱にいれておくべきです。