翻訳に必要な3つの技術

翻訳に必要と思われる技術について整理する。

導入

私のブログの書き方を紹介したエントリの中でもすこし触れたとおり、翻訳という作業は自分の中で大きな位置を占めるようになってきています。『エリック・エヴァンスのドメイン駆動設計』が出版されて以来、新しい翻訳をやったり、他の方の翻訳をレビューに参加させて頂いたりと、翻訳がらみの仕事が増えてきましたので、この機会に自分が考えていることを整理したいと思います。


なお、この場を借りて大先輩の翻訳論をご紹介しておきます。


私の考える、翻訳に必要な3つの技術とは「英文解釈」「翻訳のテクニック」「日本語作成技術」です。結局のところ翻訳とは、「英文を正確に理解し」「日本語に置き換え」「日本語として自然に読めるかたちにする」作業であり、それぞれに一定の知識なりテクニックが必要になるということですね。


では、それぞれ順を追って説明していきます。

英文解釈

翻訳をする上で必須なのが、英文の構造を正確にとらえることです。単語は調べればわかりますが、構造は調べることができません。英文の構造をどこまで忠実に読み取れるかが、翻訳の精度を決める最初の鍵になります。「単語だけ調べて、あとは文脈からなんとなく」という読み方は、読むだけであれば大丈夫かもしれませんが、翻訳をしようと思うと苦しくなります。読むだけならパラグラフ単位、章単位で意味が理解できていればいいケースも多いですが、翻訳となると、すべての文章を日本語にしないといけないですからね。


そのために必要なのがやはり「英文法」の知識です。「とりあえず基礎からやり直したい!」という強い思いがある方におすすめなのがこの一冊(完全に受験参考書ですが)。

NEW・山口英文法講義の実況中継 (上) 改訂新版

NEW・山口英文法講義の実況中継 (上) 改訂新版

※受験時代、私はこの本のおかげで「単語をつなげて雰囲気で理解する」ことから卒業できた気がします。

前置詞の解釈

英文の意味を正確にとらえよう(訳そう、ではない)とした場合に、かなり重要になるのが前置詞に関する理解です。受験英語ですと、inなら「中に」、onなら「上に」という具合に訳し方を覚えさせられた上で、これに収まり切らないものはすべて「慣用表現」として処理させられたりしますが、これは結構大変です。これに対して、前置詞を概念として理解しておくと意外と応用が利きます。


いくつか例を挙げましょう。特定の領域を表す"in / on / at"であれば、inは「包含」、onは「接触」、atは「焦点」です。onについては、「上」でなくてよいところがポイント。壁にかかっていれば"on the wall"ですし、時間ぴったりであれば"on time"、逆に範囲に収まっている語感を持っているのが"in time"です。一方、atは特定のポイントを指し示している感じがあって、だからたとえば"at 7 o'clock"のように特定の時間を表すのにも使われます。


あるいは方向を表す"to / for"であれば、toは「到達」、forは「途中」になります。「どこかに出かける」なら"leave for"ですし、着いているか着くことが確実であれば"go to"になります。探し物をするなら見つかっていないので"look for"、何か特定のものを見ているなら焦点の"look at"ですね。

訳文に逃げない

英文解釈という観点からすると、私は「構造を理解すること」にゴールを置くようにしています。日本語にして終わりではなく、どれが主語でどれが動詞で、に始まり、形容詞のかかっている先はどれか、指示語の対象は何か、それがわかって初めて解釈としては完了だということですね。翻訳作業を行っていて意味がよくわからない文章に出会ったとき、確実にやっているのが「このitって何だ?」「このtheって何だ?」という地道な問いかけです。最初はわからなくても、可能性をひとつずつ丁寧に追っていけば、たいていの場合正解と思えるものにたどり着けています。


それでもわからないときにはじめて「慣用表現の可能性」を考えます。よく使うのがアルクGoogle検索ですね。実際に慣用表現だった場合には、かなりの高確率でアルクで見つかります。Googleの場合は似たような文章をいくつか眺めて使われ方を理解したりします。この場合も「訳す」というよりは意味を理解することを重視します。

翻訳のテクニック

英文として理解できるようになったら、次に考えるべきは日本語にどう置き換えるかです。修飾関係や文の中の役割といった文法構造だけを日本語に置き換えて済むなら簡単なのですが、それでは「横のものを立てただけ」になってしまいます。


たとえば:

Her anger made him sad.

直訳すれば「彼女の怒りが彼を悲しませた」ですが、日本語として自然なのは「彼女が怒ったので、彼は悲しくなった」ですよね。これは英語が名詞中心で発想しているのに対して、日本語が動詞中心で発想していることに由来します。同様に形容詞も副詞に変換して訳した方が自然な日本語になるケースが多いです。


ほかにも英語という言語の持つ特性上、そのままでは日本語になりにくいものを処理するためのテクニックがあります。このパターンで、普段自分が自覚的に使っているものをご紹介します。

1接続詞/指示語の補完英文はそのまま訳すと、前後のつながりが見えにくくなることがある。必要に応じて接続詞、指示語を補う。
2強調/対比の明確化元の文において強調されている概念、対比されている概念は日本語化に際しても明確に訳出する。
3名詞の動詞化英語が名詞を中心に発想するのに対して、日本語は動詞を中心に発想する。その意味で「名詞の動詞化」は「名詞を動詞化する」とすべきだが、パターン名に関してはこの限りではない。これに伴い「形容詞の副詞化」も行うことが多い。
4語順の維持原文で情報が提示されている順序を崩さないようにする。代表的なものとしては、関係代名詞を後ろからかけるようにする。このとき、必要に応じて1つの文が2文に分かれることがある。ただし、意味上あるいは日本語の流れ上前からかけるべきケースも少なくない。やりすぎないこと。
5無生物主語の転換無生物主語は日本語としては不自然。態を転換して、意味上の主語を主語として扱う。
6「の」の展開文の中で「の」が使われている場合には、意味を明らかにして展開した方がよいケースが多い。特に連続する場合には必須。
7「が」と「は」の意識的な使い分け単なる主語ならば「が」。「は」は強調を表す。一文に「は」が連続すると焦点がぼやけるので注意。
8指示語への代入*1「これ・それ・あれ」が増えるとうるさいので、適宜内容は補う。「the」も既出の内容を受けている場合には訳出すべきケースが多い。

情報提示の順序

翻訳の際、「意味を訳すこと」と同じくらい意識しているのが、「情報提示の順序」です。これは単純な修飾/被修飾構造とは別の次元で文章を支配しています。この順序が意図されたものと違ってしまうと、おかしなことになってしまいます。一例を挙げましょう。

  • 王様がいつも赤いずきんを被っていたのは、王様の耳がロバの耳だったからだ。
  • 耳がロバの耳だったので、王様はいつも赤いずきんを被っていたのだ。

何か違うストーリーが混ざっているような気もしますが、言わんとしていることは伝わるでしょう。たぶん「赤ずきんの王様」みたいなタイトルのおとぎ話です。この文章を読む読者にとって、王様が赤ずきんを被っていることは「既知」の情報、その理由が「未知」の情報になります。この場合、「既知」→「未知」の順番に情報が流れてくれないと、なんとものっぺりとした文章になってしまうのです。


私自身のスタイルとして基本的には情報提示の順序を崩さないようにしているため、通常はそれほど強く意識していません。ただ、語順をひっくり返さざるを得ないときには、既知/未知の関係や文の中での強調点が崩れないように、ちょっと特別な注意を払うようにしています。逆に言えば、この点に注意していれば語順を変えた方がよいケースもすくなくありません。


日本語作成技術

「英文を理解する」、「日本語に移し替える」、その次に来るのは「日本語としての完成度を高める」です。そのテクニックを学ぶ上での必読書がkdmsnrさんのエントリにも紹介されていたこちら:

新装版 日本語の作文技術

新装版 日本語の作文技術

特に以下の3つの章は大いに参考になります(私がこの本を読んだのはDDDを翻訳した後のことでしたが)。

  • 修飾の順序
  • 句読点のうちかた
  • 助詞の使い方(特に「は」と「が」)

翻訳に限らず、日本語である程度の長さの文章を書こうと思うなら、この本は必ず参考になります。

日本語での表現力

「辞書で一番上に出てきた単語を使う」という段階を超えようと思うと、どうしても日本語表現の幅が必要になってきます。たとえば「英語では一語のものが日本語だと何語かになる」「英語だと数語のものが日本語なら一語で表現できる」ということが起こりますし、「同じ意味を表す自然な言い回し」が見つけられればそれに越したことはないでしょう。このあたりを流暢にコントロールできるようになると、翻訳も職人技の域に入っていくのでしょうが、私はまだそこまでは到達できていません。ただ、悩んだときにはシソーラスが結構役に立ったりします。


