戦略的設計入門

"Beautiful Development"(2011.04.09 DevLOVE)の講演資料と原稿

はじめに

本日(4/9)、DevLove様と共同で、第2回"Beautiful Development"を開催致しました。これは、日本語版DDDの発売を記念し、DDDに造詣の深い方々に集まって頂き、2枠構成で講演して頂くという豪華なものでした。このカンファレンスでトリを務めさせて頂きましたので、講演資料と原稿を公開致します*1。なお、今回の発表は「ドメイン駆動設計入門」では駆け足でまとめてしまった部分を、改めてクローズアップした続編と考えて頂くこともできるでしょう。


アジェンダはこちら

  • 戦略的設計とは?
  • サンプル業務
  • モデル駆動設計をすると?
  • 戦略的設計


スライドはこちら

戦略的設計とは?

「戦略的設計(Strategic Design)」とは、DDD第4部のタイトルです。DDDは全体で4部構成になっており、第1部が基本概念の導入、第2部が実際の実装方法、第3部がリファクタリングとブレイクスルー、そして第4部が戦略的設計という構成になっています。4部構成と聞くと、分量としては、おおよそ1/4ずつ割り当てられていることを期待するものですが、実際には本全体の大体1/3程度を占めており、また議論の抽象度も上がるため、英語版をがんばって読んでいた方にとっても、第2の挫折ポイントになったケースが少なくないのではないかと思います。今回は、その戦略的設計について、基本となる考え方を説明していきたいと思います。


戦略的設計の対象は、一言で言うと、「個々のオブジェクトレベルでは把握できない、巨大で複雑なシステム」です。いわゆる「大規模」と言えばそうですが、単に「画面数が多い」というだけでなく、エンタープライズ全体を統合するようなシステムを想像して頂ければよいかと思います。システムが巨大になると、全体で1つのまとまりとして理解することが難しくなり、モデルも複数登場してきます。そうした複数のモデルを統一的に扱う手法が、戦略的設計なのです。少し見方を変えると、モデル駆動設計が1人あるいは1つのチームのドメインエキスパートのメンタルモデルを対象とするのに対し、戦略的設計では、経営層のメンタルモデルを対象とする、とも言えるかもしれません*2


この戦略的設計は、3本の柱によって支えられています。それが、コンテキスト蒸留(あるいは抽出)、そして、大規模な構造(あるいは大局的な構造)です。戦略的設計はパターンの数も多く、この時間の中ですべてを網羅することはできませんが、今回はそれぞれの基本的な考え方をお伝えしたいと考えています。


その目的のために、1つ具体的な業務を取りあげ、それがどのようにモデリングされていくのかを見ていくことにします。

サンプル業務

ここで扱うのは、「旅行手配」業務です。*3


さて、みなさん、旅行をしたことはありますか?海外旅行である必要はありませんが、「社内旅行で1泊で温泉」という感じではなく、修学旅行での「京都・奈良」のように、いくつかの都市をまわる旅行をイメージしてください。旅行者の視点からすると、「何を観光する」「何を買う」といったことが重要になると思います。しかし、ドメインエキスパートから見ると、旅行はだいぶ姿を変えます。すなわち、「各移動手段の時刻表」といったものが大切になってくるのです。これは、DDD本の随所で取り上げられている貨物輸送システムと、抽象的なレベルでは似ているかもしれません。貨物輸送システムは、定期的に運行している船なり鉄道なりといった輸送手段を予約し、それらをつないで貨物を目的地まで届けるものでした。旅行手配も、同じようにお客さんの移動手段をつなぎながら目的地まで届けます。少し違うのは、貨物では最終到着地点だけが重要なのに対して、旅行手配の場合、最終目的地は自宅ですから、乗り継ぐ場所が重要になっているということですね。


ここでは海外旅行を題材として、旅行手配の業務について見ていきます:
お客さんの動きを整理すると次のようになります。飛行機を使ってまずは目的とする土地に移動し、夜はホテルに泊まります。空港からホテルに行くために、もしかしたら送迎もつくかもしれません。ある都市から別の都市への移動には鉄道を使います。それぞれの都市では、お客さんは思い思いに観光をしますが、場合によっては事前にチケットを取っておかなければいけないものがあるかもしれません。こうしたパーツを組み合わせて、1つの旅程を組み立てます。そして、その旅行にかかる金額を見積り、成約したら請求書を送ります。

モデル駆動設計をすると?

戦略的設計に入る前に、少し1つのモデルに対して何をするかを整理しておきましょう。モデル駆動設計は、まずは「知識のかみ砕き」から始まります。ドメインエキスパートにとっての旅行のイメージを図示すると、以下のようになることが分かりました。ある都市に飛行機で入り、それぞれの都市(=黒丸)を移動手段で結び、最後は飛行機で帰ってきます。ただ、図には表現されていませんが、各都市で観光したいものに応じて滞在所要時間もかわってくるため、組み立てはもう少し複雑になります。


ドメインエキスパートの考えていることがおおよそ理解できたら、次はそれをモデリングします。その際に使われる言葉は、ドメインエキスパートが理解できるものでなければなりません。また、そこで出来上がるモデルは実装可能なものでなければなりません。こうした考え方が、ユビキタス言語モデル駆動設計です。


DDDではさらに、「すでに確立されている概念体系をモデルに当てはめることができる場合がある」と説明されています。そうすることで、そのモデルに初めて触れる人であっても、その基になっている概念体系を知ってさえいれば、スムーズに理解できるからです。今回であれば、滞在場所をノード、移動手段をエッジと考え、「グラフ」を使うことができるかもしれません*4

戦略的設計

それでは、いよいよ戦略的設計に入っていきましょう。今度は個別のモデルの内部をモデリングするのではなく、システムの全体像の把握するように試みていきます。

コンテキスト

モデリングをする際の武器は「言葉」です。特に、話し言葉のレベルでドメインエキスパートと共有し、その言葉を実装でも用いなければなりません。しかし、言葉はそれ単独で意味を持つわけではありません。言葉の意味を規定するもの、それがコンテキスト(=文脈)です。


戦略的設計の第一歩は、各モデルが位置づけられたコンテキストの境界を明確にするところから始まります。これが境界づけられたコンテキスト(BOUNDED CONTEXT)です。旅行手配ドメインを見ると、境界づけられたコンテキストを3つ見つけることができます。

  • 旅行手配:旅程を組んで代金を見積もる
  • 予約:移動手段や滞在先を予約する
  • 経理:実際に金銭を扱う


こうして境界づけられたコンテキストが確立されたら、次はこのコンテキストの内部を安定させなければいけません。このコンテキスト内で行われるすべての作業に対して一貫性を保つために行われるのが、継続的な統合(CONTINUOUS INTEGRATION)です。この統合には、自動化されたテストに代表されるようないわゆるCIだけではなく、概念上でも、ユビキタス言語を絶えず洗練させ続けることも含まれています。


コンテキストが1つであれば、ここまででよいのですが、境界づけられたコンテキストが複数存在している場合には、コンテキスト間を関係を定める必要があります。ここで登場するのが、コンテキストマップ(CONTEXT MAP)です。ここで重要なのは、コンテキスト間の接点を明示的にするということです。共有しているオブジェクトがあれば強調しなければなりませんし、何らかの変換が必要になるのであれば、それについて説明しなければなりません。


コンテキストマップが出来上がった後は、それぞれのコンテキスト間の関係を明確にしていく必要があります。DDDの第14章では、そのためのパターンがいくつか紹介されています。すべてを紹介することはできませんが、イメージをつかむためにいくつかのパターンを紹介します。


図の中に、予約のための専用端末があるとあります。これは特に航空券の予約に使われるのですが、ここではその端末との関係について考えていきたいと思います。航空券予約端末の場合、この専用端末とやり取りするためのプロトコルは予め定められています。SQLと同じような機能を持っていて、さらにそれが短くなったDSLがあると思って下さい。端末と人間が直接やり取りする場合、ドメインエキスパートは、素人にはまったく解読できないこのDSLを高速でコンソールに打ち込み、これまた素人には理解できない文字列が結果として返ってきます。Unix/Linuxのエキスパートがコマンドラインを叩いているのと、雰囲気的には一緒ですね。ここまでは、人間がインタラクションする前提で書いていますが、こうした機能はプロトコルを定めてまとめることで、たとえば、Webサービスとして公開することもできるようになります。こうしたサービスの集合が、公開ホストサービス(OPEN HOST SERVICE)と呼ばれます*5


こうした公開ホストサービスのクライアントとなる場合、何も手を打たないと、自分たちのモデルがサービス側のモデルに引きずられてしまいます。それでいいケースもありますが、自分たちのモデルの独自性を保ちたい場合には、中間に変換層を設けることが考えられます。これが腐敗防止層(ANTICORRUPTION LAYER)です。大手の旅行会社であれば、こういう仕組みがありそうです。


ただし、こうした変換層を作るためには多大なコストがかかりますし、それが常に利益と見合うわけでもありません。「せっかくCUIがあるのだから、別に人間がやっても構わない」ということであれば、システム的に統合する必要はありません。ある程度までは、Excel秀丸が最強の統合ツールだということですね。これが、別々の道(SEPARATE WAYS)と名付けられている、統合しない統合パターンです。

蒸留

さて、戦略的設計の2つめの柱が蒸留(抽出)です。専用端末との統合と聞いて、心が踊った方も多いのではないでしょうか。それが、エンジニアの基本的な習性ですよね。しかし、ドメイン駆動設計が重視するのは、技術ではなく、ビジネスです。したがって、フレームワークチームにエースを投入するというのは、ドメイン駆動設計にとってはアンチパターンとなります。そのビジネスにとって、最も重要な場所に注力しなさい、という命題が蒸留では語られています。この最も重要な場所がコアドメイン(CORE DOMAIN)と呼ばれます。


