「Seven Languages in Seven Weeks」読了



11月から読み始めて10週間ほどかかってしまいましたがなんとか読了しました。長い時間かけて読むだけの価値は十分あったと思います。もともとHaskellに興味があって、最終章から読み始めたのですが、普通の方は第一章から読み始める事をお勧めします。Ruby -> IO -> Prolog -> Scala -> Erlang -> Clojure -> Haskellの順番なのですが、かなり考えぬかれた順番になっていると思います。

最初のRubyは一番既存のOO言語の王道的なものですが、そこからプロトタイプベースのIOに移り、「ロジックベース」のPrologへと移っていきます。Rubyはどの人が始めてもとっつきやすいものですが、そこから2つのマイナー言語(失礼)に移る事で、読者に「今までのプログラミング考え方を変えなきゃいけないよ」と揺さぶりをかけているような印象を受けました。

後半の最初はScalaから始めるのですが、Rubyを読んだ後だと比較的分かりやすいのではないでしょうか。個人的には「静的型のRuby」という印象を受けます。次のErlangですが、ErlangはもともとPrologをベースに作られた言語というのを今回初めて知りました。「文法がいけていない」とよく批判されるErlangですがPrologを先に学習した後だと案外すんなり学習できました。ここからLispJVM上の方言であるClojureと「純粋関数言語」であるHaskellに移るのですが、それまでに習った言語に既に重複したコンセプトがあるので、結構すんなり理解できるのではないでしょうか。その点私はそういった下積みなしにいきなりHaskellから始めたのでかなりつらかったです。

以下にそれぞれの章の主な感想を連ねてゆきます。ちなみにサンプルのほとんどはこの本からです。

日頃から使っている言語なので、特に目新しいものはなかったのですが、それでも「Rubyのメッソドは全て返り値を返す」というのが特徴といわれ、そういえばそうだなあと思いました。最近以下のようなスタイルで書く事が増えてきたのですが、これなんかも「全て返り値を返す」に通ずるものがありますね。

a = if true 
  "foo"
else
  "bar"
end

あと例題として木構造を扱う例や、act_as_csvといったクラスマクロを作成する例題などはけっこう楽しめました。そういえばこの時にforestとか作りましたが、これの影響でしょうか。

  • IO

javascriptの同じプロトタイプベースの言語なのですが、いちばんぶっ飛んでいるのが「予約語0」という点です。while, if, loopといったコントロールフローもただの関数ですし、&, |, orなどといったオペレータもOperatorTableというメタテーブルに入っているだけなので、自由に新たな語を追加したり、既存のものを変更することが出来ます。

Io> OperatorTable
==> OperatorTable_0x1002745f0:
Operators
  0   ? @ @@
  1   **
  2   % * /
  3   + -
  4   << >>
  5   < <= > >=
  6   != ==
  7   &
  8   ^
  9   |
  10  && and
  11  or ||
  12  ..
  13  %= &= *= += -= /= <<= >>= ^= |=
  14  return

Assign Operators
  ::= newSlot
  :=  setSlot
  =   updateSlot

To add a new operator: OperatorTable addOperator("+", 4) and implement the + message.
To add a new assign operator: OperatorTable addAssignOperator("=", "updateSlot") and implement the updateSlot message.

「オレオレ言語を作ろう」と思ったらIOを使えばなんでも出来るんではと思いました。japanizeという日本語インタープリターをRubyでつくりましたが、IOでもいけるかもしれませんね。あとは並列を意識したアクターモデル(coroutine, actore, future)とかもこの後のScala, Erlangで出てきますが、IOでもサポートされています。

今回読んだなかで一番ぶっ飛んだ言語だと思いました。なにしろifやloopといったコントロールフローが潜在せず、「これこれに該当するもの」というルールを定義する事で回答を導き出してくれるというものです。最初学び出した時はSQLに似ていると思いましたが、それよりももっと柔軟にデータ検出のルールを定義できるなと思いました。

ちなみに一例ですが、「アメリカの5つの州を描いた白地図がありますが。隣接する州と重複しない」というルールはこう書きます。

different(red, green). 
different(red, blue). 
different(green, red). 
different(green, blue). 
different(blue, red). 
different(blue, green).
coloring(Alabama, Mississippi, Georgia, Tennessee, Florida) :- 
  different(Mississippi, Tennessee), 
  different(Mississippi, Alabama), 
  different(Alabama, Tennessee),
  different(Alabama, Mississippi), 
  different(Alabama, Georgia), 
  different(Alabama, Florida), 
  different(Georgia, Florida), 
  different(Georgia, Tennessee).

そして以下のような感じで回答を導き出してくれます。