どうすればこの部分を高めることができるかは模索中なのですが、最近、ほかの方が翻訳された文の査読を行うことで、スキルをかなり盗めることを知りました(ありがとうございました)。

最後に

大切なことを最後に1つ。翻訳は独自のロジックを持つ技術です。「英語の本が読めること」と「翻訳ができること」は等価ではありません。「英語の本が読めるから翻訳ができる」わけではありませんし、「翻訳ができないから英語の本が読めない」わけでもありません。「英語の本が読めなければ翻訳はできない」正しいのはこれくらいでしょう。冒頭で触れたとおり、文章レベルで正確に読めていなくても、パラグラフのレベルや章のレベルで論理が追えていれば問題ないケースも多くあります。また、理解が目的であれば、きれいな日本語に置き換える必要もありません。


もちろん、「だから翻訳には手を出すな」と言っているわけではありません。むしろ逆で、英文を読める方が「じゃあ翻訳もしようかな」と思ったときに、とりあえず理解しておくとよさそうなことをここでまとめたつもりです。確かに、本当に自然な日本語にしようと思えば、幅広い語彙と豊かな表現力が要求されるのであり、こうしたことはすぐに身につくものではありません。ただ、日本語としての練度をどこまで上げる必要があるかは、翻訳対象にもよります。技術系の文書であれば、文芸作品ほどの練度は必要ありません。ここにご紹介した本を読み、いくつかのテクニックを理解すれば、間違いなく訳文の質を上げることができます(また、翻訳作業にかかる時間も減るでしょう)。


日本語で読める技術情報を充実させていきたいと願う方々にとって、このエントリが役に立てば幸いです。



おまけ

構文が取れるようになったというところからもう一歩先に進み、英文の「意味」を解釈するとはどういうことかを考える上では、この本が圧倒的におすすめです。


議論されるものに、たとえば「次の2つの文がどういう意味の違いを持っているか」といったことがあります(p.96):

種明かしをすると、後者はキスの対象はあくまで「手」であって単なる儀礼だが、前者はキスの対象が「女王」であってなんらかの含みを思わせるそうです。受験英語で「ジムの頭を殴った」を「"hit Jim's head"ではなく、"hit Jim on his head"としろ」と言われ、慣用表現として覚えた方もいらっしゃるかもしれませんが、これと同じ理屈ですね。殴っている対象はジムであり、頭は場所でしかないということです。この本の中ではほかにも、エントリ中で触れた情報提示の順序についても言及されています。


1点補足しますと、この本、文庫本ではありますが、内容はかなりマジメな認知言語学です。翻訳のためのパターンを探すという目的にはちょっとそぐわないかもしれません。ただ、英文を深く読めるようになりたいと真剣に考えている方には間違いなくおすすめできます。




MAC-Transer 2010 プロフェッショナル

MAC-Transer 2010 プロフェッショナル

最近愛用している翻訳ソフトです。自動翻訳はもちろん使いものになりませんので、テキストエディタとしてしか使っていないという話もありますが、エディタとしての基本性能の高さと辞書機能の使いやすさから愛用しています。ただし、購入を検討されている方には1点注意事項です。

いきなり買わない。まずはテキストエディタを、次にOmega-Tを試すこと。

理由は、「イラストレーターを買う前に、紙にペンで絵を描くべき」なのと一緒です。なお、ご紹介したのはMac用で、Windows用にはPC-Transerという商品があるようです(Mac-Transerより高いですね・・・種類がいくつかありますので、調べてみてください)。

*1:このパターンは渡邉さんがDDD査読時に出して下さったものです。感謝します。

ドメイン駆動式ソフトウェアの育て方

レッツゴーデベロッパー2011での発表原稿とスライド

導入

2011年05月28日「レッツゴーデベロッパー2011@仙台」が開催されました。このイベントのテーマは「共有と交流」。"「共有」には、最新技術、知識、復興への想い、それぞれの決意を共有することを、「交流」には、東北と東北圏外のデベロッパーやコミュニティ同士の交流を深めることを込めて。" このイベントにてDDDセッションに登壇させて頂きましたので、そのときの発表原稿とスライドを公開致します。なお、当日はワークとして参加者の方にペアモデリングを行って頂きましたが、このドラフトではその部分を割愛しています。


さて今年4/9にDDD日本語版が出版されました。それから2ヶ月弱、翔泳社様から、はやくも増刷のお知らせを頂きました。多くの方々とおかげと深く感謝しています。さて、この増刷が意味しているのは今や千人単位の方の手元にDDD日本語版があるということです。出版と時期を同じくして著者のエリックも来日しました。読書会も同時多発的に開催され始めています。日本におけるDDDは、間違いなく次のステージへと進んでいます。いわば、「読むことが目的だった時代の終焉」。読み、理解したら、その次に来るのは実践でしょう。DDDの考え方に従ってアプリケーションを作ったらどうなるのか、ということを考えることが今回のテーマです。

ドメイン駆動設計とは?

ドメイン駆動設計の考え方

先日、とある方とお話していたときに、非常に鋭い言葉を聞かせて頂きました。「ユーザさんには、自分が「ユーザ」であるという意識はない。彼らは自分の仕事をやっているだけなんだ」と。これは非常に的確でありながら、開発者としてはなかなか気づくことのできない視点だと思いました。DDDの根本的な関心もやはり同じようなところにあります。「システムを作る上ではまず顧客がどのようなビジネスを行っているかが重要である」、言葉にしてしまうと当たり前すぎるこのメッセージがドメイン駆動設計の出発点になります。逆に言えばこれまで我々は技術などにとらわれて、顧客の仕事を理解するという本当に重要なことを忘れていた、ということでもあるでしょう。


ドメイン駆動設計ではさらに、顧客のビジネスを顧客自身の言葉で理解することが求められます。単純にとらえれば、「同じ言葉で会話をする」、つまり「顧客が使っている言葉を自分も使う」ということです。ただ同時に、顧客が描いているモデルを共有するということでもあります。「こういう場合は特殊ケースなのでフラグを立てて・・・」ということを、勝手にやってはいけないということですね。勝手なフラグは言葉として相手が理解できないだけでなく、それを成立させる別のモデルを導入してしまっていることも示唆しています。会話やドキュメントなど、あらゆる場所で顧客の言葉を使うこと、それがユビキタス言語というパターンです。


「モデル」という言葉が出てきました*1。ここで言うモデルとは、「顧客の目から見た業務の姿」だと考えてください。モデルは現実そのものではありません。人間が現実をとらえたときの姿です。その際には、適切な抽象化が行われ、不要な詳細は捨て去られます。「何が重要で、何が重要ではないか」、その取捨選択の中にモデルの本質があります。開発者には、顧客の持つそういうモデルを共有することが求められます。先ほどのフラグの例で言えば、一見「特殊ケース」に見えるものが、業務の全貌を理解している人からすれば正常パターンの1つでしかない、ということはあり得ることです。


さて、せっかくモデルを共有しても、実装時に失われてしまっては元も子もありません。共有したモデルをソフトウェアの中に持ち込むこと、これがモデル駆動設計と呼ばれる重要パターンです。モデルと実装を結びつける上ではなんらかのパラダイムが必要になりますが、それにあたってDDDが準拠するのがオブジェクト指向です。概念をクラスとして表現することで、顧客の描くモデルをソフトウェアの中に反映するのです。

アーキテクチャ

そうした場合ソフトウェアはどのようなアーキテクチャになるのでしょうか。それについては第2部で語られることになります。中でも概要を理解する上でポイントとなるのがレイヤ化アーキテクチャという考え方です。レイヤ化アーキテクチャでは次の4つのレイヤが説明されています。

この中で最も重要なのがドメイン層です。これはまさに、モデルが存在するための空間となります。


ソフトウェアの中にドメイン層を確保する上で重要な役割を果たすのが、リポジトリです。通常のトランザクションスクリプトの場合、SQLを発行した結果得られるリザルトセットを直接使うケースがほとんどだと思いますが、DDDの提唱するアーキテクチャでは常にモデルオブジェクトを操作します。そのためリポジトリがクライアントからの要求に応じて、必要なオブジェクトをクライアントに渡すことになります。こうすることでクライアントは、オブジェクトがメモリ上にあるかのように操作することができます。

プロセス

「顧客はドメインをとらえるモデルを持っている」と言っても、そのモデルをアップフロントにすべて理解し実装することはできません。第一にドメインが複雑であれば、開発者側がすべてを一度に理解することはできませんし、顧客自身も実装可能なレベルで完璧なモデルを事前に持っているわけではないからです。したがって、顧客とドメインモデルを共有するプロセスは当然イテレーティブなものになります。