それでは、今回の旅行手配業務におけるコアドメインはどこでしょう?コンテキストで言えば旅程作成ですが、さらに細かく言えば、実際に旅程を組み立てるところだと言えるでしょう。商品の価格を集計して見積りする部分に対しては、旅行手配に限らず、もう少し汎用的に考えることができるはずです。こうしたコアドメインを補佐する汎用的な領域が汎用サブドメイン(GENERIC SUBDOMAIN)と呼ばれています。戦力をコアドメインに集中させることが重要で、汎用サブドメインであれば、実は既製品を当てはめても構わないかもしれないのです。

大規模な構造

さて、ここまでで、登場するコンテキストの地図とそれに対する重みづけはできるようになりました。しかし、まだ1つ欠けているものがあります。それが全体をとらえるための構造です。「木を見て、森を見ず」にならないよう、全体をとらえるパターンを見出すこと、それが大規模な構造(大局的構造)です。ここで言う大規模とは、単純に数が多いという意味での規模ではなく、全体をとらえるという意味であるということに注意してください。


こうした構造をとらえるパターンの1つが、責務のレイヤ(RESPONSIBILITY LAYERS)です。モデルの中で概念上の依存関係を明確化し、階層化された責務に対して、抽象的な責務を割り当てます。ここではDDDで取り上げられている例のうち、いくつかを当てはめてみました:

  • 能力:何ができるのか。このシステムで扱うリソース。
  • 業務:能力を利用して、何ができるのか。その時の状況を反映する。
  • 意思決定支援:どのように活動するべきなのか


もし、システムの目的が、引き継ぎなどのためにお客さんに提示した旅程を保存するためだけであれば、あるいは、登録した旅程を帳票出力したり、商品の金額計算を自動的に行ったりするためだけであれば、意思決定支援層は必要ないでしょう。そうではなく、たとえば、特定の航空会社を優先的に使いたいというバイアスをかける必要があるのであれば、そうした情報を保持する意思決定支援層あるいはポリシー層が必要になるでしょう。


さらに、重要なことですが、こうした概念上の大規模な構造は、アップフロントに設計し、詳細な設計を制約しすぎてしまってはいけません。アプリケーションと共に成長させ、必要があればまったく別のものに置き換えなければならないとされます。この命題には、進化する秩序(EVOLVING ORDER)という名前が与えられています。

最後に

戦略的設計にしても、モデル駆動設計にしても、DDDに一貫して流れている1つのテーマがあります。それが「有機的秩序(organic order)」と呼ばれているものです。1つのオブジェクトから、それが集まってエンタープライズ全体を統合するに至るまで、それぞれの抽象度でモデルとしての統一性を失わない点にあると言えるでしょう。一定の凝集度を持ったそれぞれの単位が、環境に対して柔軟に適応しながら成長し、1つの有機的な全体として変化し続ける。DDDは、そういった、ビジネスに寄り添って成長していくシステムを作るための方法論なのです。

*1:事前に準備していたもののため、内容については一部相違がある可能性がありますが、ご了承ください

*2:この辺りについては、「スクラムによるドメイン駆動設計 - Digital Romanticism」をご参照ください

*3:実はこれは、日本語版DDDのレビューとして全章を読破し、今や強力なドメインエキスパートになった妻の本業でもあります。ちなみに、妻はDDDに対するコメントとして、「大切なところにできる人を入れて、知識をかみ砕いて、リファクタリングするんでしょ?」と言っておりました。だいたいあっているかと。

*4:【2011.04.11 追記】私自身は汎用化とは別のこうした抽象化は、ビジネスモデルをとらえる上での重要な武器になると考えていたのですが、Ericと話していて間違っていたことに気がつきました。こうした「確立された形式」でとらえることができない特殊なものこそが、実はビジネスの核心だということのようです。こうした視点は、安易なシステム開発が早々にシステム化をあきらめる場所に向かっていく力を生み出しえるものです。

*5:実際にそういうサービスが存在するかどうかは問題にしていません。あるかもしれませんし、ないかもしれません。

関数型Scala(4):クロージャ - Mario Gleichmann

この記事はMario Gleichmann氏による、「Functional Scala」シリーズの第4回「Functional Scala: Closures | brain driven development」を、氏の許可を得て翻訳したものです。(原文公開日:2010年11月15日)




関数型Scalaの第4話にようこそ!


今回はあちこちで議論されているクロージャを詳細に調べ、クロージャに関する誤解を解きたいと思います。単純な関数とクロージャを混同している議論が非常に多いからです。第一に、クロージャはある特別な特徴(これについて話していきます)を備えた関数です。その特徴によって、関数はクロージャになるのです。


一般的な関数の定義と、その特徴については、これまでの3話を通じてすでに深い知識を得ていますので、もう時間を無駄にせずに、関数の他の例を見てみましょう。この関数は、与えられた整数値のリストを、そのリスト内にある最初の要素によってフィルタリングすることを意図しています。つまり、最初の要素よりも小さい値はすべて結果に含まれることになります。確かに、この関数にはそれほど意味はありませんが(しかも、リストが空だとうまく機能しません)、クロージャの世界に対する、適切な入り口にはなっています:

val belowFirst = ( xs : List[Int] ) => { 

    val first = xs( 0 ) // ★

    val isBelow = ( y : Int ) => y < first // ★★

    for( x <- xs; if( isBelow( x ) ) ) yield x // ★★★
} 

... 
belowFirst( List( 5, 1, 7, 4, 9, 11, 3 ) ) // => List( 1, 4, 3 ) 

ちょっと待って下さい!ここで何が起きているのでしょう?実際、この例には新しい機能がいくつか登場しています。関数の中身の仕組みを一行ずつ理解していきましょう:


3行目(★)には目新しいことはありません。この式は単に、与えられたリスト内にある最初の要素を、値 first を宣言することで参照しています。そして、その値の型は、Int でなければなりません。整数値のリストから取得した要素だからです(このワクワクする仕事は、コンパイラ型推論能力に委ねます)。こうすることで、最初の要素を参照する際、この別名を使用できるようになりました・・・


よろしい、値の宣言の1つは、理解できました。もう1つは5行目(★★)にあります。それでは、詳細に見てみましょう。値 isBelow の型を当てられますか?そう、これはもう一つの(ローカルな)関数です。これについては、問題にならないでしょう。型が普通の(第一級の)値であり、他の値(例えば、Int 型の値 first )と比べて、それ以上でもそれ以下でもないからです。あなたが、Haskellについてよく知っていれば、このネストされた関数がwhere句で定義されていると考えるかもしれません。さて、このローカル関数が今回クロージャとしての資格を与えられていることがわかります。これについての詳細は、すぐに見ていきます。


この調査を適切に完了するため、7行目(★★★)を見てみましょう。ここで行われているのは、いわゆるリストの内包です(これについては別のエピソードでとりあげます)。さしあたり、これについては、入力されたリスト xs の各要素を見て、ifによるガード節に適合するかどうかで、出力されるリストに(内包表記全体の結果となる値として)設定するかどうかを決定しているのだと考えて下さい。このリストの内包表記は、関数内での最後の表現なので、これも関数の結果となります。

開いた項

ネストされたローカル関数を調べて、いくらか不審に思ったかもしれません。コンテキストから取り出し、個別に細かく見てみましょう:

...
val isBelow = ( y : Int ) => y < first
...

関数は結果を算出する際、引数だけに依存すると言いませんでしたか?そうだとすれば、変数 first はどうなるのでしょう(命令型の言語でいう可変な変数ではありませんね)?宣言された引数は y だけです。したがって、関数内での y の使用はその引数に束縛されます。他に引数はないので、関数の本体での first の使用は引数に束縛されません!関数の引数として宣言されたり、ローカルで導入されない引数を見つけたら、おめでとう、あなたが見つけたのは、いわゆる自由変数(free variable)です。話はさらに続きます。少なくとも1つの自由変数を含んでいる関数は、開いた項と呼ばれます。完全な関数型の関数に到達するには、あらゆる自由関数は束縛されなければなりません(これは、開いた項を閉じた項にすることで実現されます)。このために、コンパイラはいわゆるレキシカルな環境*1に向かいます。この環境において関数が定義され、束縛の対象を探そうとします。このプロセスが囲い込みclosing over )と呼ばれ、その結果が、閉じた表現、あるいはより短く、クロージャと言われるのです。


上記の例において、ローカル関数が定義されるスコープは、レキシカルな環境の中で最も近いローカルの部分でした。それは、ローカル関数を取り巻く関数の本体です。そして、そのスコープの中で、自由変数は値の定義である first に束縛され、与えられたリストの中の最初の要素を参照するのです。しかし、自由変数が周囲を取り巻く関数の本体のスコープ内で値に束縛されなかった場合には、どうなるでしょう?そうですね、束縛プロセスは完了しないでしょう。このような場合、コンパイラは適切な束縛を求めて検索範囲を広げる必要があります。しかし、次はどこを探すのでしょう?周囲を取り巻く関数の引数も、レキシカルな環境に属しています。したがって、クロージャの中の自由変数も、引数に束縛される可能性があります(そのおかげで、後のエピソードで見ていく、強力な抽象化が引き起こされます。ここでは、クロージャの持つ力を、特に高階関数と紐づけて言及する必要があります。これまでいつも高階関数に触れてきたからです)。そこまで見ても束縛する対象がなければ、コンパイラはスコープを拡大し、周囲を取り巻く関数自体が定義されている領域を探します。したがって、次のシナリオは完全に合法的なのです:

val offset = 3 
...
val below = ( barrier :Int, xs :List[Int] ) => { 

    val isBelow = ( y : Int ) => y < barrier + offset 

    for( x <- xs; if( isBelow( x ) ) ) yield x 
}

... 
below( 5, List( 5, 1, 7, 4, 9, 11, 3 ) ) // => List( 5, 1, 7, 4, 3 )

よろしい、関数 isBelow の中には2つの自由変数、barrier と offset があります。そして、barrier は周囲を取り巻く関数の第一引数に束縛される一方で、コンパイラは、offset に対する妥当な束縛対象を見つけるために、さらに外へと閉じるスコープを広げなければなりません。全体としては、もはや怖いものはありません。単なるシンプルな普通のクロージャです!