| ?- ['map.pl']
.
map.pl for byte code...
map.pl compiled, 14 lines read - 1769 bytes written, 17 ms

(1 ms) yes
| ?- coloring(Alabama, Mississippi, Georgia, Tennessee, Florida).

Alabama = blue
Florida = green
Georgia = red
Mississippi = red
Tennessee = green ? 

なんかすごいです。ちなみにこの「ルールにマッチするものを抜き出しなさい」というのをパターンマッチングと呼びますが、これ以降の関数型言語でもたびたび出てきます。

Scalaは以前自分で勉強したことがあったのであまり驚きはなかったと思います。

http://d.hatena.ne.jp/makotoi/20100614

Scalaにもパターンマッチングはあります。階乗は以下のように表します。

def factorial(n: Int): Int = n match {
  case 0 => 1
  case x if x > 0 => factorial(n - 1) * n
}

あとアクターモデルを使った並列処理もサポートしています。

import scala.actors._
import scala.actors.Actor._

case object Poke
case object Feed

class Kid() extends Actor {
  def act() = {
    loop{
      react{
        case Poke => {
          println("Ow...")
          println("Quit it...")
        }
        case Feed => {
          println("Gurgle....")
          println("Burp...")
        }
      }
    }
  }
}

val bart = new Kid().start
val lisa = new Kid().start
println("Ready to poke and feed...")
bart ! Poke
lisa ! Poke
bart ! Feed
lisa ! Feed

「!」 で 非同期に実行するのをしているところはgolangの「go」と似ていますね。

http://d.hatena.ne.jp/makotoi/20100628

各章は「Day1」「Day2」「Day3」と3日で読める構成なのですが、Day1を読み終わった時の感想は「Prologとほぼいっしょ」という感じでした。ただPrologとちがって少しですがif文ありますし、Pythonなどでおなじみ(ScalaHaskellにもありのですが)のList Comprehensionもあります。以下のようなかんじです。

double_all([]) -> [];
double_all([First|Rest]) -> [First + First| double_all(Rest)].

たぶんErlangで一躍有名になったであろうアクターモデルは以下のように書きます。

loop() ->
  process_flag(trap_exit, true),
  receive
    {monitor, Process} ->
      link(Process),
      io:format("Monitoring process.~n"),
      loop();
    
    {'EXIT', From, Reason} ->
      io:format("The shooter ~p died with reason ~p.", [From, Reason]),
        io:format("Start another one. ~n"),
      loop()
    end.

Scalaと同じく「!」で呼び出します。ただScalaとの違いとして(たぶん)「node@server!message」みたいな指定をすることでプロセス/マシーン間を超えた通信ができますし、"EXIT"を補足する事で、死んだプロセスを再起動とかできます。あと本書では詳しくは説明されていませんでしたがコードをホットスワップとか可能です。

Scala, と並びJVMで走る新しい言語の筆頭、「より良きLisp」とも呼ばれています。「コードはデータ」を標榜するLispでは関数もデータもコントロールフローも全て同列に扱われています。そういう意味では「予約語0」のIOと少し通じるところがあるかなと思いました。そのぶん括弧ですべてを表現する事になるのですが、色々なシンタックスシュガーがあるので本来のLisp (Common Lisp, Schema)などに比べて読みやすいそうです。でもこういうのを読みやすいかといわれるとちょっと微妙です。

(defn put
  ([cache value-map]
    (swap! cache merge value-map))
  ([cache key value]
    (swap! cache assoc key value)))
(take 5 ( drop 2 ( cycle [:a :b :c])))

は[a b c]の配列を永久に循環(cycle)し、その最初の2つを落として、その後の最初の5つをとってくるという意味で激しく括弧がつきますが

 (->> [:a :b :c](cycle) (drop 2) (take 5))

というシンタックスシュガーで左から右へ読むスタイルに変更可能です

データ型はLists, Maps, Sets, Vectorsとあって豊富です。ArrayとHashしかつかわないRubyistとしては「なんでそんなにデータ型がいるの」と疑問に思ったりしますが、それは私が関数脳になりきれていない事でしょうか。ちなみにRubyではSetは標準関数としてついてきます。結構便利なのですが結局Hashで満足してしまう事が多いですね。

話がそれました。Lispの力といえば、言語そのものを変化させるマクロです。
rubyの「unless」をマクロで作成すると以下のようになります。

user=> (defmacro unless [test body]
        (list 'if (list 'not test) body))

コマンドプロンプトで実際に確認できます。

user=> (unless  false(println "this is false"))
this is false
nil


macroexpandというコマンドを使うと元の構文を確認することが出来ます。