これについては、現在Ericが整備を進めているプロセスがあります。それが「モデルを探究するうずまき」です。このうずまきはまず「シナリオ」から始まります。顧客からシナリオを聞き出しつつ、それをモデル化していきます。モデルができたらすぐに新しいシナリオを聞いて、モデルに揺さぶりをかけます。この内側のループにかける時間は1日2日程度だそうです。ある程度モデルができあがったら、「コードプローブ」に入ります。シナリオをテストとして実装し、モデルが実際に使えるかどうかを確認します。結果はシナリオにフィードバックし、また新しいループが始まります。

イベント参加受け付けをモデリングする

2011年4月9日のDDD前夜祭では、@t_wadaさんによる"DDD Boot Camp"が開催されました*2。そのときのお題は「DDD前夜祭の申し込みをモデリングする」でした。今回は「レッツゴーデベロッパー2011の申し込みをモデリングする」と置き換えて考えてみたいと思います。


ポイントは先ほどの「モデルを探究するうずまき」の内側のループ、シナリオとモデルの往復を試すことにあります。その際、主要な概念とその関連性が見えてくれば成功だと言えるのですが、実際にやってみると意外と行き詰まります。シナリオを考える際に、「申し込みの締め切りをどうするか」という重要かつやや複雑な処理にいきなり飛び込んでしまうと「上限がどう決まるか?」「当日キャンセルは見込むか?」といった疑問がわいてしまって手が止まってしまうのです。できれば、わかりやすくシンプルなところから始めて、すこしずつ複雑なものに取り組んでいくという段階を踏みたいところです。シナリオの例を示しましょう。

  • イベント情報を表示する
  • 申し込みを受け付ける
  • 参加者一覧を表示する
  • 申し込みを締め切る

ソフトウェアを「育てる」

このようにすこしずつソフトウェアとモデルを膨らませていく上で大きなヒントを与えてくれるのが、「Growing Object-Oriented Software, Guided by Tests (Addison-Wesley Signature Series (Beck))」です。この本ではソフトウェアを「育てる」ために、受け入れテストとユニットテスト(場合によってはインテグレーションテスト)というレベルの違うテストで2重のループを構成することが提唱されています*3


具体的には、まず「Walking Skeleton」と呼ばれる、動くための最低限の機能を作ります。その上で、エンドツーエンドの受け入れテストを書きながら、1機能ずつ作っていくのです。これを実際に試してみるにあたり、今回は技術スタックとして以下に示すものを使います*4


ケルトンでは、シナリオ「イベント画面の初期表示」を実装します。

def "イベント画面初期表示"() {
    when:
        go "/PlanTheEvent/event/show"
    then:
        $("h1").text() == "イベント情報"
        $("td#detail").text() == "レッツゴーデベロッパー"
}

これだけでも、やることは意外とあります。画面とモデルオブジェクト、データベースのテーブルを作る必要がありますし*5アーキテクチャも定めなければなりません。また、フレームワークに慣れていなければ、使い方も一通り調べる必要があります。


こうしたことを行ってはじめてテストが通るのですが、その後ある程度リファクタリングも行わなければなりません。たとえば、前述したテストコードからはHTMLに依存した記述を取り除く必要があります。

def "イベント画面初期表示"() {
    when:
        go "/PlanTheEvent/event/show"
    then:
        $("#pageTitle").text() == "イベント情報"
        $("#detail").text() == "レッツゴーデベロッパー"
}

この段階でモデルオブジェクトが1つできあがっています。


次は、申し込み受け付け画面への画面遷移を経て、登録処理のテストを書きます。

def "参加者一件登録"() {
    when:
        go "/PlanTheEvent/participant/apply"
        $("form").twitterId = "@digitalsoul0124"
        $("form").message = "よろしくお願いします"
        $("#register").click()
    then:
        $("#pageTitle").text() == "イベント情報"
        $("#detail").text() == "レッツゴーデベロッパー"
        $("#participantsCount").text() == "1"
}

このテストがグリーンになるころにはモデルに参加者オブジェクトが追加され、申し込み受け付けの基本モデルが完成します。


先ほど話題にあげた要件「申し込みの締め切り」はこのモデルをベースに考えることになります*6。申し込みの上限をどう決めるかは、その業務を実際に行っている人に聞くしかありません。仮に「イベントを行う部屋の広さに加えて、ある程度のキャンセルを見込む」という答えが返ってきたとしましょう。ここに「部屋」という概念が新しく追加されます。


10%のキャンセルを見込むと考えて、次のようなコードを書けば、とりあえずテストは通るようになります。

// 満席判定
boolean fullToCapacity() {
    participantsCount() >= (roomsCapacity() * 1.1)
}

しかし、ここには概念が1つ暗黙的に潜んでいます。


「キャンセルを見込んで、多めに予約を取る」という考え方は通常「オーバーブッキング」と呼ばれます。このオーバーブッキングのポリシーをモデルで表現します。

具体的にどのようなメソッドを持たせるべきかは、コードを書いて確認します。

def "満席/11人で満席"() {
    when:
        def overbookingPolicy = new OverbookingPolicy()
        def room = new Room(capacity:10)
        def LIMIT = overbookingPolicy.limitFor(room)
        def event = new Event(room:room)
        for(i in 1..LIMIT){
            event.addParticipant(new Participant())
        }
    then:
        overbookingPolicy.fullToCapacity(event)
}

ここまでで、前述したシナリオを実現するモデルがいったん完成します。

※ここまでのコードはgithubにて公開しています*7

まとめ

今回は「ドメイン駆動設計を実践する」という観点から、シンプルなところから始めて、すこしずつ育てていくという手法を実際に試してみました。シナリオと紐づけながらモデルを作っていくことについて、ある程度のイメージを持って頂けたのではないでしょうか。


このような開発はユーザにとっても開発者にとっても理想だと思うのですが、今の日本ではなかなかできないのが実情でしょう。しかし、「できない」と嘆くだけではなく、「実際に自分はできるのだろうか?」と自問し、チャンスがあったときに確実に実践できるようにしておくことが求められる時代になってきているのではないかと思うのです。




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

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

Growing Object-Oriented Software, Guided by Tests (Addison-Wesley Signature Series (Beck))

Growing Object-Oriented Software, Guided by Tests (Addison-Wesley Signature Series (Beck))

*1:この点については、ドメイン駆動設計入門 - Digital Romanticismで詳しく説明しています

*2:http://www.slideshare.net/t_wada/devlove-dddbc

*3:この本の中ではモックを使ったユニットテストがきわめて重要なものと位置づけられていますが、今回の発表ではその点には触れていません。

*4:SpockとGebの選定にあたっては、@bikisukeさんにご支援頂きました。ありがとうございました。

*5:今回、永続化は行わずオンメモリで実装しています。

*6:参加者一覧の表示はこのモデルを使って実装できますので、ここでは割愛します。

*7:デモ時にあった警告は@nobeansさんにより修正して頂いています。また、@kiy0takaさんが受け入れテストをPageクラスを使って書き換えてくださっています。お2人ともありがとうございます!!

関数型Scala(7):ラムダとその他のショートカット - Mario Gleichmann

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




前回は、関数型プログラミングの世界における最も強力な概念のうちの1つ、すなわち高階関数を見てきました。本質的に、ある関数が高階と呼ばれるのは、他の関数を引数として受け取る場合か、結果として関数を出力する場合でした。この考え方により、抽象化の新しいやり方がもたらされるだけでなく、ある種の状態やいわゆるコンビネータ(関数ビルダと呼んでも構いません)を捕捉したり受け渡したりするといった、かなり便利なこともできるようになるのです。なお、コンビネータとは入力された関数もしくはその他の入力を元に新しい関数を構築するもののことです。


特に、新しい抽象化の形式に関して言えば、ユースケースに特化した変更されるロジックを関数から抜き出し、後には純粋な機能だけを残す方法について見てきました。特殊なロジックは独自の関数で定義され、引数として渡されます。このようにして、フィルタリング用の単一の関数と、それぞれでリストがどのようにフィルタリングされるかを決定する多くの述語関数が作られました。この種の抽象化が抽象的であるように思えるなら、もう一度フィルタ関数を掲載しましょう。

val filter = ( predicate :Int => Boolean, xs :List[Int] ) => { 
    for( x <- xs; if predicate( x ) ) yield x 
} 
…
val even = ( x :Int ) => x % 2 == 0 // ★1
val odd = ( x :Int ) => x % 2 == 1  // ★2val candidates = List( 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 )
val evenValues = filter( even, candidates ) 
val oddValues = filter( odd, candidates ) 