束縛 - 値(val) vs. 変数(var)

自由変数が不変の値に束縛される限り、関数型の世界ではすべてがうまくいきます。クロージャの複数回呼ばれても、その値が変わることはないので、少なくとも、束縛変数に依存する箇所では、関数の結果となる値が変わることはありません(ただし、引数が同じなら、ですが)。そして、もちろん、逆もまた然りです。つまり、クロージャ自体は、束縛変数を変えることができず、したがって、一切の副作用をもたらすことができないのです。したがって、全体として見れば、扱う値がすべて不変であれば(もしくは、その言語に値の割り当てという考え方がなければ)、クロージャであっても純粋な関数になるのです。


しかし、Scalaではどうでしょう?Scalaでは、可変な変数が提供されており、したがってそうした変数の(再)割り当ても提供されているので、クロージャが純粋な関数かどうかは、Scalaがある種の静的束縛を行うのか、それとも動的型づけを行うのかにかかっています。静的束縛においては、自由変数が(不変な値)に直接束縛されるのに対して、動的型づけでは、自由変数が、値を格納できるメモリ内の相対的な位置に束縛されます(そして束縛された位置にある値は変化するかもしれません)。さて、このScalaのふるまいは、単純な実験を行うことで明らかにすることができます。

var minAge = 18 

val isGermanAdult = isAdult( 20 ) // => true 

minAge = 21 // ★

val isUsAdult = isAdult( 20 ) // => false 

おそらく、あなたはもう何が起きているのか明確にわかるようになっているのではないでしょうか?関数 isAdult は、自由変数 minAge を含むことからクロージャであるようです。自由変数は、周囲を取り巻く環境において、変数に束縛されます。minAge が不変の値ではなく、可変の値として宣言されていることに注意してください!それでは、「★」の行を、より詳細に見ていきましょう。ここで変数の値が変更されています。これは、同じ引数に2度適用されているクロージャ isAdult に対する2回の呼び出しのちょうど中間で行われています。まさか!ここで書いた関数の結果は、関数呼び出しの間に束縛される変数が変わったことに伴い、異なる値になります。


では、どういう種類の束縛が行われているのでしょうか?クロージャが、変数の変化に合わせて異なるふるまいをしていることから、動的であるに違いありません。関数が定義された時には、束縛は値を参照せず、メモリ上の位置を参照するのです。またしても、Scalaにしてやられたわけですが、これはScalaがオブジェクト(命令型)と関数型のハイブリッド言語であり、命令的な文法構造を許可していることによるものです。クロージャの構築と使用に関して言うと、ここでもまた純粋ではない関数を作り出してしまう罠に陥ってしまうかもしれないということを意味します。

ポイント

それでは、今回のポイントは何でしょうか?
今回見てきたのは、クロージャがある特殊な関数であり、関数を取り巻くレキシカルなスコープ内で値に束縛される自由変数を参照するということでした。「なるほど、それから?」とあなたは不思議に思って尋ねるかもしれません。「本質的には一般的な関数に関する議論をほとんど理解したという点を除けば、クロージャについては部分的にか理解していません」と。でも、それがどうだというのです?今回は、その問いを完全に満足させる答えは出せません。この後のエピソードでわかることですが、クロージャはさまざまな領域で本質的な役割を果たします。これは、単純な状態の表現にはじまり、モナドに至ります(ここで、目が覚めたのではないでしょうか。モナドは今日とても流行っていて、チームメンバや友人に感銘を与えることができるでしょうから)。


そして、何度か我々が発見したことがもう1つあります。Scalaは命令型と関数型のやり方の両方に対して大いなる自由を与えますが、混ぜ合わせようとすると問題に行き当たるでしょう。関数や、関数の息づく環境に、状態を導入すると、とたんに純粋ではない関数を作り出してしまう危険が生じます。 そのことは、覚えておかなければなりません。もちろん、オブジェクト指向と関数型の考え方をブレンドすることで得られる選択肢から利益を享受することはできます。しかし、自由度が広がれば、規律も強めなければならないのです。




プログラミングScala

プログラミングScala

Scalaスケーラブルプログラミング[コンセプト&コーディング] (Programming in Scala)

Scalaスケーラブルプログラミング[コンセプト&コーディング] (Programming in Scala)

*1:静的スコープ、もしくは構文スコープとも言われる。クロージャは変数の解決を、実行時の環境ではなく、関数が定義された環境に基づいて行う。クロージャ - Wikipedia

関数型Scala(3):関数としてのオブジェクトとしての関数 - Mario Gleichmann

この記事はMario Gleichmann氏による、「Functional Scala」シリーズの第3回「Functional Scala: Functions as Objects as Functions | brain driven development」を、氏の許可を得て翻訳したものです。(原文公開日:2010年11月8日)




関数型Scalaの第3話へようこそ!
今回は、Scalaの内部を軽く見ていき、前回お話しした不可解な関数の型の謎を明らかにしていきたいと思います。つまり、今回は一般的な関数型プログラミングよりは、むしろScalaの専門的な話になります。


関数は、単純に関数リテラルを指定することで定義できるのだということを、思い出して下さい。この関数リテラルは、関数パラメタのリストと関数の本体で構成されています(両者は関数矢印 => によって分けられます)。さらに、特定の関数の型を持った名前つきの値としてその関数が宣言されるかもしれません:

val power : ( Int, Int ) => Int = ( base: Int, exp :Int ) => if( exp <= 1 ) base else base * power( base, exp - 1 ) 

それでは進めていきましょう!ここにあるのは、普通の関数で、Int 型の2つの引数に適用されることで、Int 型の別の値になります。したがって、関数の型は次のようになります。

( Int, Int ) => Int

目新しいものは何もありません!既にわかっている通り、あらゆる関数は特定の関数の型の1インスタンスなのです。

しかし、すべてはObjectなんでしょう?

おそらく、そう聞いたことがあるとおもいます。Scalaにおいては、すべてがObjectです。プリミティブもなければ、ラッパー型もなく、オートボクシングも(目に見えるところには)存在しません。しかし、それでは、このれっきとした関数はどうなるのでしょう?つまり、関数は関数型の世界に属し、オブジェクトはオブジェクト指向の世界に属しているのではないのでしょうか?確かに、Scalaはオブジェクトと関数のハイブリッドなので、両者の間で整合性をとらなければなりません。そして、Scalaにおいてはあらゆる値がObjectなので、関数もObjectなのです。わかりきったことじゃないですか!?

FunctionN

これまでに、Scalaで定義した関数はどれも、特定のFunction トレイトの特徴を持つ実装の1インスタンスになることがわかります(トレイトが何であるかということについて、手がかりを持っておらず、ただ、Javaについて聞いたことがあるというのであれば、トレイトについて、さしあたりインタフェースの強化版だと考えて下さい)。このFunction トレイトは、Function1 から Function22 まであります。なぜ、これほど多いのでしょうか?Scalaの製作者が重装備を思いつき、関数の重要性を示すために出費や努力を惜しまなかったということでしょうか?まあ、そんな感じです。Scalaにおいて、関数はObjectであり、そして、Scalaが静的型付け言語であるため、異なる数の引数を持つ関数それぞれに対して適切な型を提供しなければなりません。2つの引数を持つ関数を定義すれば、コンパイラはその型として Function2を選び出します。引数の7つある関数を定義すれば、Function7 になるわけです。そして、最大が Function22 であることから、引数を 23 以上取る関数を定義することはできません。残念ですね。そう思いませんか*1


もう少し詳細に見ていきましょう。前回のエピソードで使った関数を思い出して下さい。これは、 Int 型の引数を3つ取り、結果が boolean の値となるもので、3つの引数が、いわゆるピタゴラス数になるかどうかを判定するものでした。

val pythTriple : ( Int, Int, Int ) => Boolean = ( a :Int, b :Int, c :Int ) => { 
    val aSquare = a * a 
    val bSquare = b * b 
    val cSquare = c * c 

    aSquare + bSquare == cSquare
}

さて、どのFunction トレイトが使われるか当ててください。Function3 を選ばなかった方、残念ながらハズレです。
いいでしょう。3つの引数を取る関数であれば、Function3 になり、n 個の引数を取る関数であれば、FunctionN となります。それでまったく問題ないのですが、引数の方についてはどうでしょう?また、関数の結果の型についてはどうなるのでしょう?おそらくあなたは、型付きパラメタを思いついたでしょうし、それが正解です!トレイトのscala.Function3を見てみましょう。今回の話と関係のある部分だけを見せることにします:

trait Function3[-T1, -T2, -T3, +R] extends AnyRef { 
    ... 
    def apply( v1 :T1, v2 :T2, v3 :T3 ) :R
    ...
}

先ほど書いた関数がどこにあるかわかりますか?3つの型パラメタ、T1T2 、そして T3 が引数の型となり、型パラメタ R が、関数の結果の型を表しているのです。さしあたって、型パラメタの前にある記号は気にしないで下さい。この記号が示しているのは、各パラメタの変位指定(variance)の種類です(引数が反変(contravariant)であるのに対し、結果は共変(covariant)としてふるまいます。しかし、心配しないでください。あなたがリスコフ置換原則と、スーパータイプの代わりにサブタイプをどう使うかということについて知っているならば、型パラメタの変位指定が何を表現しようとしているのかについて、適切な直観を得ることができるでしょう)。


さて、我々のよく知っている関数リテラルによって定義される具体的な関数は、どれも、コンパイラが適切なFunction トレイトのインスタンスに変換します。そのFunction トレイトでは、型パラメタが引数の型と関数の結果の型を用いてパラメタ化されます。ちょっと待ってください。我々の書いた関数には、すでに型がありますよね。上記の例を見てください。関数 pythTriple の型は明らかに次に示すものとなります:

( Int, Int, Int ) => Boolean

そうです!しかし、これは実際に操作される適切な関数の型のための糖衣構文にすぎず、次に示すものと変わらないのです:

