RubyからScalaに乗り換えた15くらいの理由

| | コメント(0) | トラックバック(0)
[注意] この文章を読むと、既存のRubyコードをScalaでrewriteしたくなる、 Rubyコードで型チェックをやるのが鬱になる、Ruby案件でやる気が出なくなる、 などの幻覚や異常行動が報告されています。 現在関わっているプロジェクトや家族のことを常に意識し、 気を強く持って冷静に読むとよいでしょう。
「Scalaプログラミング入門」を読みはじめて、いきなり大きく頷いてしまった。
  • "コーディング時間の半分をテスト作成に費やさなければならなかった"(p.3)
  • "Railsによって得られた生産性の向上は、テスト作成の作業に失われてしまいました"(p.3)

まさにここ数年私が抱いてた漠然としたストレスの正体が、的確に文章となっていたからだ。そしてほどなく、「あ、この機能がRubyに欲しかっ た!」という驚きと共に Scala が本物であることに気付いた。さらに読み続けていくと、その驚きの回数は減るどころか、最後にはため息へと変わっていった。

  • はぁ...そんなんまでできるの?チートですやん。絶対勝てませんやん

もちろん Ruby は悪くない。当然 Java も素晴らしい。ただ、Scala が凄いんだ。

そんな待望のあるいは驚愕した機能を徒然と並べてみる。言語に異様なこだわりのある人なら、この一覧を見ただけで唸ることだろう。

[追記:2010-04-28] 各言語の仕様比較(※)としてまとめると価値がありそうなので、 表形式に変更しました。 (本文との関係で0を追加)。 わからない部分が多いので、 各言語の情報を頂けたら助かります。

(※ Scalaでできることリストがベースなので仕様比較は語弊があるかもだけど)

(ST: Smalltalk, JS: JavaScript)

No.機能RubyJavaScalaC#STJSHaskellOCaml
0全 てがobject×
0オ ブジェクト指向記述××
0関 数型記述×××××
1引 数の型指定×××
2ダッ クタイプ型定義××
××
3自 明な end は不要×××××
4変 数の型指定×××
5再 代入不可な変数×final
×let
6型 推論機能×
3.0××
7変 数の遅延評価×

×
8ドッ トレスなメソッド××××
×
9Java 並に速い×??

[ベ ンチマーク(対GCC)]-1.962.764.02

4.484.31
10primitive 型のboxing---
11XML リテラル××××E4X
×
12パ ターンガード×
if


○(|)when
13無 名ブロックの無名引数





14強 力なパターンマッチ




15引 数の名前渡し×
×



16first class object×
17豊 富なライブラリ××
18名 前付き引数(+default)×
2.84.0



[記載予定]
19 trait
20 暗黙の型変換
21 関数型指向の支援 (Optional, Unit, Some, None)
22 並列化の支援 (actor)

綺麗な仕様してるだろ?動くんだぜ、これで

参 考: 速度ベンチ http://shootout.alioth.debian.org/u64q/which-programming-languages-are-fastest.php

1. メソッドの引数に型指定ができる

Rubyは自由で心地良い。変数の宣言も不要だし何でもできる!もちろん自分の手に一番馴染んでいる言語はRubyだし、ちょっとした脳内のアイ ディアをコードに落とすのはRuby(やRails)が最速であると胸を張って言える。しかし、ミスが許されない業務で、複数人での開発で(想定されない 利用を想定する必要がある)、そして規模が大きくなっていくと、必然的に保守(や安全性のケア)の割合が増していく。例えばこのように、

  def build_servers(servers, options = {})
raise TypeError, "servers" unless servers.is_a?(Array)
raise TypeError, "options" unless options.is_a?(Hash)