これはもう知っています!1行目から始まる高階関数と、5行目(★1)6行目(★2)の述語関数が2つですね。これらの関数を背景として、リストと任意の述語関数をフィルタ関数に適用し、フィルタリングされたリストを受け取ることができます。そして、フィルタリングによって素数のリストを作りたいと思えば、適切な述語関数をもう1つ定義するだけです。さて、ここで疑問なのですが、関数フィルタに渡すためには、常に予め述語関数を定義しなければならないのでしょうか?何のための質問でしょうね?もちろん、フィルタに渡すためには関数が必要ですし、関数が天から降って来るわけではありません!もちろん、もちろんです。しかし、覚えていらっしゃるでしょうか。関数を扱った第2話で純粋な関数リテラルについて見ています:

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

あー、思い出しました?ここに書いたのは関数の中核です。つまり、引数のリストがあって、その後に関数矢印( => )と関数の本体が続きます。これは完全な関数で、ただ名前が与えられていないだけです。このようなやり方で関数を定義したら、後でその関数を参照する方法はなく、したがって後で呼び出すことはできません。名前のない、無名関数だからです。そしてここに、いわゆるラムダ式が登場します!これはまさに、純粋で無名の関数定義です。


これは私たちの疑問に対して、どう応えてくれるのでしょう?そうですね、無名の関数定義であっても、値を表します。これは他のあらゆる型のあらゆる値を定義し、それを名前と関連づけない場合と変わりません(後で参照するために、その値に別名をつけることをやらない、ということです):

val almostPi = 3.14159265                     // Double 型の値で、後に名前 almostPi を使って参照できる
2.71828182                                    // Double 型の別の値。今度は「無名」
val mult =  ( x :Int, y :Int  )  =>  x * y    // ( Int, Int ) => Int 型の値で、名前 multを使って参照できる
(  x :Int, y :Int )  =>  x + y                //  ( Int, Int ) => Int 型の別の値。今度は「無名」

Double 型の値を参照している別名を用いて関数を呼び出すことができるのと同様に( double( almostPi ) のように)、Double 型のリテラルを使ってその関数を呼び出すことも当然できます( double( 2.71828182 ) のように )。ふぁーあ。いつもの通りですね。その通りです。関数型プログラミングにおいて、ラムダ式を直接高階関数に渡すのは完全に普通のことなのです!そしてこれが、私たちのいくぶんわざとらしい質問に対する答えとなります。つまり、高階関数の呼び出しに使う前に、述語関数を名前と関連づけて導入する必要は必ずしもないということです。その代わり、関数を呼び出す際に都度定義することができるのです:

val evenValues = filter( ( x :Int ) => x % 2 == 0, candidates ) 
...
val positiveValues = filter( ( x :Int ) => x > 0, candidates ) 

OK、素晴らしい。しかし、一度しか使わない関数を前もって定義しなくてよくなるということ以外に、どういうメリットがあるのでしょうか?そうですね。儀式的なコードの量をさらに減らすことができるということがわかります。Scala型推論カニズム万歳!関数フィルタの型を細かく見ると、( Int => Boolean, List[Int] ) => List[Int] という型であることがわかります。そして、その型がわかるのはあなただけではありません。Scalaコンパイラも、関数の型を見抜くことができるのです!そして、コンパイラが述語関数の型を知っているので、引数の方を関数リテラルを使って明示的に註釈をつけなくてもよいのです(したがって、引数の型は、高階関数の呼び出しで関数定義をする際に、その都度決定されます)。

val evenValues = filter( x => x % 2 == 0, candidates ) 
... 
val positiveValues = filter( x => x > 0, candidates ) 

さらに、お望みであればもっと簡潔に書くこともできます。すべての引数を関数本体内で一度しか参照しないのであれば、関数リストの宣言を省略することができるのです!うーん、えっと、それでは、関数本体の中でどうやって引数を参照するのでしょうか?ここで、不思議なアンダースコア( _ )がはじめて機能するようになるのです。この記号は(後の回でも見るように)何でも屋です。今回は、与えられた引数リストを順番に参照するためのショートカットになります。関数本体で登場するアンダースコアは、与えられた引数の値と置き換えることができます。つまり、最初のアンダースコアは最初の引数の値を参照し、次のアンダースコアは2番目の引数の値を参照する、といった具合です。

val evenValues = filter( _ % 2 == 0, candidates ) 
...
val positiveValues = filter( _ > 0, candidates ) 

人によっては、この形式は簡潔すぎると感じるようです。趣味の問題だと思いますけどね。しかし、可読性の高いコードという観点からすると、この書き方はあまりに多くの情報を隠しているかもしれません(少なくとも、私のように静的型付け言語から来た人間からすると)。そこで、簡単なアドバイスですが、アンダースコアの記法を適用するのはきわめて「経済的な」場合だけとし、裏にある型がコンテキストから容易に把握できる状況に限定した方がよいでしょう。


実際にはアンダースコア記法は、正統的な関数定義において、コンパイラが与えられた引数の型を推論できる限りは使うことができます。コンパイルエラーが起きる例を示しましょう。

val mult = _ * _ // compile error : missing parameter type ...

次に示す、若干変更したバージョンはスムーズにコンパイルされます。関数本体においてアンダースコアで示されている各引数の型を明示的に記述できているからです:

val mult = ( _ :Int ) * ( _ :Int ) 

この場合、コンパイラには、関数の値を導き出すのに必要なものがすべて与えられています:アンダースコアは2回登場しますが、どちらもInt 型として註釈がつけられています。したがって、この関数の引数リストは2つの引数からできていて、どちらも Int 型となります。関数をこのかたちで定義するのがわかりやすいかどうかの判断は、あなたにお任せします。しかしながら、(少なくとも私には)もう少し読みやすいと思える妥協点があります。関数リテラルを別名と関連づける際に、式全体の型を明示的に宣言してもよいのです:

val mult : (Int, Int) => Int = _ * _ 

いいでしょう。ここでは、2つのアンダースコア両方のコンテキストは、関数の型註釈内で見事に記述されています。引数リストを見さえすれば、2つのアンダースコアの並びが混乱を招くことはありません。関数の本体が、例で示したように短い場合には特にそうです。

まとめ

今回は、関数リテラルの、ちょっとかっこいい新しい用語を学びました。それが、ラムダ式です。ここまで、その都度定義されて使い終わったら捨てられることになる関数を使うのが適切な状況をとりあげ、ラムダ式が役に立つところを見てきました。これにより、特殊な形式の関数につけられた奇妙な名前がレパートリーに加わったことになります。それに加え、関数の引数に対するプレースホルダとしてアンダースコアを用いることにより、儀式的なコードを減らす方法にも触れました。おわかりの通り、使い方は好みの問題かもしれません。こうした過程で型情報が失われてしまうかもしれないからです。しかしながら、コンパイラが型情報を見失うことは決してない、ということは本質的です。Scalaは静的型付け言語ですから(型情報を省略できる時もあるので、Scalaが動的型付け言語であるように見えるかもしれませんが)。そのような場合には、失われた型情報を私たちが提供しなければなりません。やり方は、関数本体でアンダースコアを書くたびに型情報を添えても、関数式全体に型を明示的に書いても構いません。いずれにしても、こうしたショートカットを活用する場合には、特に可読性という観点から見た結果について、明確に意識しなければなりません。

関数型Scala(6):高階関数 - Mario Gleichmann

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




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


今回は、このシリーズでほぼ毎回、何度も言及してきた強力な概念について詳しく見ていきましょう。それが、高階関数です。かっこよくありません?そうでもないですよね。名前がかっこいいだけでは、あなたを興奮させることはできませんから。そこでこれから、どうかっこいいのかを実演し、「高階関数は、関数型プログラミングにおいて、エレガントに、コンパクトに、そして効率的に書くための基本原則の1つである」という、少なからぬ声の原因となっている本質的な側面を示しましょう。


さて、それでは高階関数とは何でしょう?高階関数にはなんらかのメタマジックがあるのでしょうか?あるいは、なぜ高階(higher ordered)と呼ばれるのでしょうか?まずは一般的な問題に焦点を合わせることで、高階関数に対する感覚をつかみましょう。一般的な問題とはすなわち、コードの重複です。単純な関数を書くところから始めましょう。次のコードは整数値のリストに適用できる関数で、結果として入力されたリストのうち偶数の値だけを含むフィルタリングされたリストを生成します。:

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

    for( x <- xs; if x % 2 == 0 ) yield x 
}

こうなりますね。この単純な関数は、整数値のリストを受け取り、その整数値のリストは内包の中でジェネレータとして使われます。素晴らしいですね。すでに前回、Scalaの内包モデルについて解説していますので、ここで何が起きているのかを理解するのに、なんら問題がありません。つまり、出力変数 x(ジェネレータに由来する)がとり得るすべての値を走査し、与えられたガード節が与えられた述語を満たす値だけを出力関数に渡します。そして、述語 x % 2 == 0 を満たすのは・・・ジャジャーン・・・偶数です。