Function3[Int, Int, Int, Boolean]

さらに、お気づきの通り、ある形式の型宣言は、別の形式の糖衣構文にすぎず、両者は交換可能なのです。したがって、我々の書いた関数は次のように宣言することもできたのです:

val pythTriple : Function3[Int,Int,Int,Boolean] = ( a :Int, b :Int, c :Int ) => ...

よろしい!しかし、問題が1つ残っています。それでは、抽象メソッドである apply は何を表現しているのでしょうか?そうです、このメソッドが、関数を適用する際に呼び出されるものなのです(したがって、関数の本体はこのメソッドの中に位置づけられます)!すでに別の種類の糖衣構文をかぎつけましたね?正解です。これまでに示した、わかりやすく数学っぽい関数の適用を表す記法は、単純に引数を丸カッコで括り、関数名の後に続けるというものでしたが、これは、与えられた引数でメソッド apply を呼び出す糖衣構文に他なりません。これら2種類の記法は、もっとおかしな混ぜ合わせができます。


一方で、与えられた関数の applyメソッドを明示的に呼び出すことで、関数を適用することもできます:

val isPythTriple_3_4_5 = pythTriple.apply( 3, 4, 5 ) 

逆に、適切なFunction トレイトを実装し、必要な apply メソッドを関数本体として定義することで、関数をつくることもできます。今回の例では、Function3 を使います:

val isPythTripple2 : (Int,Int,Int) => Boolean = new Function3[Int,Int,Int,Boolean]{ 
    def apply( a :Int, b :Int, c :Int ) :Boolean = { 
        val aSquare = a * a 
        val bSquare = b * b 
        val cSquare = c * c 
        
        aSquare + bSquare == cSquare
    }
}

誘惑に注意

関数を定義するのに、よりオブジェクト指向的なスタイルを用いて、上記の例で示したように FunctionN に対する無名の実装を提供したり、特定の Function トレイトを継承した新しいクラスを作ることさえもできます(その場合、関数を構成するのはそのクラスのインスタンスであって、クラス自体ではありません)。そこで、ふるまいだけでなく、状態も導入したいという誘惑にかられます。クラスであれば、メソッドだけでなく、フィールドも宣言できるからです。そして、そうしたフィールドは変数としても宣言できるので(覚えていますか。Scalaは関数型の世界と命令型の世界の両方に貢献しなければならないのです)、副作用のある関数(つまり、引数だけに依存するのでない関数、あるいは適用の結果として単一の値だけを生成するのでない関数です)を書こうとした時に妨げるものはありません:

class Last( init : Int ) extends Function1[Int,Int]{ 
    var last = init 

    def apply( x :Int ) : Int = { 
        val retval = last 
        last = x 
        retval 
    }
}

val last : Int => Int = new Last( 1 ) 

val x = last( 5 ) // x == 1 // ★1
val y = last( 5 ) // y == 5 // ★2
val z = last( 8 ) // z == 5 
 ...
val a = last( 5 ) // a == 8  // ★3

よろしい、これ自体はそれほど役に立つ関数ではありません。しかし、これによって、Scalaが関数型のスタイルを促進しているかどうかという問いに対しては、いくつかの弱点が示されています。少なくともScalaでは、純粋ではない、副作用のある関数を書くことを防げません。思い出していただきたいのですが、純粋な関数は引数にのみ依存し、ある引数 x に対していつ適用させても、同じ結果 f(x) になります。これはその引数に適用させた回数にも依存しません。(この性質は、関数 last では明らかに破られています。実際に、13 行目と14 行目(★1と★2)を慎重に見れば、関数が同じ引数 5 に対して2回適用されているのがわかると思いますが、結果は異なる値になっています)。この性質は、「関数の等価性(the Equality of Functions)」と呼ばれることもあります。引数が等価であれば、関数を適用しても等価のままだからです。あなたが、この性質に対する、もっと正式で数学的な説明が欲しいと思うのであれば、その好奇心を満足させることができます。 この性質は次のように説明できます:

x == y <==> f( x ) == f( y )

確かに、これは数学の講義でありません。しかし、あなたがもう少し読み続けてくれるなら、純粋な関数の持つ興味深い性質をご紹介します。前述した通り、同じ引数に対して関数を適用すれば、常に同じ値が返されます。そして、これは、関数をいつ呼び出しても同じ値が返されるということを意味します。したがって、関数型の世界には時間の概念がないのです。なるほど、こうして輪が完結しました。時間の概念がないのであれば、関数の呼び出しをどういう順序で行うかは問題でなくなります。つまりどういう順序で実行されてもよいのです。このことは、第1話で、関数型プログラミングを命令型プログラミングのパラダイムと比較した際の、興味深い特徴でした。命令型の、状態を変化させる世界では、ステートメントがどの順序で実行されるかということを常に意識しなければなりません。そして、前述の例を見直すと、ここで書いた関数は純粋ではないので、再び時間が関係してきてしまっています(17 行目(★3)を見て下さい。結果となる値について説明するには、もはや関数の定義を見るだけでは十分でなく、関数が呼び出される順序についても意識しなければなりません。この場合であれば、どの値が関数に対して最後に適用されたのかが問題になります)。

ポイント

今回のエピソードで覚えておかなければいけないものが1つだけあるとすれば、関数に状態を導入するのは危険な誘惑だということです。関数リテラルのスタイルとその根底にあるオブジェクト指向の記法を望みのかたちで自由に混ぜ合わせることができても、です。関数が状態を持ってしまうと、とたんに、実行時に時間の影響を受けることになります。関数を定義する際にリテラル形式にこだわれば、そのリスクを最小限におさえることができます。このやり方では、状態を導入するのが一層難しくなるからです。ただ覚えておいて頂きたいのは、少なくともScalaにおいては、関数は常に特定の Function 型のインスタンス(つまりオブジェクト)にまで煮詰められるということです。そこから逃れる術はありません!



付録

Scalaでは、関数が Object まで煮つめられるので、Javaでも似たような解決策があるかもしれないと思ったかもしれません。そして、実際に、Javaで少しでも関数型プログラミングを行うことを目論んだライブラリもあります。lambdaj や、Functional Java を見たことがあるかもしれませんね。私自身も functJ というライブラリを昔書いたことがあります。これは、関数定義を行う際に、どこまで定型コードを避けられるかを実験したものでした。次回以降のエピソードで見ていくように、Scalaによって、糖衣構文としての関数リテラルがスムーズに統合されているだけではなく、他にも関数型プログラミングの考え方がいくつかJavaプラットフォームにもたらされています。

*1:訳註:もちろん、冗談

読書マップとDDD 〜DDDは3冊目の本?〜

技術書の読み方を考えつつ、『ドメイン駆動設計』を位置づける。

はじめに 〜1冊目の本〜

あなたは技術書をどのくらい読んでいるでしょうか?多い人は月何冊も読んでいるでしょうし、もう何年も読んでいないという人もいるでしょう*1。あるいは、「買っているけど読んでいない」ということもあるかもしれませんね。私にも積んでしまっている本があります。


技術書はすでにかなりの数が出版されており、その数は増え続けています。経験を積んだ方であれば、「自分にとって必要な本を見抜く眼」を持っているものですが、これから学ぼうとする方にとっては、読んだ方がよさそうな本の数も値段も絶望的かもしれません。このエントリの目的の1つに、『ドメイン駆動設計』の紹介があることは否定しませんが、それ以上に、「技術書の読み方」について考えてみたいと思います。


仕事としてプログラミングに携わる人であれば、「退職するまでに1冊も読まない人」はおそらくいないでしょう。手がけることになるプログラミング言語について、少なくとも1冊はその言語の入門書を読むことになるからです。あなたがまだ新人のJavaプログラマで、最初に読むべき1冊を探しているのであれば、結城さんの次の本がオススメです。

改訂第2版 Java言語プログラミングレッスン (上)

改訂第2版 Java言語プログラミングレッスン (上)

改訂第2版 Java言語プログラミングレッスン (下)

改訂第2版 Java言語プログラミングレッスン (下)

この類の本は「読む」だけではなく、絶対にサンプルコードを自分で打って、動かしながら読み進めるべきです。あとは、JavaScriptSQL、さらにはXMLなど、特定の要素技術についてのもう少し詳しい本が読みたくなることもあるでしょう。書店でパラパラとめくって、自分に合いそうなものを選ぶのがよいと思います。こうした必要に応じて買っていく本を、ここでは1冊目の本と呼びます。

2冊目の本 〜より美しく〜

自分のやりたい処理がとりあえず実装できるようになっても、プログラマとしてはまだ足りません。コードは書くものであると同時に、読まれるものでもあるからです。美しいコード、あるいは読みやすいコードを書く、という観点から考えると、やはり古典はこの1冊でしょう。

リファクタリング―プログラムの体質改善テクニック (Object Technology Series)

リファクタリング―プログラムの体質改善テクニック (Object Technology Series)

  • 作者: マーチンファウラー,Martin Fowler,児玉公信,平澤章,友野晶夫,梅沢真史
  • 出版社/メーカー: ピアソンエデュケーション
  • 発売日: 2000/05
  • メディア: 単行本
  • 購入: 94人 クリック: 3,091回
  • この商品を含むブログ (312件) を見る
リファクタリング」とは、ご存知の通り、「ソフトウェアの外部のふるまいを保ったまま、内部の構造を改善していく」というもので、プログラミングにおいてはもはや必須のプラクティスです。その具体的なやり方をまとめたこの本は、あらゆるプログラマにとっての必読書であり、コードレビューに無駄な時間を費やすくらいなら、1日かけてチームメンバで『リファクタリング』の読書会でもやった方がよっぽど生産的ではないかと思わせる1冊です。「とりあえず業務ができればいい、それほどたくさん技術書を買うつもりはない」と思っている方も、入門書の後でもう1冊、どうかこれだけは読んで下さい。