# サーバーのビルド
servers.map{|s|...

気 付けば、各メソッドの先頭には定型の引数チェックコードが入り、本来それだけでいいはずの本質部分のコードの頭には「#ここからメイン」というコメントが 入り、そのせいでさらにメソッドの行数が肥大化し、可読性が落ちて、余計なコメントが増え、さらに compose methods の先でもまた型チェックして、という悪循環になっている事が多々ある。でも、仮に Java に逃げても、紋切り型の getter/setter 地獄で可読性を損なうから本末転倒。これは受け入れなければならない税金なんだ。諦めよう。

一方、Scala は引数に型を記述した

scala> def build_servers(servers: List[String], options: Map[String, String]) = {}
build_servers: (List[String],Map[String,String])Unit

2.「ダックタイプな型」を定義でき、引数で指定できる

あ、博士、こっちこっち!昨日のコードなんですけど、HashWithIndifferenceAccess で option 渡せなくて困ってるんすよ。あー、そうだね、すまんすまん。ここはエレガントに duck しちゃう?お、いーすねー。respond_to!respond_to!

  def build_servers(servers, options = {})
raise TypeError, "options" unless options.respond_to?(:[])

博 士ぇ、これで option に他の形式が来ても大丈夫ですよね!おうよ、全ての options を使ってるコードに埋めて行けば我々の勝利だ!(キリッ

一方、Scala は構造化typeを定義した

scala> type Gettable = {def get(key: String): Unit}
defined type alias Gettable

scala> def build_servers(servers: List[String], options: Gettable) = {}
build_servers: (List[String],Gettable)Unit

3. 自明な end は不要

        ...
end
end
end

end に嫌気が差して何度も Python に浮気した。でも、Python に逃げても、エントロピーの総和は一緒だった。もしくは、ドリフのタンスと換言してもいい。end を削ると self が飛び出してきた。我慢しよう、end も self も税金だ。

一方、Scala は裏でパーサが頑張った

scala> def index = render()
index: Unit

scala> var a = if (0 < 1) "ok" else "ng"
a: java.lang.String = ok

4. 変数に型がある

博士、どうしても謎のエラーが出るんですよ。

  def value_at(hash, i)
# 前処理
hash_each_with_index{|(k,v), i| ... }
# 要素を返す
return hash[i]
end

はっ はっは。これはRubyの有名な落とし穴でね、ほら、block の仮引数に i を使ってるだろ?これで元の i が上書きされちゃってるんだよ。えー、なんすかそれ。落とし穴ぐらい埋めといて下さいよ、ブツブツ(※)。てか、i は文字列なんすけど、数値で上書きされるんですね(涙)。でも、これでRubyistのレベルが上がりました!

(※ 1.9では多分埋まってる(と思う))

一方、Scala は変数に型を用いた

scala> var i:String = "name"
i: String = name

scala> i = "age"
i: String = age

scala> i = 10
<console>:5: error: type mismatch;
found : Int(10)
required: String
i = 10
^

5. 一度しか代入できない変数

さらに、一度だけ代入可能な変数も用意した

scala> val i:String = "name"
i: String = name

scala> i = "age"
<console>:5: error: reassignment to val
i = "age"
^
var再代入可能
val再 代入不可

6. 型推論機能がある

博士、さっきは生意気言ってごめんなさい。慣れれば落とし穴は回避できるし、もし型指定な方式だと多分 "String i "とか毎回書くのが面倒で、いずれ発狂してたはずです。これでよかったんだと思います。おやすみなさい。

一方Scalaは、自動で型推論した

scala> var i = "name"
i: java.lang.String = name

scala> i = 10
<console>:5: error: type mismatch;
found : Int(10)
required: java.lang.String
i = 10
^

7. 遅延評価する変数

博士、使うかどうかわからないけど、作るのに時間がかかる変数があるんですよ。lambda にすると call が面倒だし、メソッド化するには大げさだし。うーん、まぁ、初期化時に作っちゃいますね。

一方Scalaは、遅延評価可能な変数を用意した

scala> lazy val x = { println("[DEBUG] creating x"); 10 }
x: Int = <lazy>

scala> 1+1
res0: Int = 2

scala> x
[DEBUG] creating x
res1: Int = 10
(lazy 自体は昔からありますが、REPL(scala console) で print されないのは scala-2.8 の機能です)

8. 自明な場合はドット不要

型推論を駆使して、できる限りいい具合に解釈してくれます。

[訂正] 型推論でなく中置演算子式のお陰でした (thanks to ると)
詳細はコメントを参照: http://wota.jp/ac/?date=20100426#c07

scala> "100".toInt
res1: Int = 100

scala> "100" toInt
res2: Int = 100

scala> 1 to 5
res3: scala.collection.immutable.Range.Inclusive with
scala.collection.immutable.Range.ByOne = Range(1, 2, 3, 4, 5)

scala> 1 to 5 take 2
res4: scala.collection.immutable.Range = Range(1, 2)

scala> 1 to 5 take 2 toList
res5: List[Int] = List(1, 2)

scala> 1 to 5 take 2 toList sort(_<_)
<console>:6: error: not found: value sort
1 to 5 take 2 toList sort(_<_)
^
scala> (1 to 5 take 2 toList) sort(_<_)
warning: there were deprecation warnings; re-run with -deprecation for details
res7: List[Int] = List(1, 2)

Specs (p.272)

"'hello world' matches 'h.* w.*'" in {
"hello world" must be matching("h.* w.*")
}

See also http://code.google.com/p/specs/

9. Java並に速くて全てがobjectである

Javaはprimitive型がメソッドに反応してくれないので、純粋なオブジェクト指向でないです。(純粋が正しい、という意味でなく、オブ ジェクトとの会話ができないと楽しくない)

10. ボクシング

でもprimitive型は高速であるのも魅力。純粋か速度、どっちを取る?その答えを出したのは.NET。通常 primitive 要素として扱い高速に処理し、メッセージに対しては自動的に自分をwrapするオブジェクトを作成して返答する。この仕組みをボクシングと呼ぶ。色んな言 語の長所を集めたScalaは、もちろんこれを選択した。

11. XMLリテラル

言語レベルで必要なのかどうかは正直わからないが、XMLによる設定も多いので、そういう時代なのかもしれないという気もする。ただ、ひとつ言える ことは、特別な魔術なしでコード中に埋め込めるのは凄く楽しい。

scala> val html = <li>foo</li><li>bar</li>
html: scala.xml.Elem = <ul><li>foo</li><li>bar</li></ul>

scala> html.length
res0: Int = 2

scala> val name = "maiha"
name: java.lang.String = maiha

scala> <author>{name}</author>
res3: scala.xml.Elem = <author>maiha</author>

12. for文のパターンガード

私が Scala で一番素晴らしいと思うのは 良いプログラマへとこっそり導く仕様 になってる点。ここで「良いプログラマ」とは「並列化可能な安全で効率的で可読性の高いコードを書く人」のこと。そりゃ、プログラマであれば誰もが「なれ るならなりたい!」と思うはず。でもどうやったらいいのか簡単にはわからない。いや、本当は答えは単純で「関数型指向」で書けばOK。でも、関数脳にする には1,2年はかかるし(多分)、なにより敷居が高そう。そこで Scala は、コードを短くかけるシンタックスシュガーをたくさん用意した。短く書けるならそっちを選ぶのがプログラマの本能。そして、それが関数型コードになって いるという素晴らしき罠。この糖衣構文は、

  • ユーザの関数脳化
  • コードの可読性
  • 関数型コード

の一石三鳥なのである!具体的には、「100までの偶数のループ」を考えると、ruby ならこういうコードがすぐ思い浮かぶだろう

(1..100).each do |i|
next if i % 2 == 1
...

もち ろんこれは手続脳としては100点満点なのだが、Scala は for 文にパターンガード(適用条件)という概念を追加した。「100までの数」に「偶数」という適用条件を加えて、それを対象とするのだ。

scala> def isEven(i: Int) = i % 2 == 0
isEven: (i: Int)Boolean

scala> for {i <- 1 to 100 if isEven(i)} ...

こ れは一見して違いがなさそうだが、プログラミングパラダイム的には全く違うアプローチである。

パラダイム操作対象
手 続型100までの数
関数型100までの偶数

つ まり、手続脳では「100までループして、奇数を除外(or 偶数なら)」という処理の手順にまで考えが及んでいるのである。逆に言えば、手続脳としてのスキルは相当高いことになる。それに対して、Scalaの関数 脳では、あくまで操作の対象は当初の予定通り「100までの偶数」そのものである。いやいや、それなら ruby でも先にこんな風に select するよ!それでもう関数脳じゃない?

(1..100).select{|i| i % 2 == 0}.each do |i|

正 解!これであなたも立派な「関数脳な ruby 使い」です。ただし、実用には問題が2つあります。

  1. 読みにくい
  2. 非効率(50個のオブジェクトが生成される)

2は特にデータ数が100万件とかになると顕著で、コードレビューで「無駄なオブジェクト作るんじゃねーよ」と叱れて、せっかく芽生えだした関数脳 が潰されてしまいます。専門用語で言うと「怒られるの怖い」((c)猪子)状態です。やはり、関数型でない言語で関数型コードを書くのはどこかに無理があ るのです。

うーん、なんとなく言わんとすることはわかるけど、関数型であるメリットがあまり見えないんだけど?手続型で上のコードだとダメなん?答えは並列化 です。シングルプロセス、シングルスレッドの上では両者は同じ100点です。しかし、これらを並列化するときに、違いが現れます。関数型とはぶっちゃけ、

「イミュータブルな操作対象」と「副作用のない処理」

関数型
L1
L3

これ満たして定義していってくれれば、
あとは僕が並列実行できますけど、
お客さん、どうします?

L6 L9 L8

という話に過ぎません。流行のMapReduce、もしくは、かっこつけて言えばリエントラントになるのです。1人のMapちゃんが「これは偶数 か?」という付加条件を気にせずに、自分の仕事に集中できるのです。

関数型を追求してたらいつのまにかオブジェクト指向原則の「単一責任の原則(SRP)」を満たしてるのが面白いです。もしかしたら、所詮みんな同じ チューリングマシーンなので、目指す場所は一緒なのかもしれません。あるいは、単なる「Simple is best」。それを「SRPでやるべき!」と理論的に推奨するか、「ほら簡単に書けるよ(はぁと)」で実践的に誘導するか、の差でしかないのかもしれな い。そして、後者をあちこちに言語仕様として体現させている Scala は正直凄いです。(どこまで意図してやってるかはわからないけど)。

イミュータブルとか副作用とかも本当は難しい。でも、Scalaで短く書けば全部そうなってるから!

13. 強力な無名ブロックの引数

ここまで、短絡的な人、もしくは Ruby 原理主義な人からすると、 「なに、Ruby の悪口言ってるんだ?」 となってたら困るので、一言断っておきます。 私をみくびらないで頂きたい。 悪口はここからだ! あ、嘘です。やめて。Matz、石は痛い。 一番 Ruby が書きやすいから、サンプルコードとして使ってます。 今でも私は Ruby Love ですニダ!

実際、悪口はないけど、イケてないと思うのは sort ブロックの仕様。これは Rubyist なら賛同してもらえると思う。特に複数の条件で連鎖したいときに、あの(c/perl由来の?) -1, 0, 1 判定は辛いですね。せめて真ん中が nil なら

a.created_at <=> b.created_at or a.name <=> b.name

と か綺麗に書けたことでしょう。しかし、ここで取り上げたいのは、block そのものの方です。私は、

files.select{|i| i.exist?}

の i が冗長でずっとイライラしてました。ただ数文字を削りたいだけじゃなくて、「集合(files)に対する操作(exist?)」を指示したいのに、なぜこ うも冗長な表記しかないのか。だから、ActiveSupport の Proc#to_symbol を見たときは、心の底から震えました。

files.select(&exist?)

で も、AS は引数を伴う場合には無力でした。引数が追加されたとき、上のエレガントなコードからまた block に展開する作業は、涙無しではできませんでした。あるいは、件の sort の場合がより明確かもしれません。私は、sort の block を消すために "<=>" を定義しました。

files.sort

も うルンルンです。でも、順序を逆にする必要があって愕然としました。reverse が一番楽なのですが、怒られるの怖い状態ですし、また涙を流しながら

files.sort{|a,b| b<=>a}

に 戻しました。a とか b とか、そんな事どうでもいいじゃない!落とし穴怖いよぉ。怒られるのも怖いよぉ。

一方 Scala は、ワイルドカード(_)を導入した

scala> List(1,2,3) sort(_<_)
res1: List[Int] = List(1, 2, 3)

scala> List(1,2,3) sort(_>_)
res2: List[Int] = List(3, 2, 1)

"(_>_)" を暗号だから嫌だと思う人もいるでしょう。もちろん仮引数による書式もあるので、そういう人はそれを使えばOK。でも、私にとって、コードの本質 (">"部分)がノイズに埋もれることの方が暗号なんです。ここは Perl のいい所だと思うし、こういう選択肢もあるというのは重要。

一方、真の Rubist は、クールに sort_by と Array をキメた

collenction.sort_by { |x| [x.created_at, x.name] }
と書きますね。(そのため(だけ)の Array#<=>)
by musha

14. 強力なパターンマッチ

Ruby の case when のパターンマッチ力は異常で、簡潔かつ強力なRubyコードを象徴する例だと思う。例えば、unixtime を可愛い表示にする場合

def pretty_unixtime(sec)
case sec
when 0 ... 60 then "#{sec}秒"
when 60 ...3600 then "#{sec/60}分"
else "たくさん"
end
end

脳 内のロジックイメージとコードの間にブレが全くない!素晴らしいことだ。しかし、sec に負値が入ってくると話は一変してしまう。あぁ、MinusInfinity があれば "when MinusInfinity...0" を追加するだけでいいのに、と思いながら

def pretty_unixtime(sec)
raise "minus" if sec < 0
...

という 判定コードを入れた記憶があるRubyistも多いだろう。

一方 Scala は、case文にもパターンガードを入れた

scala> def pretty_unixtime(sec: Int) = sec match {
case i if i < 0 => "minus!"
case i if i < 60 => sec + "秒"
case i if i < 3600 => (sec/60) + "分"
case _ => "many"
}
pretty_unixtime: (Int)java.lang.String

scala> pretty_unixtime(-10)
res1: java.lang.String = minus!

scala> pretty_unixtime(30)
res2: java.lang.String = 30

scala> pretty_unixtime(500)
res3: java.lang.String = 8

変数型の考え方で行けば PositveInt な構造化typeを定義するのかもしれない。

TODO: Scala で "case 1 to 59" みたいな書き方ができないか調べる

15. メソッド呼び出しの名前渡しができる

def execute(logger)
if #some error
logger.error(...)
end
end

execute(Logger.new)

execute を呼び出す前に、Logger.newが実行される。これはRubyの初歩の初歩。loggerを使わない時もあるけど、それはご愛嬌。

一方 Scala は、引数にも遅延処理(名前渡し)をサポートした

class Logger { println("Logger.new is called") }
def execute1(logger: Logger) = {} // 値渡し
def execute2(logger: => Logger) = {} // 名前渡し

scala> new Logger
Logger.new is called
res0: Logger = Logger@584ba778

scala> execute1(new Logger)
Logger.new is called

scala> execute2(new Logger)

lazy val があるからできて当然だし、使うことも年に数回あるかないかだと思うけど、なんかワクワクしね?まさにこの機能がピッタリだった年に数回のその日には、一 日中ウキウキしてる気がしね?

16. メソッドがfirst class object

ファーストクラスオブジェクトの定義は難しい。でも、ぶっちゃけ本質的には 変数に入れられるこ と ぐらいの認識でOK。(wikipediaより舞波を信じろ!)。これだけでもミーティングでは60%ぐらいは話が通じるはず。あとは適当なタイミングで 「あぁ、はい」「そうですね」とか言ってればOK。もうすぐGWだし。とはいえ、定義より何が嬉しいかを見たほうが覚えやすいので、例として、配列をソー トするアルゴリズムを使い分けることを考える。

Array#obakasort  # 単純ソートを定義
Array#qsort # quicksortを定義

array.qsort # qsortで実行

これは、ソート手段を換える度に、メソッド自体が変わってしまうので楽しくない。さらにこのやり方だと、レシーバ(ここではArray)の種類ごと に定義が必要になるのも面倒だ。だから、sort という API だけ定義して、その具体的な手段は別の場所に切り出すことにする。

Array#sort(strategy)  # 何らかの「手段情報」に従ってソートする

array.sort(obaka)

こうしておけば、「sort(手段名)」を変更するだけで切り替えられるし、後から手段を追加するのも簡単だ。これをGoFでは、ストラテジーパ ターンと呼ぶ。そして問題は、この「手段情報の渡し方」だ。答えから言えば、どう考えてもアルゴリズムオブジェクトそのものを渡せるのが一番自然だ。例え ば Ruby なら module で定義するだろうから、

module Qsort
def sort(array)
...

array.sort(Qsort)

とできて、素晴らしい。さらに、Ruby には無名ブロックがあるため、わざわざ module に切り出さなくても、呼び出し側にブロックとして付与することができる。

array.sort {|a,b| ... }

おいおい、Rubyって完璧じゃねーか!短く書いたら(ユーザに自覚がなくても)ストラテジーパターンをいつのまにか使ってるのだ。勘違いしないで よ。べ、別にプログラマのスキル向上を意図したかった訳じゃなくて、こういうのがあったら便利かな、って思っただけなんだからねっ!Scalaにも感じる この「暗黙的に良いコードを強要(矯正)する仕組み」を「ツンデレ仕様」と呼ぶ。閑話休題。ここで重要なのは、module にしろ block にしろ

  • 変数にも入れることができ
  • (結果的に)メソッドの引数として使える

という点だ。つまり、この意味で「Ruby では module や block も first class object である」となる。これは「Everything is an object」とも関係するが、個人的に Java がイケてないと思う理由はここ。

array.sort("qsort")

クラスを引数にすることができないから、対象クラスそのものでなく、文字列のような参照手段を渡すことになるだろう。(Java使ってないので自信 なしだけど、多分こんな感じ※)。

(※ JavaだとHoge.classでTypeを渡すようです (thanks to i))

いやいや、それで名前解決できたらいいんちゃうの?否!純粋オブジェクト指向脳からすると、絶対に無理なんです。それは、sum(1,2) の足し算メソッドを

sum("1","2")

と書いてもいいんちゃうの?てのと同じ話になるんです。sum はちゃんと内部で str2int するから大丈夫ですよ!とか、そんな心配でなくて、明らかに無駄ですやん!これを自然に sum(1,2) と書けるのは、1,2である数値(Integer)が first class object だから。メリットが見えてきましたね。

  • Sortings.Qsort
  • Sortings.Obaka

があるんだが、君は、どれ使えばいいと思うかね?へへへ、博士。こんなこともあろうかと、徹夜で凄いソートパッケージを作っておきましたよ!おー、 でかしたぞ、我が助手よ。どれどれ、早速インポートっと。

  • Sortings.MottoObaka
  • Sortings.MottomoObaka

array.sort(Sortings.MottoObaka)

うわー、すげー、本当に今までの3倍遅いよこれ!こんな無駄な車輪の再発明を見たの初めてだ。よく、頑張ったな!ありがとうございます。博士、僕 だってやるときはやりますよ!(キリッ。

博士
L1
L3

(こいつ真性だ。この会社はやはり俺が頑張らねば...)
L6 L9 L8
L1
L3

(バカを演じて、上司に自信をつけさせる...
楽な仕事じゃないよ!)

L6 L9 L8
助手

import scala.util.Random
object Random extends Random

def ObakaSort(a: Int, b: Int) : Boolean = { false }
def MottoObakaSort(a: Int, b: Int) : Boolean = { Random.nextInt(100) % 2 == 0 }

scala> List(1,3,2).sort(ObakaSort)
res0: List[Int] = List(3, 2, 1)

scala> List(1,3,2).sort(MottoObakaSort)
res1: List[Int] = List(3, 1, 2)

こんなの素敵じゃね?この MottoObaka は多くの純粋オブジェクト指向言語では、Class や Module になる。でも、もしメソッドが first class object だったら、また新しい可能性が見えてこない?だが、first class object、つまり、変数に入れてメソッド引数で使うこの行為を、一体どこまで適用させると便利なのだろう?Int,Stringは最低限として、

  • Class,Moduleは必要なのか?
    • → んなこたあない (Javaの立場)
    • → 便利だって (Rubyの立場)
  • Methodまで行ったら本当に便利なのか?
    • → そうだよ! (Scalaの立場)
  • オブジェクトに送るメッセージ自身まで行ったら便利なのか?
    • → そうだよ! (Smalltalkの立場)

この明確な答えがあるのか、もしくは、いい落し所がどこなのか、は僕も知らない。でも、無料で手に入るなら、飛行機もメソッドもfirst classに越したことはないよね。っていう、お話。

[訂正] Scala のメソッドは変数に入れられないので first class object ではありませんでした。 単に、引数として使われると関数に変換されるだけでした。(thanks to ると)。 試さずに書いてすいません。 イメージ的には Rubyのprocぽいです。 proc の方は変数にも入れられるので、first class object と言えそうです。 でも Ruby のメソッドは引数に直接渡せないので、first class 具合は

   Ruby Method < (引数の壁) < Scala Method < (変数の壁) < Ruby Proc

でしょうか?
詳細はコメントを参照: http://wota.jp/ac/?date=20100426#c07

Ruby のメソッドとブロックの話と言えば、もう少し可搬性があれば、とよく思います。あるクラス(もしくはモジュール)のインスタンスメソッド定義を proc として抜き出して、別のクラスのインスタンスメソッドに定義する的な。(クロージャ部分の参照は抜きにしていいので)

Foo = Class.new { def foo; end }
Bar = Class.new

foo = Foo.instance_method(:foo)
Bar.send :define_method, :foo, foo
Bar.new.foo
=> TypeError: bind argument must be an instance of Foo

ActiveSupportのあるモジュールのあるメソッドだけ、俺のクラスに入れたい!てときがよくある。これができるようになると、Ruby がもっとパワフルになるはず!

17 ライブラリ

これまでもよさげな言語を発見することはあって、何度もRubyから浮気しようとした。でも、言語自体はよくても、毎回実用に至らなかったのは

  • あぁ、NKFないのか...
  • ImageMagick とかないですよねぇ

みたいなライブラリの不足が原因だった。RubyはC拡張も豊富で大概のものは揃ってる。これはPythonでも同じだ。Javaと.NETはその 点で最高かもしれない。そして、ScalaはJVM上で動作するためJavaの資産を全て利用可能だ。もちろん、Scala言語内からもJavaライブラ リを直接呼び出せるので実行効率も問題ない。ただ、その時にJavaのクラスとScalaのクラスの直接的なマッピングをまだ理解してないので、 Scalaのライブラリ能力は☆2つで(比較表で◎でなく○)。比較表と言えば、Haskell や OCaml も機能的にはよさそうに見えるし、それらに対するScalaの優位性はもしかしたらここだけかもしれない。でも、それが覆せない決定的な差でもある。

18 名前付き引数(+default)

ずっと欲しかったけど、実装される気配がないので耐えかねて自分で作った。

http://github.com/maiha/optionize

#  Optionize can extract args with various formats like this.

user("maiha", 14)
user(:name => 'maiha', :age => 14)
user("maiha", :age => 14)

# In this case, we define 'user' method as following

require 'optionize'
def user(*args)
opts = Optionize.new(args, :name, :age)
opts[:name] # => "maiha"
opts.name # => "maiha"
opts[:age] # => 14

も ちろん期待通り動いた。でも、メソッド定義から引数情報が消えて、僕が大事にしてる可読性(保守性)が落ちてしまった。せめて、JavaScript の arguments オブジェクトがあったなら。。。

一方Scalaは、2.8でサポートした

def url_for(controller:String = "home", action:String = "index", id:Any = "") = {
controller + "/" + action + "/" + id.toString }

scala> url_for()
res0: java.lang.String = home/index/

scala> url_for(id = 1)
res1: java.lang.String = home/index/1

scala> url_for(action = "list")
res2: java.lang.String = home/list/

scala> url_for(controller = "users")
res3: java.lang.String = users/index/

scala> url_for(controller = "users", action = "show", id = 10)
res4: java.lang.String = users/show/10

scala> url_for("users", "list")
res5: java.lang.String = users/list/

scala> url_for("users", id = 5)
res6: java.lang.String = users/index/5

scala> url_for("users", controller = "foo")
<console>:7: error: not enough arguments for method url_for: (controller: String,action: String,id: Any)java.lang.String
url_for("users", controller = "foo")
^

[2.8を使おう!] 他にも2.8ではREPLでTAB補完ができたりと色々とパワーアップしてます。 タイミング的にもRC1が出たのは何かの縁。 どうせ勉強中の身なら、 積極的に2.8を使ってどんどんバグ報告して、 将来、業務利用するときにより洗練された製品を使えるように、 みんなでScalaを育てましょう!

2.8RC1のダウンロードはこちら: http://www.scala-lang.org/downloads

19 trait

後日執筆予定。

まとめ

Scalaのいい点がどんどん出てきて終わりが見えないので、先にまとめを書きます。 本文の方は少しずつ更新予定。

十数年前にはPerlを使っていた。やがてオブジェクト指向の存在を知り、そのエレガントさに憧れた。すぐに、その象徴たる Smalltalkに飛びついたが、その壁は厚かった。その世界ではオブジェクト指向での記述しか許されず、手続脳からオブ脳へのパラダイムシフトは、凡 人が一朝一夕に成し得ることではなかったからだ。だから、両方のパラダイムで記述可能なRubyやJavaを見つけたときは嬉しかった。ゴールたる Smalltalkユーザからすれば、ライブラリが多い以外は何の魅力も感じなかったことだろう。(そのライブラリ数は重要なのだが)。でも私にとって は、まずは慣れ親しんだ手続き型で記述することができ、習熟度と共に自分のコードがオブジェクト指向へと変わっていき、それを実感できるという楽しく充実 した時間だった。

手続き型           ...    オブジェクト指向
[Perl]
[ Ruby ]
[Smalltalk]

そして近年、インフラの分散並列化の勢いは止まらず、そこに一番適したモデルは恐らく関数型であろうことはわかっている。そのゴールはもしかしたら Haskell,OCamlかもしれない。しかし、その世界では関数型での記述しか許されず、オブ脳から関数脳へのパラダイムシフトもまた一朝一夕には成 し得ない。かつてRubyが手続脳からオブ脳へと導いてくれたように、今度はScalaがオブ脳から関数脳へと導いてくれるのだ。そのゴールたる Haskellユーザからすれば、ライブラリが多い以外は何の魅力も感じないのかもしれない。(そのライブラリ数は重要なのだが)。でも私にとっては、長 い間待ちわびた次のステップへの架け橋を担う存在なのだ。

オブジェクト指向   ...              関数型
[Ruby]
[ Scala ]
[Haskell]

そして嬉しい誤算は、この関数脳養成ギブス用途のつもりであるScalaが、今のところ私が知る限り最も欠点が少ない言語であり、もしかしたらゴー ルそのものなのかもしれないという点だ。

Let's play Scala!

http://www.scala-lang.org/downloads

本日のツッコミ(全15件) [ツッコミを入れる]
_ pie (2010-04-28 12:51)

maiha が AKB に転んだことと Scala に転んだことのどっちがインパクト大きいんだろ。とりあえず「Scalaプログラミング入門」買いにいきます。Google App Engine で動くといいな。「キャンパスライフ」は買ったけど封は開けてない。

_ yasuyuki (2010-04-28 13:15)

ずっとEclipseにプラグインだけインストールして放置してたのを、動かす時が来たようです。(はじめからちゃんと使えよ)

_ ななし (2010-04-28 15:03)

>ここは Perl のいい所だと思うし

_ musha (2010-04-28 20:47)

collenction.sort { |a,b| a.created_at <=> b.created_at or a.name <=> b.name }

collenction.sort { |a,b| [a.created_at, a.name] <=> [b.created_at, b.name] }
すなわち
collenction.sort_by { |x| [x.created_at, x.name] }
と書きますね。(そのため(だけ)のArray#<=>)

配 列を生成しないストリーム志向版selectは私を含め幾人か実装して議論しているけど合意が取れそうな提案までには至っていません。

_ ちく (2010-04-29 00:44)

XMLリテラルはJavaScriptのE4Xがありますね.

_ metanest (2010-04-29 08:30)

「block の中」は「block の引数」ですね。

「block の中」で外の環境と同じ名前を使ったら、外の環境に影響が出るのは 1.9 でも同じです、というかそうでないとクロージャとして使えないというか。

_ ると (2010-04-29 17:10)

全てがobject:
JavaScript(ECMAScript)では数値などはプリミティブな値です。
メソッド呼び出しなどのときに自動的にオブジェクトに変換されます。
参照: ECMA-262 4.3.20節, 4.3.21節, 10.4.3節など


引数の型指定:
Haskellでも引数の型の指定は可能です。
ただし、関数の定義とは別の行に書く必要があります。
例:
f :: Integer -> Integer
f x = x + 1


自明な end は不要:
Haskellでもdoなどの{}はインデントで表現可能です。


変数の型指定:
Haskellでも関数同様に型の指定は可能です。


再代入不可な変数:
Javaではfinalキーワードにより可能です。
Haskellでは変数は全て再代入不可です。


変数の遅延評価:
Haskellでは式は遅延評価されます。


ドットレスなメソッド:
Scalaにおいて、ドットを省略するのに型推論は関係ありません。
ドットを省略した記法は中置演算子式であり、
+などの演算子を含め左結合の中置演算子式e1 op e2はe1.op(e2)の構文糖衣に過ぎません。
(右結合の場合は{val x = e1; e2.op(x)})
参照: Scala言語仕様2.7版 6.12.3節


primitive型のboxing:
Javaにおいてもプリミティブ型は自動でboxing, unboxingされます。
参照: Java言語仕様第3版 5.1.7節, 5.1.8節
また、前述しましたがJavaScriptでも数値などはboxing, unboxingされます。
ただし、Javaでは
(new Object()).equals(1)
とか
Object x = 1;
と書いた場合はboxingされますが、
(1).toString();
と書いた場合にはboxingされません。
Javascriptでは(1).toString()でもboxingされます。


パターンガード:
Haskellにもあります(|)。


強力なパターンマッチ:
Haskellにもあります。

また、Rubyでは以下のような記述が可能です。

Negative = Object.new
def Negative.===(n)
  n < 0
end

case -1
  when Negative then "negative"
  else "zero or more"
end

これは、case式がパターンの方の===メソッドを呼ぶためです。
なお、Scalaでもunapplyメソッドを定義すると同じようなことができます。
Scalaのunapplyを使ったパターンマッチはマッチの成否だけではなく
値を取り出して変数に入れられるため、より強力です。

なお、Rubyで負でなおかつ指定された範囲にマッチさせたい場合は以下のように書けます。

Even = Object.new
def Even.===(n)
  n % 2 == 0
end

class And
  def initialize(p1, p2)
    @p1 = p1
    @p2 = p2
  end

  def ===(o)
    (@p1 === o) && (@p2 === o)
  end

  def &(p)
    And.new(self, p)
  end
end

def Even.&(p)
  And.new(self, p)
end

case -2
  when Even & ((-5)..(-1)) then "ok"
end


引数の値渡し:
execute1は値渡し(Call-by-value)であり、execute2は名前渡し(Call-by-name)です。
参照: Scala言語仕様2.7版 4.6.1節, Wikipedia「名前渡し」 http://ja.wikipedia.org/wiki/%E8%A9%95%E4%BE%A1%E6%88%A6%E7%95%A5#.E5.90.8D.E5.89.8D.E6.B8.A1.E3.81.97

HaskellではCall-by-need(Call-by-nameと似ているが、パラメータを2回以上使っても1回しか評価しない)が使わ れます。


メソッドがfirst class object:
Scalaにおいて、メソッド型はあります(言語仕様2.7版 3.3.1節)が、
それを変数に入れたり、メソッドに渡したりはできず、
first class objectではありません。
ただし、メソッドは必要に応じて関数に変換されます(同6.25.2節)。
これはRubyにおけるMethodと同じ位置付けのものであり、
単に構文上Rubyで言うところのObject#methodやMethod#callが省略可能というだけです。

もしScalaの関数を指して「メソッドがfirst class object」と言うのならば、
「Rubyのメソッドもfirst class object」であると言えます。

また、Javaの場合は、Comparatorのような1つだけメソッドを持つインターフェースを定義し、
そのインスタンスを渡す方法が一般的であり、クラス名を文字列として与えるのは
動的にクラスをロードする場合などでなければ一般的ではありません。

_ ると (2010-04-29 18:04)

訂正:

パターンガード:
Haskellにもあります(|)。


for文のパターンガード:
Haskellにもあります(リスト内包表記)。

_ ると (2010-04-29 18:26)

C#のrefは参照渡し(http://ja.wikipedia.org/wiki/%E8%A9%95%E4%BE%A1%E6%88%A6%E7%95%A5#.E5.8F.82.E7.85.A7.E6.B8.A1.E3.81.97)で あって、名前渡しではありません。
例にあるような、execute2(new Logger)のような式はC#では書けません。

_ i (2010-04-29 18:46)

> クラスを引数にすることができないから、対象クラスそのものでなく、文字列のような参照手段を渡すことになるだろう。(Java使ってないので自信なしだ けど、多分こんな感じ)

Javaでクラスを渡すなら、Hoge.class ですね。何故かプリミティブにも.classが使えます。(int.classとかvoid.classとか)
ただ、例に挙げられたようなロジックを渡すケースについては、Javaではinterfaceを用意するのが一般的ですね。
とは言え、独自の比較ロジックを食わせるのにいちいち
array.sort(new Comparator<String>() { int compare(String a, String b) {...} })
なんて書いてられるかぁーーー!!と思うのは事実。

_ ると (2010-04-29 22:13)

RubyのMethodとProc, Scalaのメソッド(から変換される関数)について、
「変数に入れられるか」「メソッドに渡せるか」の2点からまとめました。
キーワードがハイライトされる適当なエディタにコピーして読むと読みやすいです。


#### Rubyの場合
### 変数に入れられる?

x = 1

# Methodは変数に入れられる
f = x.method(:to_s)
f.class # ⇒ Method
f.call # ⇒ "1"

# Procは変数に入れられる
g = proc do x.to_s end
g.class # ⇒ Proc
g.call # ⇒ "1"


### メソッドに渡せる?

def foo1(p)
  p.call
end

def foo2(&p)
  p.call
end

def foo3
  yield
end

def foo4(&p)
  p.class
end

# Methodはメソッドに渡せる
foo1(x.method(:to_s)) # ⇒ "1"
foo2(&x.method(:to_s)) # ⇒ "1"
foo3(&x.method(:to_s)) # ⇒ "1"

# ただし、&で渡した場合はProcに変換される
foo4(&x.method(:to_s)) # ⇒ Proc


# Procはメソッドに渡せる
foo1(proc do x.to_s end) # ⇒ "1"
foo2(&proc do x.to_s end) # ⇒ "1"
foo3(&proc do x.to_s end) # ⇒ "1"
foo4(&proc do x.to_s end) # ⇒ Proc



////// Scalaの場合
//// 変数に入れられる?

val x = 1

// 右辺の型がわかっていれば関数に変換される
val f1 : () => java.lang.String = x.toString
f1() // ⇒ "1"

// これも右辺の型がわかっているので関数に変換される
var f2 = () ⇒ "hoge"
f2 = x.toString
f2() // ⇒ "1"

// これも右辺の型がわかっているので関数に変換される
val f3 = x.toString : (() => java.lang.String)

// _を付ければ関数に変換される
val f4 = x.toString _
f4() // ⇒ "1"

// 型がわからない場合、0引数メソッドの場合はメソッド呼び出しとして扱われる
val f5 = x.toString // ⇒ "1"

// 1引数以上のメソッドではエラーになる
val f6 = x.equals // error: missing arguments for method equals in class Any

// _を付ければ変換される
val f7 = x.equals _
val f8 = "".replaceAll _


////// メソッドに渡せる?

def foo(f : () => java.lang.String) = f()
def identity[X](f:X) = f

// fooの引数は関数型とわかっているので関数に変換される。
foo(x.toString) // ⇒ "1"

// 明示的に_を付けても関数に変換される
foo(x.toString _) // ⇒ "1"
identity(x.equals _) // ⇒ (Any) => Boolean = <function>
identity("".replaceAll _ ) // (String, String) => String = <function>

// identityの引数は関数型とは決まっていないので、エラー
identity(x.equals) // error: missing arguments for method equals in class Any


// オーバーロードしている場合
object hoge {
  def bar(f : () => java.lang.String): java.lang.String = f()
  def bar(f : () => Int): Int = f()
}

// barの引数の型がわからないので関数に変換されない
hoge.bar(x.toString) // エラー

// 明示的に変換すればOK
hoge.bar(x.toString _) // ⇒ "1"

_ hat (2010-04-30 02:28)

ruby と比較しているのであれば、ぜひ強力な trait も紹介してください...。
Module にはないものがいくつかありますので

class Hoge { def hello() { print("hello") } }
trait Foo { def world() { print("world") }}
var bar = new Hoge with Foo;
bar.hello(); => "hello"; bar.world(); => "world"

var baz = new Object with Foo;
baz.world(); => "world"

_ aereal (2010-05-02 22:27)

> 4. 変数に型がある

このRubyのコード例は、ブロック引数のスコープの問題と混同しているような気がします。

http://www.ruby-lang.org/ja/man/html/FAQ_CAD1BFF4A1A2C4EABFF4A1A2B0FABFF4.html#a2.2e11.20.a5.d6.a5.ed.a5.c3.a5.af.a4.cb.b0.fa.bf.f4.a4.f2.c5.cf.a4.b9.a4.cb.a4.cf.a4.c9.a4.a6.a4.b7.a4.de.a4.b9.a4.ab
> この仮引数は、普通のローカル変数で、ブロックの外側ですでに使われて いる変数の場合は、そのスコープになりますので、注意が必要です。

Ruby 1.9ではブロックローカルになっているようです。
http://gihyo.jp/dev/serial/01/ruby/0003

_ bleis-tift (2010-05-08 12:57)
C#er です。

0.全てが object で Scala が△、C# が○になっていますが、Scala が△なら C# も△な気がします。

0.関数型記述が×になっていますが、これはなんのことを言っているのかよく分かりません。
Scala での (Int, Int) => Long のようなもののことを言っているのであれば、Func<int, int, long> がそれに相当するのではないかと。

5.再代入不可な変数が○になっていますが、ローカル変数では「定数」を表す const しか許されていません。
再代入不可を表す readonly は、フィールドでしか使用できないので、個人的には×に近い△だと思います。

トラックバック(0)

このブログ記事を参照しているブログ一覧: RubyからScalaに乗り換えた15くらいの理由

このブログ記事に対するトラックバックURL: http://blog.amhp.jp/cms/mt-tb.cgi/341

コメントする

このブログ記事について

このページは、alphaが2010年5月26日 07:48に書いたブログ記事です。

ひとつ前のブログ記事は「超シンプル!hover画像作る必要ナシ、CSSだけで、画像をロールオーバー!」です。

次のブログ記事は「2年前の障害報告書から学んだAmazon S3の凄さ」です。

最近のコンテンツはインデックスページで見られます。過去に書かれたものはアーカイブのページで見られます。