ちなみに、結果となるリストのために選択される適切な値を検出するという、きわめて重要な部分は、ガード節で用いられる述語であるようです。述語を取り巻くそれ以外のものは、どれも必要なものとそうでないものを振り分けるための機械的な仕組みにすぎず、重要な決定は述語によって行われるのです。ここで述語が持っている例外的な位置づけを強調するために、述語を取り出して独自の関数に入れることができます:

val filterEven = ( xs :List[Int] ) => { 
    val even = ( x :Int ) => x % 2 == 0 
    for( x <- xs; if even( x ) ) yield x 
}

ここまでは、本当に刺激的なことは起きていません。ただ、述語を抜き出して、ローカル関数として定義しただけです(クロージャを扱った際にローカル関数についてはすでに見ました)。こうすることで、後に出て来る内包のガード節でローカル関数を参照することができます。また、内包の式は関数の中にある最後の式であるため、この式の値は自動的に関数全体の結果となります。これはすなわち、内包によって作られる、フィルタリングされたリストにすぎません。


いいでしょう。では、ちょっと面白半分に、整数値のリストをもう一度フィルタリングしてみましょう。ただ今度は、入力されたリストのうち、奇数の値だけを出力します。すでに、フィルタリング関数を書くことについては、すでにいくらか経験を積んでおり、いくつかフィルタを書き足すのは本当に楽しい練習ですので、この課題は朝飯前でしょうし、私が「ボブはあなたのおじさんです」と言い終わる前に書くことができるでしょう:

val  filterOdd = ( xs :List[Int] )  =>  { 
    val odd  =  ( x :Int )  =>  x % 2 == 1 
    for(  x <- xs;  if odd( x )  )  yield x
}

・・・「ボブはあなたのおじさんです」


おお、あっという間で、本当に楽しいですね!いいでしょう。もう少したくさんフィルタを書くこともできますね。たとえば、素数、特定の範囲の数字、5の倍数、特定のチェックサムを持つ数字、フィボナッチ数、特定の数字の自然因数・・・それでも、まだ楽しめますか?これは、時間の経つのが遅い、長く寒い夜のことかもしれません。しかし、他の人が皆寝静まっている以上、このフィルタリングをするための雛形を何度も何度も繰り返し書くという作業を避ける方法はないのでしょうか?その繰り返しの作業を避ける方法がありますか? すでにおわかりの通り、変化するのは、これら別々のフィルタのための述語だけです。それでは、フィルタリングというタスク(出力すべきものとそうでないものを区別するという純粋なふるまい)を抽象化して、どれを出力するかを決めるタスクと区別することがどうしてできないのでしょうか?


残念ながら、述語を表現する関数をフィルタの残りの部分から切り離すことはできないんですよね・・・ん、ちょっと待った!関数がファーストクラスの値であると言いませんでしたか?言いました!では、関数も他の値と変わらないんですよね?その通り。型が違うだけです。型の違いを別にすれば、特別なことは何もありません。たとえば Int 型と同じです。そうであれば、List[Int] 型とも同じですか?その通り!ということは、List[Int] 型の値をフィルタ関数に引数として渡すことができるならば、それが意味するのは・・・それが意味するのは・・・?その通り、わかりましたね!それが意味するのは、ここに書かれた述語関数のような関数も、ある関数に対する普通の引数と同じように渡すことができるということなのです!


唯一残っている疑問は、フィルタリング関数に対して述語関数を新たな引数として追加する際、それをどうやって宣言するかです。述語関数の正確な型を知る必要があります。中間のステップとして、前述した最後の関数を拡張し、述語関数の型を明示的に記述することができます:

val  filterOdd = ( xs :List[Int] )  =>  { 
    val odd : Int => Boolean  =  ( x :Int )  =>  x % 2 == 1
    for(  x <- xs;  if odd( x )  )  yield x
}

なるほど。述語が、Int 型の値が結果となるリストに入るかどうかを(true か false か)決定するものである限り、明らかに述語関数は、Int => Booleanという具体的な型になります。あとは、その関数(と必要な値)をフィルタ関数の引数のリストに、もう1つの入力される値として追加するだけであり、それは次のようになります:

val filter = ( predicate :Int => Boolean, xs :List[Int] ) => { 

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

おめでとうございます。はじめての高階関数を作ったことになります!難しいことは何もありません。高階関数とは、引数として他の関数を受け取る関数にすぎないのです。そして、関数が引数の値を結果の値に紐づけるものである一方(関数も値です。念のため)、関数の結果が関数になるのも完全に正当なのです。そこで、関数がその引数のうちの1つとして別の関数をとるか、結果として別の関数を返すようになると、ただちにそれが高階関数と呼ばれます。それだけですよ、皆さん!


高階関数について、これほどのお祭り騒ぎが繰り広げられているのがなぜなのか、今は不思議に思うかもしれませんね。でもちょっと待ってください。今はただ表面をひっかいて、高階関数が主に適用される領域への扉を開いただけなのです(これについては、今後のエピソードで1度ならず取り上げていきます)。私たちがちょうど今発見した新しいツールは、後になれば、非常に柔軟で、しかも強力だということがわかるということです。マクガイバー*1が関数型プログラマであったら、この高階関数をお気に入りのツールの1つに選ぶでしょう。仮にペーパー・クリップを使ったとしても、マクガイバーがそこから何を生み出せるのか、あなたはご存知ですよね?


後には何が残るでしょう?整数値のリストをフィルタリングするための純粋な仕組みを、単一の関数 filterカプセル化することに成功しました。その際に、フィルタの述語(結果となるリストに残るのがどの値かを決める責務を持つ)は、独自の関数に抽象化されて切り出されました。そして、このフィルタ関数は、今や望みのフィルタ述語を使ってパラメタ化できるので、私たちはこの同一のフィルタ関数を参照し、別々の述語関数を渡すことによって、さまざまな用途に使うことができるのです。:

val even = ( x :Int ) => x % 2 == 0 
...

val odd = ( x :Int ) => x % 2 == 1 
...

val candidates = List( 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ) 

val evenValues = filter( even, candidates ) 
val oddValues = filter( odd, candidates ) 

いいでしょう。では最後にもう1つ、もう少し複雑なことを練習してみましょう。これがわかれば、高階関数の基本はすべて身につけたことになるでしょう!素数のリストをフィルタリングできるように、もう1つフィルタ述語を書いてみましょう。これは、少なくとも2段階で行い、これまでに学んだ概念をいくつか活用してみましょう。まず第一に、素数として認められる数字の性質について考えましょう。2から、その数の半分までの間に自然因数があってはならないという事実に合意できるでしょうか?よろしい!


それでは、素数の述語に戻る前に、固定の整数に対する自然因数になるであろう数字のリストをふるい分ける関数を書いてみます。通常であれば、引数を2つとる関数を書きます。つまり、最初の引数は任意の数をあらわし、2つめの引数はその数字がとり得る因数をあらわします:

val isFactor = ( num: Int, factor :Int ) => num % factor == 0 

残念ながら、この関数はフィルタ述語として使うことができません。フィルタは、 Int => Boolean 型の関数を要求するからです(今書いた関数の型は、(Int, Int) => Boolean です)。そのため、たとえば100のすべての因数からなる整数値のリストをフィルタリングしたければ、次のような述語関数を書く必要があります:

val isFactorOfHundred = ( factor :Int ) => 100 % factor == 0 

これでいいですか?次に、99、そして1000、その次が1558で、その次が・・・うーん、使いやすそうですか?ここで書いた filter 関数と同じで、因数のリストをフィルタリングしたいすべての数字に対して、新しい述語関数を書きたいとは思えません。ここで高階関数が救いの手を差し伸べます!任意の数字を受け取って、別の関数を返すような関数はどうでしょう?ここで返される別の関数が、他の数を受け取って、それが最初の数の因数かどうかを評価するのです。ややこしいですか?実際にその関数を見れば、すぐに明確になります:

val isFactorOf  =  ( num :Int ) => {

    ( factor :Int ) => num % factor == 0
}
...
val isFactorOfHundred = isFactorOf( 100 )
val isFactorOfNinetyNine = isFactorOf( 99 )
val isFactorOfThousand = isFactorOf( 1000 )

ちょ、ちょ、ちょっと待ってください!どういうワザですか?ここに書いた関数 isFactorOf は、単なる高階関数です。ただ、今回の場合、結果が別の関数になることからそう呼ばれるのです。本体の中にあるのは、もう1つの関数の定義にすぎません。この関数は、isFactoryOf が呼び出されるたびに構築されるのです。そして、その関数定義が関数本体における最後の式であるため、その式の値(つまり関数の値)は、関数全体の結果となります。同時に、結果として生じる関数は、クロージャと呼ぶことができます。なぜかわかりますか?そうですね、引数として宣言されていないことから、num は自由変数です。この場合、囲い込みが行われて、自由変数は周囲を取り巻く関数の引数に束縛されます。こうすることで、任意の数の因数となる整数のリストをフィルタリングできるようになりました。:

val factorsOfHundred =  filter( isFactorOf( 100 ), candidates ) 

ここにも、特筆すべきものはありません。何が起きているかを完全に理解するために必要な素材はすでに手のうちにあります。ここで本当に興味深いのは、第一引数だけです。isFactorOf を値 100 に適用し、その結果は関数になります。その関数が今度は別の整数値をとり、100の因数かどうかを決定します。その都度生成されるこの関数は、Int => Boolean 型であり、したがって、フィルタの述語関数として用いることができます。そこで、フィルタは渡された述語関数を使い、入力されたリストのどの数字が出力されるリストに入るのかを決定します。


この述語関数メーカーを用いることにより、私たちは頭を切り替えて、再び素数をフィルタリングするためのメインの述語に集中できます。ある数字を素数であると判定する上で、関数メーカーがどう役に立つかを考えてみましょう。2から問題となる数字の半分の範囲で、因数があってはならないという事実については、すでに合意しています。これでピンときますか?整数値のコレクションの中に因数がないことは、どうやって調べることができるでしょうか?それでは、その数字の因数だけを取り出すような適切な述語を使って、そのコレクションをフィルタリングするというのはどうでしょう?フィルタリングされたリストが空であれば、その範囲内にある数字には明らかに因数がないことになります。それゆえ、その数字は間違いなく素数です:

val prime = ( num :Int )  =>  num > 1  &&  filter( isFactorOf( num ), (2 to num/2).toList ).isEmpty

いかがでしょう。フィルタリングに基づいて素数かどうかを判定する述語関数ができました。この関数が今度はフィルタリングに使用されるのです。

まとめ

今回のエピソードを通じて、高階関数と呼ばれるものの意味がわかりました。今回は主に、他の関数を受け取るか、関数を返すような関数という基本的な特徴について焦点を合わせてきましたが、それがどういう力を発揮するのか、あなたがたは少しわかっているのではないかと思います。実際、関数型プログラミングの世界における演算作業の多くにとって、(すでに見た通り)高階関数は欠かせません。高階関数によってもたらされるのは、新しい抽象化のやり方(たとえば、Scalaローンパターン*2と呼ばれるリソース制御)や、ある種の状態のシミュレーションもしくは捕捉(差分リストについて考える際に見ていきます)だけでなく、関数型プログラミングが他にも持っている、よく知られた強力な特徴(たとえば、いずれ見ていくカリー化など)の基礎にもなっているのです。

付録

Scalaオブジェクト指向と関数型の特徴を併せ持つハイブリッド言語であるため、Scalaの List型は(実は、List型コンストラクなのですが、これはまた別の機会に)すでに、汎用的な filter メソッドを提供しています。つまり、整数値のリスト(List[Int] 型のオブジェクト)があれば、そのオブジェクトの filter メソッドを呼び出し、Int => Boolean 型の任意の述語関数を渡すことができるのです(これは、前述した filter 関数で行ったのと同様です)。

val candidates = List( 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ) 
val evenValues = candidates.filter( even ) // List( 2, 4, 6, 8, 10 ) 
val oddValues = candidates.filter( odd ) // List( 1, 3, 5, 7, 9 ) 
val factorsOfTwelve = candidates.filter( isFactorOf( 12 ) ) // List( 1, 2, 3, 4, 6 ) 
val primes = candidates.filter( prime ) // List( 2, 3, 5, 7 ) 

お望みなら、このメソッドを高階メソッドと呼ぶこともできるかもしれません。


参考文献

プログラミングScala

プログラミングScala

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

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

*1:訳註:アメリカのドラマで、邦題「冒険野郎マクガイバー」の主人公の名前。紹介サイトによれば、「手近にある材料を使って、悪の陰謀を打破する」タイプのアクションドラマで、「ペーパークリップで核ミサイルの発射を食い止めたり」できるそうです。日本ではちょうど「特攻野郎Aチーム」の後にテレビ放送されたみたいですね。私は見たことがありません。

*2:ローンパターンとは、オープン・クローズが必要なリソースに対して、それを制御する関数がリソースを「貸し出す」というもの。詳しくは「Scalaスケーラブルプログラミング[コンセプト&コーディング] (Programming in Scala)」(p.167)を参照。

関数型Scala(5):内包を理解する - Mario Gleichmann

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




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


これまでに集合の内包について聞いたことはありますか?そうですね、数学の授業を受けたことがあるなら、きっと聞いたことがあるでしょう。しかし、聞いたことがなくても、怖がることはありません!これは単に、ある集合のメンバが満たさなければならない性質を述べることで、要素の集合を定義する数学的な記法にすぎないのです。この説明があまりに抽象的に思えるなら、単純な例を見てみましょう:

{ x | x ∈ N : x == x² }

ここにあるのは、ある値の集合に関する記述です。その値とは、自然数に含まれていて、自乗したものがそれ自身と等しい数字です。まじめに考えれば、与えられた制約を満たす自然数が2つしかないことにすぐに気づくでしょう。そうです、上記の集合の内包は、集合 {0, 1} のための定義なのです。かっこよくありません?それほどでもないのはわかっています。この集合の内包は単に集合を書き出すだけよりもスペースをとりますから。ただ、要素の集合をすべて列挙したら、集合の内包を使うよりも時間とスペースを使う場合もあるでしょう。これを見てください:

{ x | x ∈ R : x > 0 }

そうです。これは、正の実装すべての集合です。それでは、たっぷりと時間をとって、この集合を直接書き出してみましょう。ちょっと場所が必要かもしれませんね・・・OK、その辺でいいでしょう。しかし、こうしたことは関数型プログラミング全般と、とりわけScalaとは、どう関係するのでしょうか?そうですね、ほとんどの関数型言語は、いわゆるリスト内包と呼ばれる形式を何らかのかたちで提供していることがわかります。集合の内包と対照的に、集合の要素を定義するのではなく、代わりに・・・リストの要素を定義するのです。


それでは、他の例を用いて、Scalaの内包を使うとどのように表現できるのかを見ていきましょう。最初の5つの平方数を定義するリストを試してみましょう。最後に、まずは集合の内包として表現してみましょう:

{ x² | x ∈ N : x > 0 ∧ x < 6 }

これをScalaのリスト内包に翻訳する前に、内包に含まれる個々の要素をより詳細に見ておきましょう。パイプ(|)の手前にあるのは、出力関数と呼ばれるもので、出力変数 x を伴っています。出力関数により、結果として生じるリストの値を算出する方法が決定されます。パイプの後にあるのは、出力変数のドメインを規定する入力集合であり、その後に、与えられたドメインをさらに限定する制約が続きます。出力変数に対する妥当な値と考えられる全ての要素に対して、この制約一式が当てはまらなければなりません。


前節で、「最後に」と言ってしまったのがウソだったことを、私は認めなければなりません。もう一度だけ、前述の集合の内包に変わるバージョンをお見せします。しかし、どうか許して下さい。これはScala風の内包に入りやすくするためだけです:

{ x² | x ∈ { 1 .. 5 } }

なるほど、出力変数のドメインを特徴づけるために別の集合を使えば、前述した制約のいくつかは必要なくなるということを理解して下さい。この観点からすれば、内包は、より一般的な集合から、より特殊な集合を構築するための仕様であると言えるかもしれません。よろしい、Scalaにおける最初の内包を構築するには、これで十分です。内包にある決まった要素を知りたいと思うかもしれませんね。では、見ていきましょう・・・

for( x <- ( 1 to 5 ) ) yield x * x

もう、ウソはついていませんよ、本当です!最初は、for ループのように見えますが、これは純粋な内包です!ただ、個々の要素のための表記法が、最初はすこし奇妙に映るかもしれませんね。明らかに、x は出力変数です。x は、for ブロック内の最初の要素として示され、x のための入力ドメインが後に続きます。出力変数と入力ドメイン間の相関関係は、両者の間の矢印( <- )で表現され、両方の要素を互いに接続しています。この場合、入力ドメインは範囲 ( 1 to 5 )によって構築されていますが、これは一般的にScalaでは、ジェネレータGenerator)と呼ばれています。実際、(後に見ていく、Functor という意味での)メソッド map、(Monad という意味での)メソッド flatMap、メソッド filter を提供する型はどれも、リスト内包におけるジェネレータとして機能します!
入力ドメインは?OK!出力変数は?OK!しかし、結果となるリストの形態を決定する出力関数はどこにあるのでしょう?もうおわかりだと思いますが、 yield に続く内包の最後の部分がそれに当たります。出力関数は、実際、内包表現全体の結果として生み出されるものに対して責任を持つのです。