このくらいの本になると、入門書と違って、「ちょっとよくわからない」場所が出て来るかもしれません。そういう場所を適度に飛ばしつつ、全体をとらえる読み方が求められるようになってきます。「難しい」という感想は思考の停止です。何が理解を妨げているのかを正しく分析することが大切ですね。入門から中級への橋渡しをしてくれる本も何冊も出ていますので、そういう本を読んで補うという戦略もあると思います。

3冊目の本 〜ユーザのために〜

「必要な処理がきれいに書けるようになりました」という状態になったところで、ぜひ読んでいただきたいのが、『ドメイン駆動設計』、通称DDD(Domain-Driven Design)です*2

エリック・エヴァンスのドメイン駆動設計 (IT Architects’Archive ソフトウェア開発の実践)

エリック・エヴァンスのドメイン駆動設計 (IT Architects’Archive ソフトウェア開発の実践)

プログラムコードが、実はユーザの業務のとらえ方を反映したものでなければならないこと、そのためにはどうすればよいのか、ということにはじまり、プロジェクトの中でソフトウェアが成長していく過程や、企業内の業務全体を統合するようなソフトウェアの設計手法など、ユーザの役に立つソフトウェアを作るとはどういうことなのかが、丁寧に語られています*3


扱っている内容に抽象的なものが多く、英語として見ると比較的難しめで、かつ500ページ超の防弾仕様であることから、原書を読むにはなかなか敷居が高い本でした。それにもかかわらず、長いこと翻訳が出版されずにいたためか、位置づけについて変にこじらせてしまったところがあるように思います。現実には、実に真っ当なことが体系立てて書かれている良書です。オブジェクト指向とかを好きな人が読む本」と敬遠しないで下さい。本当に大切なのは、例えば「リポジトリクラスを作るかどうか」ではなく、ユーザの業務のとらえ方にどこまで添うことができるか、どこまで言葉を丁寧に扱うかといった、もっと抽象的で普遍的な部分です。


読書、という観点で考えると、「ドメインモデルを反映したソフトウェアを作る」という観点から、いくつかのエントリポイントが提供されます。

その先に

この辺りから、読みたい本、読むべき本は少しずつ拡散していきます。いくつか例を挙げましょう。


読むべき本の量に圧倒されないように、最初は「まず5冊だけ読む」あるいは「まず10冊だけ読む」と決めて、各カテゴリから1冊ずつ選んで読んでみるというのがいいと思います。その時に大切なのは、入門書を何冊も買ってしまわないこと。本屋に行けば、同一ジャンルの入門書が何冊も並んでいますが、全てを読む必要はありません。同系の入門書を3冊買うなら、後の2冊のお金は、もう一歩上の本に振り分けた方が有益でしょう。


何かについてある程度深く知りたいと思うと、1冊読めば十分ということはなく、関係する本を何冊か読まなければいけなくなっていくと思います。良書と呼ばれる本には1冊1冊に価値がありますが、特定分野の本を何冊か読むことで見えて来るものもあります。重要なのは目的意識を持って本に取り組むこと、読んだ本を自分の中に位置づけていくことですね。さらに、巻末に必ず付いている参考文献一覧も、著者がどういう本の中にその本を位置づけているかを理解する上での参考になります。流行を追いかけることもある程度は必要ですが、自分なりの読書マップを作りながら、「自分が興味を持っている分野」を組み立てていくような読み方が大切なのだと思います。中級以上の本を5冊〜10冊読み終わる頃には、少し世界の見え方が変わってくるはずです。

*1:私自身が買っているのは、平均すれば月2冊程度だと思います。パラパラと見てはいますが、完読しているものはほとんどないですね。

*2:この辺りに位置づけたい、という訳者の欲目もあるかもしれませんが、Kent Beckも言っている通り、必読書と考えて間違いないと思います。

*3:DDDの内容については、別エントリの「ドメイン駆動設計入門 - Digital Romanticism」を参照してください。

*4:第2版では、サンプルコードが Java になっているようです。私は時期的に初版の方(C#)を読みました。

関数型Scala(2):関数 - Mario Gleichmann

この記事はMario Gleichmann氏による、「Functional Scala」シリーズの第2回「Functional Scala: Functions | brain driven development」を、氏の許可を得て翻訳したものです。(原文公開日:2010年10月31日)




関数型Scalaの第2話へようこそ!前回は、コアとなる考え方を調査し、式に基づく関数の適用を、関数型プログラミングの基本的な処理方式として抽出しました。今回は、Erik Meijer博士(※1)であれば、関数型プログラミングの必需品(the bread and butter)と呼ぶであろうものから始めましょう。それは・・・なんと・・・関数です(BGMとしてトランペットが鳴り響いていると想像して下さい)


関数を呼び出すためには(これは、関数を引数に適用すると表現されます)、まず、関数を定義しなければなりません。そこで、Scalaでは関数をどうやって定義するのか、その感覚をつかみましょう・・・

関数リテラル

前回のエピソードで示した、関数の定義を思い出しましょう:

関数とは、1つ以上の引数をとり、1つの結果を生成するマッピングなのです。(このマッピングは、引数からどのように結果が計算されるかを定める関数の定義によって行われます。)

したがって、関数を定義する上で行うべきなのは、関数が操作を行うパラメタ(と、Scalaが静的型付け言語であるため、その型)を宣言し、その結果がどのように算出されるかを、通常は与えられたパラメタを用いて定義することだけです。Scalaを使うと、関数をリテラル形式で定義することができます。これは、前述した関数の本質的な部品を定義することで行われます。つまり、パラメタのリストの後に関数の本体を定義します:

( x :Int, y :Int ) => x + y

ここに示したのは、シンプルな関数リテラルです。まずパラメタ(どちらもInt型)のリストを宣言し、結果を算出する数式が後に続きます。両者は左辺のパラメタを右辺の関数本体と区別する=>(これは関数矢印(function arrow)と呼びましょう)によって分けられます。このリテラル形式は、特殊な関数の型(function type)のインスタンスを構築する糖衣構文(syntactic sugar)であることがわかります(後のエピソードで詳しく取り上げます)。


なんと!シンプルですね。しかし、必要な関数が、上述の例よりもいくらか「長い」、より複雑な処理を必要とするとしたらどうでしょう?もちろん、複数行にわたる関数は、関数の本体を波カッコで括ることで定義できます。

( a :Int, b :Int, c :Int ) => { 
    val aSquare = a * a 
    val bSquare = b * b 
    val cSquare = c * c 

    aSquare + bSquare == cSquare 
}

いくらか長くなったこの関数を見て下さい。これは、与えられた3つの整数が、いわゆるピタゴラスかどうかを判定するものです。待て待て、とあなたは言うかもしれません。いいでしょう。例の中には複数の行があります。言われていた波カッコも見てとれます。しかし、関数が呼び出し元に何を戻すかを記述した return 文がありません。それでは、関数の本体を一行ずつ見ていきましょう。


2行目から4行目にかけては、与えられたパラメタを用いて中間的な値を計算しています。この中間的な値を後から参照するために、val を使って別名を与えています。6行目では、もう少し面白くなります。ここにあるのは、前に計算された中間的な値を用いて作られた式です。Scalaでは、関数全体の結果は、常に関数本体の最後の式の値を評価したものになることがわかっています。そして、6行目にある最後の式は、当てはまるかどうかを示す(等価)関係を構築しているので、この最後の式と関数の結果は真か偽かになります。

関数の型

早合点しないでください。以前に、Scalaが静的型付け言語であると説明しましたし、そのために、関数のパラメタの型を宣言しなければなりませんでした。しかし、関数の結果となる値の型はどうするのでしょう?そして、ここで立ち止まらないでください。すでに言及した通り、関数リテラルから作り出されるものが、特殊な関数型のインスタンスであるなら、関数全体の型はどうなったのでしょう?いい着眼点です!


関数の結果となる値の型に関しては、それに答えるのに十分な知識をすでに得ています。これは単純に、関数本体の最後の式が評価された結果の値の型となります。それでは、関数全体の型がどうなるか、要約して少し考えてみましょう。関数には本質的な部品が2つがあります。それがパラメタのリストと関数本体であり、関数矢印(=>)で分けられています。さて、今見てきた通り、あらゆるパラメタには独自の型があり、関数の本体は本質的にはその結果によって表現されます。そして、その結果にも型があります。したがって、関数の性質は、関数に適用されるパラメタの性質(これが関数の型になります)と、関数の結果の性質(つまり、結果となる値の型)によって特徴づけられるのです。関数の型を表現する際にも、パラメタの型と結果の型を分ける必要があります。しかし、そうしたセパレータはすでにあるのではないでしょうか?なるほど、ここで、関数の型を表現できるに違いありません。


最初の例で言うと、1つめパラメタの値の型は Int で、2つめのパラメタの値の型も Int です。2つの Int の値を加算すると、Int の値がもう1つできますので、その結果も Int 型になります。これについては、関数全体の型を次のように表現できるのです・・・

( Int, Int ) => Int

一般的に、型とは、共通の特徴を共有する値の集まりと考えることができるのであり、上記の関数の型は Int 型の2つの引数を Int 型の結果にマップさせるようなあらゆる関数を表現しています。我々の書く関数は、そうした集まりに含まれる特定のメンバである一方、Int 型の引数を2つ取り、結果が Int であることによって関数の型を共有しているような、非常に多くの他の関数全体について考えることもできます(これは、同じInt型を共有している、非常に多くの整数値から、ある整数を選び出すことができるのとほぼ同じです)。


2つめの例についても、同じことが言えます。3つのパラメタは、すべて Int 型です。すでに見た通り、関数本体にある最後の式は評価されてbooleanの値になりますので、結果も Boolean 型になります。それでは、関数の型を推測してみてください・・・

( Int, Int, Int ) => Boolean

いいでしょう。これで、関数の型を推測し、表現できるようになりました。ここで、我々が関数定義を見ることによって型を推論できるようになったことはよいことですが、コンパイラについてはどうでしょう?コンパイラも同じことをします。つまり、与えられた関数の型を推論しようとするのです。コンパイラがわからなければ、時にはカツを入れて、関数の型を明示的に宣言しなければなりません・・・

ファーストクラスの値としての関数

全体として見ると、あなたがオブジェクト指向世界から来ているなら、関数はオブジェクトの中に置かれた普通のメソッド以外のなにものでもないように思われるでしょう。まずは、前回のエピソードを思い出してください。関数は与えられたパラメタにしか依存してはいけませんでした。それに対してメソッドは、オブジェクトのメンバである内部状態を参照することもできれば、さらに悪いことに、そのオブジェクトの内部状態を変更することもできます。こうした状態の変更は悪しき副作用と考えることができます(この側面については後のエピソードで立ち戻ります)。


さしあたって、メソッドと関数の違いをもう1つ別の角度からお見せしたいと思います。メソッドの型は何でしょう?うーむ。
メソッドが特定のオブジェクトのメンバにすぎないということはわかりました。オブジェクトそのものは、もちろん特定の型のインスタンスです。しかし、メソッド自体には型がありません。こう聞くと混乱してしまうようであれば(私自身は、最初にこれについて考えた時には混乱しました)、視点を変えてみましょう。すでに見てきた通り、あらゆる関数には型があります。そして、( IntBoolean )のような、他の型とと同じように、パラメタの型や他の関数内での結果の型として用いられた場合、何も特別なことはありません(最初のエピソードを思い出して下さい。そこでは畳み込みfold )の例を使って、関数を他の関数に引数として渡すという考え方に触れました。これについては、高階関数を調べる際に詳述します)。したがって、ある関数を他の関数に渡すことができる一方で、関数にメソッドを渡すことはできません。このことは、関数の結果となる値について考えると、より奇妙なことが起きます。つまり、他の関数を呼び出した結果として、ある関数を受け取ることは、ごく自然なのですが(へえ?自然?高階関数の説明を待って下さい)、関数の結果として、裸のメソッドが戻されると考えることはできないでしょう。メソッドは常に自分の属するオブジェクトを必要とするからです。