#'user/unless
user=> (macroexpand '(unless condition body))
(if (not condition) body)


マクロって言語の文法そのものを正規表現のようなパターンマッチングで置き換えるものだとの理解で良いのでしょうか。

IO, Scala, Erlangの並列化はアクターモデルを使用したもので、どれもloopの中でパターンマッチングをしている感じです。golangやイベント駆動系のnode.js、event machineもちょっと似た感じです。Clojureの場合はそれらとは違いSoftware Transactional Memory(STM)というものを使います。データベースのトランザクション処理に結構似ています。

3ヶ月近く前に読んだのですでに記憶が薄れかけていますが、がんばって思い出してみています。

関数言語は色々な種類があるのですが、それを統一しようとして委員会が発足してつくりあげたものだそうです。普通コンピュータ言語は少数の人(あるいは一人)によって作られる場合が多いので、結構異色ではないでしょうか。関数型言語に見られる以下の機能を備えています。

  • List, Tuples
  • list comprehension(リスト内包表記)
  • lazy evaluation (遅延評価)
  • high order functions (高階関数)
  • anonymous functions(無名関数)

本書に出てきた関数型言語とことなる機能としては「カリー化と部分適用」というものでしょうか。これは関数の引数の一部のみを適用した関数をかえすことができると言う事です。「なんのこっちゃ」と思われる方に少し説明します。

以下はかけ算をする関数です。

Prelude> let prod x y = x * y
Prelude> prod 3 4
12

この内の引数の一部に2を入れた関数「double」、3を入れた関数「triple」を作りることも出来ます。

Prelude> let double = prod 2
Prelude> let triple = prod 3
Prelude> double 3
6
Prelude> triple 4
12

すごいですね、とおもったのですが、実はRuby1.9でもできます。

http://pragdave.blogs.pragprog.com/pragdave/2008/09/fun-with-procs.html

ruby-1.9.2-p0 > prod = proc {|a, b | a * b}
 => #<Proc:0x000001009434e0@(irb):43> 
ruby-1.9.2-p0 > curried_prod = prod.curry
 => #<Proc:0x000001009394b8> 
ruby-1.9.2-p0 > double = curried_prod[2]
 => #<Proc:0x00000100916e68> 
ruby-1.9.2-p0 > triple = curried_prod[3]
 => #<Proc:0x00000100862878> 
ruby-1.9.2-p0 > double
 => #<Proc:0x00000100916e68> 
ruby-1.9.2-p0 > double[2]
 => 4 
ruby-1.9.2-p0 > triple[2]
 => 6 

Haskellに比べると少し冗長な部分もありますがRubyの関数力もあなどれません。

っでそもそもこの本を手に取るきっかけだった「モナドとはなんぞや」なんですが、正直この本を最初に読んだだけではチンプンカンプンでした。
今でもあまりわかっていないのですが、

http://d.hatena.ne.jp/kazu-yamamoto/20080208/1202456329

を読むと少し分かった気になります。もう一度機会があれば読み返したいです。

  • まとめ

「達人プログラマー」のDave Thomasは「年に一度は新しい言語を習得しましょう」と言っていますが、その7倍をたったの7週間で成し遂げてしまおうという意欲作です。

「新しい言語を学ぶ」というのはなかなか大変なことですが、各言語の特徴を映画のキャラクターに例えているところが面白いです(例えばScalaシザーハンズErlangはMatrixのエージェントスミス、ClojureStar WarsヨーダHaskellスタートレックのスポック)。

各章は3つに分かれており、週の間に1つ、週末に2つ読むと一週間で一言語に触れる事が出来ます。1つのセクションは10ページもないので2〜3時間もかからず読むことは出来ますが、要はセクションの終わりにあるエクササイズをやるかどうかにかかってくると思います。ただ本を読んだだけだとわかった気になりますが、実際に演習をやらないと身に付かないことは多いです(えらそうなことを言っていますが、だいたい私は各章の最初のセクションの演習しかやっていません)。すこし残念なのは演習の回答が載っていない事ですが、それが載っているとカンニングしてしまうおそれもあるので、あくまで自分でやりとげることに意味があるのでしょう。

今回読んだなかで一番の収穫はIOとPrologでしょうか。これらの言語は超マイナー、あるいは結構昔の言語なので、本書を手に取らなければまずトライしてみることはなかったと思います。でも実際に触れてみると自分の中のプログラミング言語に対する既成概念がくずされる思いがしました。

どの言語も「引き続き勉強してみたい」とは思うのですが、全部継続する気力はちょっとありません。関数言語を極めるという意味ではHaskellClojure、仕事で並列処理が必要な仕事に使えそうな言語としてはScalaErlang、趣味として楽しみたいけれど仕事で使う事はまずなさそうな言語としてはIOとPrologでしょうか。どれも捨てがたいです。