複数のジェネレータ

さらに先に進みます。出力変数と、それに関連するジェネレータに関しては、1つだけに制限されません。好きなだけ多くの出力変数(と、それに関連するジェネレータ)を、俎上にのせることができるのです。たとえば、1〜3の範囲のペアのデカルト積であれば、次のように生成することができます:

for( x <- (1 to 3 ); y <- (1 to 3) ) yield (x,y) // (1,1), (1,2), (1,3), (2,1), (2,2), (2,3), (3,1), (3,2), (3,3)

それでは、出力変数のとり得る可能性を走査する際の順序を見てみましょう。そう、左から右です。つまり、最初のジェネレータ(左の x )がある値から次の値に増える前に、後のジェネレータが全てのドメインをいったんすべて走査します。これは、x の次の値、そしてその次の値と続いていきます。さらに理解するためには、ネストされた2つのforループだと考えてもいいかもしれません。最初のジェネレータが外側のループに対して作用し、次のジェネレータが内側のループに対して実行されるのです・・・


なるほど、いいでしょう。(1, 3)と(3, 1)のように順番が入れ替わっただけで値が同じペアのうち、1つだけを残したいと思ったらどうなるでしょう。問題ありません!あるジェネレータよりも前のジェネレータ(つまり外側のループ)で定義された出力変数を参照してもよいということがわかります。その場合、2番目のジェネラータの範囲を定義する際に、現在の x を参照するだけです:

for( x <- (1 to 3 ); y <- (1 to x) ) yield (x,y) // (1,1), (2,1), (2,2), (3,1), (3,2), (3,3)

そして、前に定義されたジェネレータの(中間的な)結果を、後に続くジェネレータで参照できることから、リストに対しても非常に面白いことが実行できます。リストもジェネレータとしてふるまうからです。たとえば、リストのリストで、各サブリストがなんらかの整数値を含んでいるようなものについて考えてみてください。その整数のリストのリストを平坦にして、結果として、すべてのサブリストにある整数値を全て含む単一のリストを作りたいとしましょう。そうです。内包を活用すれば、簡単に実現できます:

val flatten = ( xss :List[List[Int]] ) => for( xs <- xss; x <- xs ) yield x 

え、おっと、これだけですか?その通り、こんなに単純なのです!与えられたリストのリストから、新しいリストを作るのに、リストを開梱しただけです。つまり、最初の出力変数は、与えられたリストに含まれる全てのサブリストを辿りました。2番目のジェネレータがそれらのサブリストを参照し、その中身を明らかにしました。そして、その中身は順番に出力関数によってリストとして生成されたのです。

ガード節

ここまでは、与えられた入力ドメインに含まれるすべての要素(つまり、与えられたジェネレータによって生成されるすべての値)を走査する内包だけをつくってきました。集合の内包を振り返ると、出力変数がとり得る妥当な値をフィルタリングするために、制約を宣言することができるということは、すでに見てきました。Scalaでは、内包内でいわゆるガード節を認めているということがわかります。このガード節は、出力変数がとり得る妥当な値に対して成立する、述語の集合を表現するガード節とまったく同じです。


最初の例として、与えられた整数値のための自然因数すべてのリストを生成したいとしましょう。入力ドメインは、何になるでしょうか?そうですね、1から因数を計算したい値までのすべての自然数というのはどうでしょう?いいでしょう。しかし、それでは与えられた整数値に対する自然因数にはなりません(関数への入力を1と2に限定すれば別ですが)。こうした数字が因数となるのは、与えられた値をその数字で割り切ることができるときだけです。ふむ、意味のある理にかなった制約ですね!それでは、書いてみましょう:

val factors = ( n :Int ) => for( x <- ( 1 to n ); if n % x == 0 ) yield x 

ご覧の通り、ガード節はジェネレータの直後に、シンプルに宣言されます。もちろん、書けるガード節はひとつだけではありません。必要なだけガード節を追加して構いません(それぞれのガード節はセミコロンで分割されます)。ただ、ある値が妥当な出力変数となるには、すべてのガード節の述語に対して真にならなければならないということを覚えておいてください。理解度を上げるために、もう1つ例を挙げます。ここでは、整数値のリストから素数をフィルタリングします:

val primes = ( xs :List[Int] ) => 

    for ( x <- xs; 
           allFactors = factors( x ); 
           if allFactors.length == 2; 
           if allFactors(0) == 1; 
           if allFactors(1) == x 
        ) 
        yield x ... primes( List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12) ) // List(2, 3, 5, 7, 11) 

よろしい。どの整数値が出力変数としての資格を得ているかどうかを決定するには、すべての因数を検査しなければなりません。因数が2つしかなく、それが1とその数自体であれば、それは素数です。もちろん、そうです。しかし、ローカルの値である allFactors はどうでしょう?そうですね、これがScalaの内包表記のもう一つの特徴です。後になって参照したいと思うであろうローカルの値を、好きなだけ宣言することができるのです。これは、後に続くジェネレータの中であっても構いません!


すべてのピースを集めて、もう少し複雑な例を用いてこのエピソードを終えましょう。ここでは、Scalaの内包メカニズムを使う、別のユースケースが示されます。次のシナリオには、会社の集合と、従業員の集合があると考えてください。そのために、case クラスを2つ定義しましょう(case クラスについては、代数でデータ型(algebraic datatypes)の話をするときに詳細に見ていきます)。それが、 企業(Company) と 従業員(Employee) です。そして、企業と従業員のリストを生成します:

case class Company( val name :String, val region :String, val avgSalary :Int ) 

case class Employee( val name :String, val companyName :String, val age :Int ) 
...

val companies = List( Company( "SAL", "HE", 2000 ), 
                                 Company( "GOK", "DA", 2500 ), 
                                 Company( "MIK", "DA", 3000 ) ) 

val employees = List( Employee( "Joana", "GOK", 20 ), 
                                 Employee( "Mikey", "MIK", 31 ), 
                                 Employee( "Susan", "MIK", 27 ), 
                                 Employee( "Frank", "GOK", 28 ), 
                                 Employee( "Ellen", "SAL", 29 ) ) 

ここで、次の制約を全て満たす従業員全員を取得したいとします:

  • 25歳以上の従業員のみ
  • 地域“DA”にある会社で働いている従業員のみ
  • 勤めている企業の平均月給よりも高い月給をもらっている従業員のみ(従業員の年齢×100 で算出される月給を平均とする)

見つけた従業員すべてに対して、従業員名、勤めている企業名、企業の平均月給をどのくらい上回っているかを取得したいと思います。えっと、ちょっと待ってください。このシナリオでピンときますか?関係データベースとSQLになじみがあれば、このちょっとした難問を一瞬で解決したことでしょう。よろしい、従業員と企業の2つのリストがデータベースから読み込まれ、選択は後でプログラム的に行われると思ってください。しかし、どうやって望みの従業員を取り出せばよいでしょう?うーん。おそらく内包を活用するんでしょうけれど?おお、悪くありません。どうして思いついたんですか?実は、内包を一種のクエリと考えることができるのです。内包にも、select 句(出力関数)、from 句(入力ドメイン)、そして where 句(ガード節)があるのですから。それでは、難しい話は抜きにして、これまで学んだことをすべて考慮に入れ、クエリを内包として表現してみましょう:

val result = 
    for( e <- employees; 
                  if e.age > 25; 
                  salary = e.age * 100; 
           c <- companies; 
                  if c.region == "DA"; 
                  if c.name == e.companyName; // ★  
                  if c.avgSalary < salary 
    ) 
    yield ( e.name, c.name, salary - c.avgSalary ) 


println( result ) // List( (Mikey, MIK, 100), (Frank, GOK, 300) ) 

なんと、魔法みたいじゃないですか?そんなことないと思ったあなた、正解です。これは単に、問い合わせという特殊な問題に適用された内包にすぎません。しかし、そういう見方をすれば、この内包の各部分をよく知っているクエリと比較することが簡単にできるのです。内部結合に対応するものさえ存在します。おわかりですか?そうです。8行目(★の部分)ですね。ここでは、企業と従業員の間で企業名が関連づけられています。

まとめ

ふう、なんて素晴しい旅だったのでしょう!これまで、Scalaの for 記法のことを、今までのものよりも優れたループであると考えていたなら、少し違う見方ができるようになっていることを望みます。このエピソードの一番最初で言った通り、どちらかと言えば、一般的なデータから、いくらか特殊なデータを取り出す仕組みなのです。そして、ここにも可変なデータは登場しません。あるのは、内包の中に入って行くいくつかの値と、内包の結果から出てくるいくつかの値だけです。これを見ると、関数型プログラミングの基本原則を思い出しませんか・・・?