したがって、メソッドはそれ自身で独立した値と考えることができないのに対して、関数はそれができるのです。関数の市民権活動について何か聞いたことがあるかもしれません。この活動は関数が第一級の市民、もしくは第一級の値であると主張するものです。今ではその理由がわかるでしょう。関数は、今ある他の値や型と比べて、劣っていたり異なっていたりするものではないのです。そして、例えば Int 型の値に( val を使って)別名をつけ、その別名によってその値を参照できるように、関数に対しても同じことができるのです(なぜなら、関数もまた前述した関数の型を持つ値だからです)。

val add = ( x :Int, y :Int ) => x + y

こうすることで、関数に対して add という「別」名がつけられます。この add を参照するときにはいつでも、等号の右辺にある関数リテラルによって定義された関数を参照しているのです。


前に述べたことを思い出して下さい。コンパイラは常に関数の型を推論するわけではなく、関数の型を明示的に宣言することで手助けしてあげなければならないのでした。この型推論は、再帰を活用する際に、通常、不安定になることがわかります:

val power = ( base: Int, exp :Int ) => if( exp <= 1 ) base else base * power( base, exp - 1 ) 

この関数をコンパイルしようとすると、コンパイラに関数の型を推論できないと叱られます。

error: recursive value power needs type

なるほど、コンパイラはこの関数 power を値として認識しました。当然、それでいいのです!しかし、コンパイラは私たちのちょっとしたなぞかけを解決するのに、何らかの助けを求めています。求めているものを与えてあげましょう・・・

val power : ( Int, Int ) => Int  = ( base: Int, exp :Int ) => if( exp <= 1 ) base else base * power( base, exp - 1 ) 

これが、関数についての完全な宣言です*1。すべてが俎上に上がりました。後でその関数を参照するための別名は、として導入されました。その値に対しては明示的に型を宣言したことで、コンパイラはおとなしくなりました。そして、パラメタのリストと関数本体からなる関数の値それ自体があります。これで完了です。Graham Hutton博士(※2)であれば、喜んで次のようにまとめるでしょう。

関数は、その関数に名前を与える等式、すなわち引数と、その引数によってどう結果が算出されるかを定める本体関数に名前を与える等式を用いて定義されます。

ところで、if ステートメントを見て下さい?おっと。私は、ステートメントについては、命令型の世界に由来すると説明しようしていないことはおわかりでしょう。実際、ここでは事実上 if 式になっています(これは、その関数本体での最後の式ですので、結果となる値を評価する式として扱われます)。そこで、何が違うんだと、あなたは尋ねるかもしれません。そうですね、式が評価されると常に、ある型の値となります。したがって、ifと呼ぶ以上は、あらゆる状況に置かれている値を評価しなければなりません。これはつまり、Scalaif を式として活用しようと思えば、else を省略することができないということを意味します(ただし、オブジェクト−関数型言語として、Scalaは両方の世界で使えなければならないので、Scalaif を式として使うのでなければ、else を省略できます)!

関数の適用

さて、最初の関数を定義したので、今度は使ってみましょう!この関数を、適切な型の具体的な引数の値に当てはめてみるのです!
Scalaにおいて関数を呼び出すのは、数学的な記法で定義された関数を適用するのと、実によく似ています。つまり、引数を丸カッコで括り、関数名の後に続けることで表示するのです。


なんとラッキーなのでしょう!関数にどうやって名前をつけるかをちょうど見てきたので(それには、関数リテラルを特定の関数型のとして宣言し、その関数を名前と関連づけます)、その名前を参照することで関数を呼び出せるようになりました:

val byteStates = power( 2, 8 )

確かに、実はあまり面白いことが起きていません。ここでは、関数 power の基数( base )に 2 を、指数( exponent )に 8 を適用し、10進数での1バイトを算出しようとしています。関数の適用が表現しているのは、もう1つの式でしかないことに注意して下さい。これは、評価の結果、ある型(その関数の結果となる値の型)のある値(その関数の結果となる値)となる式です。そして、もちろん、後々に参照するため、もう1つの別名とその値を紐づけることもできます(この場合には、byteStates)。そして、もうわかっている通り、この値の型を明示的に宣言する必要はありません。Scalaコンパイラが推論できるからです。つまり、この場合、値 byteStates は、単純に関数の結果の型となります(関数の型はすでにコンパイラに伝えられているからです)。


すべての引数は丸カッコに入っているので、結びつきに関して不確実なことはありません。関数の適用は、自然と、最も高い優先順位を付けられるのが常です。どの式が表現され、引数として使われるかは常に明確です。引数としての式?わかりました!この特徴はあまりにも自明であるように見えるので、次のことは通常触れる価値がありません。もちろん、関数は普通の値に適用されるだけでなく、より複雑な式にも適用されるのです。式が評価されると、最終的には特定の型の値になるので、コンパイラはその種の組み合わせを解決することもできます:

val isPythTriple_3_4_5 = add( power( 3, 2 ), power( 4, 2 ) ) == power( 5, 2 )

これはもう、頭痛のタネにはなりません。ここにあるのは、容易に分解できる、正しい式なのです。つまり、これは、2つのサブ式の間にある単なる等号にすぎません。右辺で行われているのは、「実にこの上なく複雑な*2」関数の適用です。( Int 型の2つの値を求める)関数 add の引数が、さらに2つのサブ式から現れるのであり、さらにこのサブ式は power(この評価結果はどちらも Int 型の値になる)に対するさらなる関数呼び出しであるとわかります。全体として、まだ説明されていない点はありません。

まとめ

なんという一日でしょう。これまで、Scalaが関数の型を文字通りにサポートしているのを見てきました。そして素晴しいことに、これについて、わからないことは何もありません。あとは、慣れる必要があるだけです。関数は、よく知られた一般的な型の他の値以上でもそれ以下でもありません。そして、他の型( IntBoolean あるいは List[String] のことを考えてください)を用いてできることは、関数を用いても行うことができます。これはすなわち、別名と関連づけて、受け渡し、関数呼び出しから戻されることなど、その他諸々全てです・・・


もちろん、話さなければならない欠点もいくつかあります。Scalaがオブジェクトー関数型言語であるため、いくらか妥協をしなければなりません。本当の変数を関数と共用したらどうなるでしょう?可変のオブジェクトを関数の引数として適用することはできるでしょうか?そうだとすると、関数の中でその値を変更し、ある種の副作用を引き起こしてもよいのでしょうか?ある関数が、すでに特定の関数型のインスタンスであるとすると、ポリモルフィックな関数とポリモルフィックなメソッドはどうなるのでしょうか(これについては、別のエピソードで扱います)?


ここでは表面をさらっただけです。ここで得ることができたのは、関数型の世界に一層踏み込んでいく上での優れた基礎です。最初に言った通り、ここではScalaにおける関数型プログラミングの必需品を習得しました。しかし、全部を語るとは言わないまでも、まだいくつか付随するものもあります。関数を定義するための、別の方法についても見ていきます。たとえば、別の関数や、特別な高階関数(もう言いましたっけ?)を活用する方法、あるいは既存の関数から別の関数を引き出すという新しい概念を思いつくなどといったことが挙げられます。しかし、こうしたものは、「関数型Scala」における、別のエピソードで扱われる題材です・・・




※1 Erik Meijer博士のことを単純に、関数型純粋主義者と呼ぶ人もいます。私にとっては、関数型プログラミングに私の興味を向けてくれた、本当に素晴しい人です。

※2 Graham Hutton博士は関数型プログラミングにおいて燦然と輝くもう1人の人物です。彼はProgramming in Haskellと呼ばれる素晴しい本を書きました。(しかし、注意して下さい。この本はHaskellの紹介を装っていますが、実は、世界支配とはいかないまでも、関数型プログラミングを拡大すべく努力しているのです)。

*1:訳註:無論、通常であれば関数の宣言は def を用いて行う。ここでは関数がであることを説明するためにこのように書いているが、プロダクションコードもこのように定義すべきであると推奨しているわけではないと考えられる。kmizushima様より指摘を頂きました。要約すれば、「def で定義されるのはあくまでメソッド。Scalaではメソッドが関数として使われることもあるが、ファーストクラスとしての関数は関数リテラルで記述するのが正しい」ということです。不用意でした。詳しくは、コメント欄を参照してください。(2011/03/22)ただ、def で定義したメソッドも関数の型で宣言された引数に渡すことはできます。この辺りが概念的に少し紛らわしいところです。(2011/03/23)

*2:訳註:大げさに表現しているが、もちろん冗談

関数型Scala(1):導入 - Mario Gleichmann

この記事はMario Gleichmann氏のブログエントリ「Functional Scala: Introduction | brain driven development」を、氏の許可を得て翻訳したものです。(原文公開日:2010年10月28日)


また、当記事の翻訳許可を求めてメールした際、その返信として氏から暖かいメッセージを頂きました。ここに紹介させて頂きます。Danke schön, Mario!!

first of all let me say that all our prayers and thoughts are with you and all japanese people! For all of us here, the tragedy's so unbelievable - we just feel for all of your sorrow and pain and hope that our little donation will bring at least a bittie help!


まず、私たちの祈りと思いがあなたがた日本人の皆さん全員と共にあるということをお伝えさせて下さい。ここドイツに住む私たちにとって、今回の悲劇は実に信じられないものです。私たちはあなたがたの悲しみと痛みを感じ、私たちのわずかな寄付が少しでも助けになることを祈ります。

なお、この翻訳について「電子書籍だったら買ってもいい」と思って頂けるようであれば、その範囲の金額を、震災の義援金としてどこかお好きな団体に寄付して頂ければ幸いです。




「関数型Scala」シリーズの第1話にようこそ。Scalaの位置づけは、いわゆるオブジェクトー関数型言語ですが、Scalaに関する議論と記事のほとんどは、これまでオブジェクト指向的な特徴に中心が置かれていました。これを読んでいるならば、あなたは、Scalaのより関数型の側面について学びたいと思っているのではないでしょうか。そうだとすると、あなたは正しい場所に来ました。


これから始まるエピソードについての構想は、私が「関数型Scala」について行ったいくつかのプレゼンテーションに基づいています。私がこのテーマについて書き、また話すことを決めたのは、一般的な関数型プログラミングと、特にScalaに関する知識を確固たるものにしたかったからであり、また、Scalaを学びはじめた方に、私の考える関数型としての特徴を学ぶ上での手助けができると考えたからでもあります。


特に、Scala関数型言語としての特徴を正しく備えているか、ということについては、重要な議論が過去にいくつかありました。これについて判断するには、まず、Haskellのような広く認められている関数型言語でよくある、中核的な考え方について明確にしなければなりません。そうした考え方が、Scalaでも提供されているかどうか、提供されていたらどんなかたちであるかを見ていき、それを限界まで押し進めたいと思います。では、前置きはこのくらいにして、Scalaにおける関数型プログラミング(FP:Functional Programming)の世界に入っていきましょう。

関数型プログラミング」対「命令型プログラミング」

だれかが関数型プログラミングのメリットについて話しているのを、これまでに聞いたことがあるならば、関数型プログラミングの素晴しい約束が、本当にこのアプローチに従えば達成できるのかということについて、あなたは疑問に思ったに違いありません。ここで約束されるのは、(我々全員の探し求めている)単純さ、明確さとエレガントさといったものなのです。Liftと呼ばれているWebフレームワークの作成者であるDavid Pollakの言葉を少し引用します。どうやら彼は、最初は命令型プログラミングのパラダイムに学びましたが、関数型の世界に入った後は、関数型プログラマとしての自信を持つに至ったようです。

しかし、最も重要なのは、Scalaが私に、異なったやり方でプログラミングし、プログラムについて考えることを教えてくれたということです。バッファの割り当て、構造体やオブジェクト、さらにそうしたメモリの変更という観点から考えるのを止めました。その代わりに私は学んだのは、プログラミングのほとんどを入力を出力に変換することだと考えることです。このように考え方を変えることで、欠陥率は低くなり、コードはよりモジュール化し、テストしやすくなりました。


どうしてこれが可能なのでしょうか?我々は、オブジェクト指向(命令型言語の典型例としてのオブジェクト指向言語を用いるもの)を用いさえすれば、モジュール化され、保守しやすいコードが書けると教わったのではないでしょうか?関数型プログラミングを命令型プログラミングと区別する決定的な特徴は何でしょうか?


おそらく、こうした本質的な特徴を見分けるのに、一連の整数を合計するといったシンプルなタスクを解決する実装を比較することができるでしょう。1から10でやってみましょう。 まずは命令型スタイルで行い、次に関数型スタイルを用います。Scalaはオブジェクトー関数型言語なので、両方の解決策を示すのに使うことができます。

命令型スタイル
var sum = 0
var i = 1
while( i <= 10 ) {
    sum = sum + i
    i = i + 1
}

このコードを詳しく見ても、特筆すべきものは何もないでしょう。その理由の大半は、私が、あなたの注意を特定の題材に向けていなかったことにあります。それに加えて、あなたが命令型言語を用いてコードを書くのに慣れているならば、上記のコードに何も変わった点はありません。変数が2つあり、それぞれループを実行している間に、新しい値が割り当てられます。ループを抜けた後は、変数 sum が最終結果を保持することになります。普通ですよね?
変数に対して(中間的な)値を適切に割り当てることは、ステートメントを適切に並べることで保証されます。このステートメントは、値を計算し、値の割り当てを正しい順序で行うよう指示するようなものです。上記のコードで、4行目と5行目を入れ替えたら、望む結果にはならないでしょう。


本質的には、このシンプルな例を抽象化し、次のように述べることができます:

命令型プログラミングとは、プログラムの状態を変えるステートメントを正しく並べることで処理を記述するプログラミングパラダイムです。


変数の状態を変えられるという事実から、もう少し興味深い結論が導き出されます:2つの変数をリセットせずに、複数回ループを呼び出したらどうなるか想像して下さい。もしそうだとすると、変数 sum が値1から10までの合計だけを保持していると期待することができなくなります(sum の値はループが何度呼び出されたかに依存します。そうですよね?)。このコードの断片の持つ意図を理解するためには、一連のステートメントと変数への割り当てをすべて追いかけなければなりません。ある一行の意味を理解しようと思えば、その周りにあるステートメントの意味も理解しなければなりません。これは、変数への再割り当てのフローを追う必要があるからです。そして、変数の割り当てのフローを適切に追いかけるためには、ステートメントが実行される順序に依存する必要があります。


つまり、命令型プログラミングについて考える際には、心に留めておかなければならない主要な特徴が2つあります。命令型プログラミング(Imperative Programming)は、命令(instruction)に基づくスタイルだということを意味しており、これは処理がどのように行われるべきかということを正確に定義する単一の命令が連続することで表現されます。この命令に基づくスタイルでは、主に変数の(再)割り当て(と、そこからのあらゆる帰結)という処理手法によって指示が出されます。

関数型スタイル

関数型スタイルで書かれる解決策を見てみましょう:

val sum = fold(  1 to 10,  _ + _ )

なんと、一行だけです!しかし、ズルをしていませんか?ループを抽象化して、fold と呼ばれるメソッド(か何か)に入れただけではないのでしょうか?ここで、fold について深く見ていくこともできますが、しかし、今のところは、変数の(再)割り当てに基づくループは fold の内部にはないのだと、私を信じて下さい(後の章で fold については深く見ていくことにします!)。それでも、このコードの中に変数の(再)割り当てがないことを示す、間接的な「証拠」はあります:

val vs. var

まず第一に、変数 sum の宣言を見て下さい。ここでは、キーワード val を用いて宣言されています。val によって、sum は再割り当てのできない不変な値として定義されます。別の値を sum に割り当てようとすると、コンパイラに怒られることになるでしょう(だからこそ、我々は命令型の例で sum と i を定義するのにキーワード var を使わなければならなかったのです)。関数型の例では、sum のことをいつでも式の右辺として使用できるエイリアス(もしくは、その式を評価した値)と考えても構いません。


Scalaがオブジェクトと関数型のハイブリッドであるので、どちらの世界でも使えなければならないのです。しかし、もし関数型の側を旅行したいのであれば、値しか使ってはいけません。(階段本*1にあるように、関数型プログラマは val だけを使うべきで、var の使用は冒涜と考えられます)。

関数

わかったこととしては、fold は関数であり、2つのパラメタを取るということです:それが畳み込む(fold)値の集合と、その値をどう畳み込むかを伝える別の関数(ですよね?)です。これはつまり、値を畳み込み、合計の代わりにその結果を受け取りたいと思ったら、掛け算を行う別の関数を投げ込めばいいということでしょうか?そう、その通りです!この単純な例により、関数型プログラミングにおける中核的な特徴が示されています(ただし、関数型プログラミングの唯一の特徴ではありません):これは、関数を引数に適用することで、望む値(例えば、整数の合計)を受け取ろうというものです。関数とその特徴について深く掘り下げるまでは、関数のことを数学的な意味でとらえておくことができます:

関数とは、1つ以上の引数をとり、1つの結果を生成するマッピングなのです。(このマッピングは、引数からどのように結果が計算されるかを定める関数の定義によって行われます。)

ここにあるのは、関数のためのきわめて短い定義です。しかし、この比較的単純な定義により、実に強力な処理方式が得られます:あなたは、任意の結果を得るのに、適切な引数として関数を適用するだけでいいのです。これらの引数が他の関数である場合もあるので、新しくて強力な抽象概念を導き出すことができます。(これは、いわゆる高階関数(Higher Order Functions)を経験するときに見ていきましょう)。今や、処理に関する中核的な考え方は、一連の命令(その処理がどのように行われるかを正確に定義するもの)から、関数と値に基づく式の記法へ移りました。もはや、処理を理解するために、命令の計画を追いかける必要はありませんただ、こうした式を単一の要素に分解すればよいのです。