技術系ブログの書き方

勉強と結びつけながらブログを続けていくためのプラクティスについて整理する。

はじめに

エリック・エヴァンスのドメイン駆動設計 (IT Architects’Archive ソフトウェア開発の実践)
先日、『DDD(Domain-Driven Design)』の日本語版が出版されました。謝辞でも触れましたが、この本を翻訳するということは私にとっては目標かつ夢であり、それが実現できたことは自分にとっての1つのマイルストーンとなります。私がそこにたどり着けたのは、間違いなく多くの方々の助けによるものです。ただ、「自分では何をしたのか?」と考えてみると、継続的に勉強を続けつつ自分の立ち位置を考えていく上で、ブログを書き続けることの意味はとても大きかったように思います。


ブログを書く際には、「その記事を書く意味」をそれなりに意識してきたつもりです。そこで今回は、自分がこれまで意図的に採用してきたスタイルを整理してみます。これまで、このブログには技術ネタだけを極力客観的に粛々と書くようにしてきました。しかし、ブログを書く際に自分が考えてきたことを整理するという、ある意味舞台裏をさらすような記事も、これからブログを書こうとしている方、書き方に悩んでいる方にとっては、もしかしたら何かの役に立つかもしれないと思い、これを機にふりかえってみることにします。なお、一言補足します。「ブログの書き方」というタイトルにしてはいますが、ここにあるのは、あくまで「私が」採用しているスタイルです。「ブログたるものこのように書かなければならない」というものでは断じてありませんし、私自身も他の方の書いたものをこうした観点に照らして"評価"したことはありません。ただ、「私はこうしていますよ」という話です。

過去ログを見て頂ければわかる通り、私がブログを書き始めたのは、2005年のことでした*1。思想研究というバリバリの文系からソフトウェア開発に転向したということもあり、最初はとにかく体系的な知識を身につけなければと思っていました。そのために手っ取り早かったのが資格の勉強であり、ブログにも資格を取得した際にそのフィードバックを書いていました。「後から受ける人のために」と言えば聞こえはいいですが、自慢したかっただけかもしれません・・・。ということで、ブログの形式その1は「レポート」です。

レポート
試験、カンファレンス、セミナーの内容をまとめる。自分にとってのふりかえりになると同時に、そのカンファレンスに行けなかった人、あるいは試験であればこれから受ける人にとって、有益な情報源になります。カンファレンスやセミナーであれば、内容の要約だけでなく、自分の感想を、試験であれば、自分の使った参考書などを挙げて、「自分がどう捉えたか」を書くことで、情報は独自の価値を持つようになります。


ずっと受験報告だけを書いていた私が、記事っぽいものを書くようになったのは、2009年の1月からです。ただ、技術については勉強を始めたばかりでしたので、必然的に書くことよりも読むことが中心でした。RSSリーダに面白そうなブログのフィードを設定し、なるべく定期的に読むようにしました。そして、特に興味をひかれたものについて、最低でも月一で記事を書くようにしました・・・。ということで、ブログの形式その2は「要約・書評」です。

要約・書評
オリジナルのテキストを短くまとめ、コメントをつける。「元の記事が長い」あるいは「元の記事が英語である」、「本を一冊読む程時間が取れない」という方に対して、内容がコンパクトにまとまっていることは1つの価値になります。その際、著者の考え方と自分の考え方を明確に区別し、「こういうことが書いてある」「それについて自分はこう考える」をまとめることで、読む人にとってもわかりやすく、また単に短くしただけではない価値が生まれます。


いくつかの記事を読んでいるうちに、要約ではなくオリジナルをそのまま紹介したい、と思うものに出会うことがあります。元が日本語であれば、特にやるべきことはありませんが、元が英語であれば、それを日本語にすることにも価値があります・・・。ということで、ブログの形式その3は「翻訳」です。

翻訳
外国語で書かれた記事を日本語に翻訳する。著者に翻訳および公開の許可を得た上で、記事中で翻訳であることを明記し、オリジナルへのリンクを貼ることが必須です。最初はメールを送るのは怖いですが、たいていの相手はよろこんで承諾してくれます*2

この翻訳という行為は、やがて自分の中でかなり重要な位置を占めるようになってきました。


別の記事や書籍などで紹介されている技術を実際に動かしてみせるのも、情報としての価値があります。私の場合は、概念的なパラダイムを実装に落とすケースが多いですが、要素技術が得意な方であれば、新しい技術や製品を使って何かを作ってみるということも考えられるでしょう・・・。ということで、ブログの形式その4は「実装」です。

実装
実際に作ってみる。ただコードだけを公開するよりは、ある程度考え方なども合わせて紹介する方が、読む人が実際に手を動かす際の助けになりやすいと思われます。

ここまでは、ある特定の記事なり本なりを「1つ読んで1つ書く」というスタイルでした。そういうことを積み重ねていくうちに、同じことを別の視点から語っている人の存在に気づくようになりはじめます。ある題材に基づき、複数の記事を比較することで、その情報が立体的に浮かび上がってきます・・・。ということでブログの形式その5は「比較・列挙」です。

比較・列挙
ある題材について、複数の記事を並べることで、立体的に説明する。実は、私自身は意識してこのスタイルをとったことがありません。サンプルを挙げると、InfoQの記事はこのパターンが多いですね。

こうしたことを繰り返していると、なんとなく自分の立ち位置というか、自分の属する「クラスタ」のようなものがわかってきます。そうした領域においては、あるパラダイムなり考え方なりを自分なりの視点でとらえなおしてみたいと思うようになってきます・・・。ということで、ブログの形式その6は「解説」です。

解説
特定のパラダイムや考え方を自分の視点からとらえなおす。要約との違いは、著者の提示する枠組みをなぞるのではなく、別の視点も取り込みながら「再構成」する点にあると考えられます。

最近になってようやく、分野によってはこの種の語りができるようになってきました。


解説できるような分野に対する知見が深まるにつれて、既存のパラダイムに対する疑問が生まれて来ると思います。その疑問はやがて、新しい考え方へと結晶します。この考え方を人に語るためには、これまでに論じられてきたことを批判的に継承しつつ、新しい考え方を1つずつ立証していく作業が必要になります・・・。ということで、ブログの形式その7は「論証」です。

論証
自分の意見を立証する。すでに過去に語られていないことを証明するだけでも、いわゆる「先行研究調査」を膨大に行う必要があります。先人の偉業を1つ1つ整理した上で、その上に1つ石を積み上げるような作業と考えるべきです。いわゆる論文。

残念ながら、まだ私はこの段階には到達していません。

まとめ

すべての情報には、なんらかのかたちでその元となる情報があります。したがって情報の価値とは、その元となる情報との「距離」であると言えます。短くする、日本語にする、実装する、再構築する、そうやって、元となる情報との距離が生まれてくるわけです。「勉強はしたいけれど、どこから手をつけていいかわからない」という方は、「要約」から入るのがいいでしょう。私は英語の勉強を兼ねて、対象を英語のブログに限定していましたが、そうしなければならないものでもありません。技術書の読み方の記事でも5〜10冊と書きましたが、要約記事も5本〜10本くらい書き溜めるうちに、なんとなく自分がどういうものに興味を持っているのかが見えてくると思います。


もう1つ、これはブログに限らず色々な場面に当てはまると思うのですが、「間違いを犯すことを恐れない」ことは重要です。何かを言って「間違っている」と指摘された場合、そのやり取りも、それを知らなかった人にとっては情報としての価値を持ちます。そこで得られるのは、「こう考えるのが正しい」あるいは「こういう方向に考えるのは正しくない」という、ある意味間違えない限り得られないフィードバックです。また、こうしたフィードバックに情報としての価値を持たせるためには、「正しく訂正すること」も重要です。間違いを消して、なかったことにしてしまいたくなりますが、元の文は残した上で訂正するべきです*3


最後に、大切なのは続けること。これは、ゆっくりでもいいので、一定のペースを保つということです。そして何より、読んでくださる方々への感謝ですね。どうもありがとうございます。

*1:当時はココログでした。

*2:「あんたの記事超クールだよ、他の人にも紹介したいんだけど、翻訳していいかな?」と聞かれて、嫌な気持ちになる人はそういないでしょう。

*3:私もブログ上でちょいちょいやらかしています。最近ですと「関数型Scala(2):関数 - Mario Gleichmann - Digital Romanticism」の脚注など。

戦略的設計入門

"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:実際にそういうサービスが存在するかどうかは問題にしていません。あるかもしれませんし、ないかもしれません。