上記の定義では、きわめて重要な単語を1つ見落としているかもしれません:関数の結果は、その引数だけに依存するのです。グローバル変数がなければ、副作用もありません。これは、数学的な感覚で関数を見るときにも、あなたが予想もすると思われるものです。単純な関数定義を見てみましょう:

val add = ( x: Int, y: Int ) => x + y 


この関数定義では、所与の(正式な)パラメタ x と y に対して、どのように結果を算出するかを記述しています。これら2つのパラメタのことを、変数と呼ぶこともできます。しかし、数学においては、これらの変数は常に固定された値であり、何らかその時の値(つまり、関数適用時の実際のパラメタ)に適用されて関数に渡されるのです。同じことは、関数型言語において定義された関数にも言えます ー もう少しわかりやすく言うと次のようになります:関数が適用される値は、単なる値です。それ以上でもなければ、それ以下でもありません。しかし、値であるがゆえに不変なのです(命令型言語においては、変数はメモリ内の特定の領域を参照し、そこに値が入っています)。

ステートレス vs. ステートフル

ここまで、変数という概念が有効なのは、命令型言語の範囲内でのみだということを見てきました。命令型言語では、変数の値は複数回変更されるかもしれません。(数学のような)関数型の世界にあるのは、関数と値だけです。変数名は、変えることのできない特定の式に対する別名以外のなにものでもありません。その式を参照する際にいつでも使用できる別名にすぎないのです。お望みであれば、こうも言えます。あるのは、定数と値とパラメタだけなのです。


それゆえに、関数型言語には割り当て操作という考え方もありません。そして、値の(再)割り当てのようなものがないため、純粋な関数型プログラムには状態というものが存在できません。なんと、状態がない?しかし、状態はなくてはならないのでは?ステートフルセッションBeansがあれば、ステートパターンもあります。状態という考え方がないとすると、どうすれば、カウンタのようなものを実装することができるのでしょうか?後でわかることですが、関数な世界には、状態がない(それゆえ副作用もない)ことを扱うために他のメカニズムが存在します。さらに、関数型スタイルにおいてカウントを処理することも(これから見ていくように)できるのです。


我々がそれらの重大な違いを理解し、命令的な世界と機能的な世界についての若干の重要な考えに名をつけた今、我々は関数型プログラミングについて最終的に特徴を描写する準備が整いました:

関数型プログラミングというプログラミングパラダイムでは、処理を数学的関数の評価として記述し、状態や共通のデータ、それゆえ副作用も避けるのです。

恐れることはありません

恐れることはありません。こうした考え方については、より詳細に再度取り上げますし、Scalaにおいてどう現れているかについても見ていきます。この最初のエピソードでは、一般的な背景を紹介し、関数型プログラミングという考え方にあなたの気持ちを向けようとしたのです。これから、Scalaを使うことでどこまで行けるのかを見ていきます。


あなたが、Scala関数型プログラミングに不慣れであっても、恐れることはまったくありません。次のエピソードは、ゼロから始めます。その際、あなたの武器のバリエーションを広げるような、新しい考え方と問題解決のための概念を提示するよう試みます。


「関数型Scala」を最終的に理解するまでに、私はおそらく2回は習得に失敗しています。私にとっては、あまりに奇妙に見えて、理解できなかったからです。しかし、一度コツをつかんで、最初のハードルを乗り越えれば、実にスムーズに事は進みました。言いたいのは:Scala関数型プログラミングをする上でもきわめて優れており、あなたがプログラミングに興味を持っているならば、たとえそれが最初は奇妙に見えても、実際に学ばなければならないということです。「関数型Scala」を学ぶことは、はじめてプログラムを学ぶことに似ています。これは楽しいですよ!


そのためには、考え方を変えなければなりません。私に、ついてきて下さい。




プログラミングScala

プログラミングScala

Scalaスケーラブルプログラミング[コンセプト&コーディング] (Programming in Scala)

Scalaスケーラブルプログラミング[コンセプト&コーディング] (Programming in Scala)

*1:訳註:表紙に階段の写真が載せられた、『Programming in Scala: A Comprehensive Step-by-step Guide』のこと。我々で言う『コップ本』。

書評:プログラミングScala

訳者の方々とオライリー・ジャパン様より献本頂きました。厚く御礼申し上げます。ありがとうございます!


プログラミングScala

プログラミングScala


はじめに

Dean Wampler and Alex Payne著『Programming Scala』の、「株式会社オージス総研 オブジェクトの広場編集部」様による翻訳です。訳質に関してはさすがと言いましょうか、まったくストレスを感じません。内容も初頭文法の説明からアクター、DSLなどの応用編まで多岐にわたっていて、じゅんいち☆かとうさん*1や、ゆろよろさん*2もおっしゃっている通り、間違いなく良書でしょう。


コップ本*3との違いについては、「訳者まえがき」で岡本さんが説明している部分を引用します。

本書でわからないことがあったり、もう少しScalaについて深く知りたいと思ったら、『Scalaスケーラブルプログラミング』(インプレスジャパン)を参照するとよいでしょう。本書がアプリケーション開発者の視点によるScalaの解説書であるのに対して、『Scalaスケーラブルプログラミング』は言語設計者による言語解説であるため、相互に補う内容になっています。(p.xii)

この言葉がまさに的確に違いを表しています。私の感覚としては、読み物として最初から読むのであれば、『プログラミングScala』の方が適しているかと思います。(出版の順序とは逆ですが)『プログラミングScala』を読んで、Scalaの考え方を一通り学び、実際にアプリケーションを作る上で、詳細な知識が必要になったらコップ本を読む、というのがよいのではないでしょうか。


一点補足すると、「Scalaへのエントリポイント」ではあっても、「プログラミング言語へのエントリポイント」ではありません。「はじめて学ぶ言語がScalaです」という方はさすがにまだいないとは思いますが*4オブジェクト指向言語に関するなんらかの知識は前提として持っていないと、さすがに読むのはきびしいかもしれません。

アプリケーション設計のために

さて、内容に移っていきましょう。Scalaでどう書いたらいいかという観点からとらえると、Scala文法の網羅的な説明(1章〜4章)から、Scalaにおけるオブジェクト指向の考え方(5章〜7章)、再帰や畳み込みなどの関数型プログラミングの実践方法(8章)までを(あまりに難解なところは適宜飛ばしつつ)一通り読めば、普通にScalaを書く上で必要な知識はおおよそ身につきます。Scalaを軽量Javaと考えるなら、ここまでで十分でしょう。しかし、それだけではやはりもったいないですし、この本が真に優れている点も、そのさらに先にあると私は思っています。


少し複雑なプログラムを書こうとすると、必ず考えなければいけなくなるものに、モジュールの分割があります。ある言語を「習得した」と言える基準の一つに、初頭文法を超えてこうしたモジュール設計ができるようになることが上げられるのではないでしょうか。この本を買って読もうと思う人であれば、モジュール設計について何らか理解している言語を1つは持っているのではないかと思います。こうした言わば母国語は、私にとってはJavaです。


Javaを基準に考えると、Scalaではこうしたモジュール設計に関して、かなりのパラダイムシフトが起きています。これは単に「関数がファーストクラスなので効率的にアルゴリズムが実装できる」といったことに留まりません。いくつか例を挙げましょう:

  • トレイトを利用した動的なミキシン*5
  • アクターを利用したメッセージ駆動のモジュール設計(9章)
  • 暗黙の型変換を活用した内部DSLの構築(11章)
  • パーサコンビネータを活用した外部DSLの構築(11章)
  • 可視性の柔軟なコントロールによる「コンポーネント」の実現(13章)
  • 暗黙の型変換とパターンマッチを組み合わせによるVisitorパターンの置き換え(13章)


こうした考え方の基礎が、具体的なサンプルコードと共に紹介されています。雰囲気に触れるだけであれば、コードを眺めつつ本文をさらっと読むだけでもいいかもしれませんが、できることならば、実際に写経しながら取り組んだ方がよいでしょう。さらにつけ加えると、自分が理解している限りでよいので、登場するクラス(あるいはコンポーネント)の図を描きながら理解を深めていくべきだと思われます。これは厳格なUMLである必要はありません。特に暗黙の型変換が入った場合は、どういう処理を経て最終的な出力に至るかを追うのが非常に難しくなりますので、それを自分なりに整理した方がいい、ということですね。


日本語版に独自に追加された、Scala 2.8における変更点とSimple Build Toolを使ったScalaの開発に関する解説も、実際に手を動かす上で大いに参考になるでしょう。

最後に

この本は「扉を開けてくれる」本です。逆に言えば、「この本を一冊読めばScalaの全てがわかる」というものでもありません。正確には、この本の内容を全て理解するには、恐らくこの本を読むだけでは足りない、というべきかもしれません。この本は、実際にコードを書きつつ、その際に発生する問題に苦しんだときに立ち返ることのできるものです。文法を覚え、設計思想(あるいはテクニック)を学んだら、次にやるべきことは、自分で実際に何か一つ作ってみることでしょうね。読んでいるだけの時とは違った学びがあるはずです。




プログラミングScala

プログラミングScala



なお、Macをお使いの方へ。Scala2.8から、インストール時にバージョン番号を要求されるようです。

sudo port install scala28

*1:[書評] プログラミングScala - じゅんいち☆かとうの技術日誌

*2:書評:プログラミングScala - The Python Paradox is now the Scala Paradox - ( ꒪⌓꒪) ゆるよろ日記

*3:

Scalaスケーラブルプログラミング[コンセプト&コーディング] (Programming in Scala)

Scalaスケーラブルプログラミング[コンセプト&コーディング] (Programming in Scala)

*4:あと何年かすれば、そういう方も出てくるんでしょうね

*5:例えば、このブログで時々ご紹介しているDCIアーキテクチャは、人間のメンタルモデルを表現するために「ロール」の概念を導入しています。トレイトはこのロールの実装手段として優れた効果を発揮します。(ScalaによるDCIアーキテクチャ:ローンシンジケート再考 - Digital Romanticism