TDDの実践 〜TDDBC仙台レポート〜

2011年7月2日に開催されたTDDBC仙台のレポート。

導入

「TDD Boot Camp」通称TDDBCにはずっと参加したいと思っていたわけですが、今回仙台で機会を得ることができました。最初はJavaでと思っていたのですが、Scala組に入れて頂きまして、山中(@ymnk)さん、武田(@takedasoft)さんと3人でチームを組んでペアプロという貴重な体験をさせて頂きました(どうもありがとうございました!)。最終的には仕様変更2が何となくかたちができたところで時間切れとなりました*1


プログラムが組み上がっていく過程や、興味深いリファクタリング、うっかりテストを書かずにコードを修正してしまったことによるバグの埋め込み、モックを使ったタイマー処理の分離など、非常に興味深い体験を数多くさせて頂きましたので、ここにご報告させて頂きます。なお、作業中のコードは記憶を頼りに書いていますので、もしかすると動かない(コンパイルすらできない)可能性もありますが、その点はどうかご容赦ください(バージョン管理しておけばよかったと激しく後悔)。なお最終的なコードはこちらで公開しています。


なお、ネタバレを気にして和田さんに確認をとったのですが、「問題なし」とのこと。プロの仕事に頭が下がります。

前半

お題
LRU方式で、最も使われていないキーに紐づくデータを追い出すようなマップを実装する。

  • 特定のキーに対して値を設定/取得された場合に使われたとみなす

まずはテストから:

@Test
def マップに1つ値を設定するとそれが取得できる() = {
    val lru = new Lru()
    lru.put("KEY1", "VALUE1")
    assertEquals("VALUE1", lru.get("KEY1"))
}

コンパイルすら通りませんが、外側=使う側から設計していくのがTDDです。たいしたことのない実装に見えますが、このコードを書いた瞬間に、かなりの設計判断をしています。

  • クラスの名前はLruにする。
  • Mapと同じように、putで値を設定し、getで取得する。


まずはコンパイルを通るようにして(つまり、クラスとメソッドを空で作って)テストを実行すると、結果はレッドになります(getでnullが返りますので)。これを通すためのコードも簡単です。

class Lru {
    
    var key:String = _
    var value:String = _
    
    def put(key: String, value: String) = {
        this.key = key
        this.value = value
    }
    
    def get(key: String):String = {
        value
    }

おなじみの仮実装ですね(よくよく見ると、keyフィールドも要らなかったですね)。しかし、テストを実行させてグリーンになることを確認するのは大切です。自分たちがすくなくとも、テストの書き方と実行のさせ方をわかっていることが確認できるわけですから。こうして、これからの「黄金の回転」のための基礎ができあがります(この段階ではリファクタリングをやりませんでしたが、今思えば、「クラス名ってLruでいいんだっけ?」という議論をしてもよかったかもしれません)。


次に、この実装に対してレッドになるテストも簡単に書けます。

@Test
def マップに2つ値を設定するとそれが取得できる() = {
    val lru = new Lru()
    lru.put("KEY1", "VALUE1")
    lru.put("KEY2", "VALUE2")
    assertEquals("VALUE1", lru.get("KEY1"))
    assertEquals("VALUE2", lru.get("KEY2"))
}

もちろん、仮実装で粘ることもできるのですが、ここでフィールドをマップに変えました。

val map = new HashMap[String,String]

個人的には、TDDで一番"難しい"ポイントは「どのタイミングで最終的な実装に切り込むか」にある思っています。テストに通すだけなら、永遠に仮実装で凌ぎ続けることもできるわけです。ただ、もちろんそれでは実際の使用に耐えられません。重要なのは次の2点だと考えています。

  1. テストは無限な現実の1ケースであり、実装は抽象的に考える必要がある。
  2. テストをグリーンにするコードは、最終形の途中にある不完全なかたちになる。

この不完全なかたちを作る作業は、頭の中では先読みしつつちょっと手前でとめる作業になります。次にどうすればレッドにできるかわかっている状態でコードを書くことがあるということですね。また逆に言えば、テストをレッドにするという行為は、目指す最終形に対して抽象化/一般化できていない部分を突き崩すものであるべきなのです。


3つ目のテストでいよいよ、核心部に切り込むことになります。

@Test
def マップに3つ値を設定するとそれが最初がnull() = {
    val lru = new Lru()
    lru.put("KEY1", "VALUE1")
    lru.put("KEY2", "VALUE2")
    lru.put("KEY3", "VALUE3")
    assertEquals(null, lru.get("KEY1"))
  }

保持しておくキーの数に特に指定はなかったのですが、とりあえず「2つ持つ」という仕様で作業を始めました。これはある意味現実を抽象化し切れていない部分ですので後で修正することにして、先にコアの実装をやっつけます。


完成形に至る不完全なかたちとしては、「とりあえず受けとった順にキーをリストに詰めていって、3つ以上あったら先頭から消していく」という実装にしました。

class Lru {
    
    val map = new HashMap[String,String] 
    var keystack = new ListBuffer()
    
    def put(key: String, value: String) = {
        keystack += key
        if (keystack.size > 2) {
            val removeKey = keystack.remove(0)
            map.remove(removeKey)
        }
        map += key -> value
    }

これでテストはグリーンになりますが、バグがあることはすでにわかっています。同じキーを連続していれたら他のものを追い出してしまうんですね。そこを突いてレッドにするテストを書きます。

@Test
def マップに2つの値を繰り返しいれて取得できる() = {
    val lru = new Lru()
    lru.put("KEY1", "VALUE1")
    lru.put("KEY2", "VALUE2")
    lru.put("KEY1", "VALUE1")
    lru.put("KEY1", "VALUE1")
    assertEquals("VALUE2", lru.peek("KEY2"))
    assertEquals("VALUE1", lru.peek("KEY1"))
}

前の実装だと、keystackの中身が["KEY1","KEY1"]になってしまうので、"KEY2"が追い出されてレッドになります。そこでコードを修正します。「keystackの中にもうすでにキーが入っていたら、keystackをいじるのをやめよう!」if文で囲むだけですね。

class Lru {
    
    val map = new HashMap[String,String] 
    var keystack = new ListBuffer()
    
    def put(key: String, value: String) = {
        if(!keystack.contains(key)) {
            keystack += key
            if (keystack.size > 2) {
                val removeKey = keystack.remove(0)
                map.remove(0)
            }
        }
        map += key -> value
    }

たしかこのあたりで、「Scalaなんだし、nullを返すのはやめようよ」というリファクタリングが入りました。さて実装自体は一見良さそうに見えたのですが、まだレッドにできます。

@Test
def 値を入れ直した場合に順序が更新される() = {
    lru.put("KEY1", "VALUE1")
    lru.put("KEY2", "VALUE2")
    lru.put("KEY1", "VALUE1") // ★
    lru.put("KEY3", "VALUE3")
      
    assertEquals(Some("VALUE1"), lru.peek("KEY1"))
    assertEquals(None, lru.get("KEY2"))
    assertEquals(Some("VALUE3"), lru.peek("KEY3"))
}

"KEY1"に対応する値がNoneであるということで、レッドになってしまいます。理由は「★」の場所でkeystackの中身が["KEY1","KEY2"]のまま変わらないからですね。["KEY2","KEY1"]にしないといけません。「keystackの中から消して、後ろにつければいいじゃないか」ここでScalaっぽさが炸裂します。

def put(key: String, value: String) = {
    keystack = stack.filter( _ != key ) // ★
    keystack += key // ★
    if (keystack.size > 2) {
        val removeKey = keystack.remove(0)
        map.remove(0)
    }
    map += key -> value
}

これでグリーンになります。よくよく見ると、keystackに対する2つの操作(★)は1行にできますね(実際にこのリファクタリングを行ったのはもうすこし後のことでしたが)。

keystack = stack.filter( _ != key ) + key

一瞬完成したかと思ったのですが、ここで1つ忘れていたことを思いだしました。「getの時も使ったって考えるんだよね?」

@Test
def 値を取り出した場合に順序が更新される() = {
    lru.put("KEY1", "VALUE1")
    lru.put("KEY2", "VALUE2")
    lru.get("KEY1") // ★
    lru.put("KEY3", "VALUE3")
      
    assertEquals(Some("VALUE1"), lru.get("KEY1"))
    assertEquals(None, lru.get("KEY2"))
    assertEquals(Some("VALUE3"), lru.get("KEY3"))
}

もちろん★の部分でkeystackの中身が更新されないので、"KEY1"が取得できず、テストはレッドになります。実装はそれほど難しくありません。putの先頭でkeystackをいじっているところをprivateメソッドに切り出し、getの先頭でも呼ぶようにしました。

def put(key: String, value: String) = {
    touch(key)
    map += key -> value
}
    
def get(key: String): Option[String] = {
    touch(key)
    map.get(key)
}

これで完成だろうとドヤ顔でテストを実行したところで、和田さんの準備した落し穴に顔面から突き刺さりました。「いままで通っていたテストが通らなくなった!なんだこれは?」こういうとき、原因に一瞬で気づく人がいるのが強力なチームのよいところ。「Assertでgetするときにも順番が変わっちゃうね」


状態を変更しないで値が取れるメソッドpeekを準備して、テストをすべて書き換えました。

def peek(key: String): Option[String] = map.get(key)

「これで要件を満たしただろう」と考えて、ちょっとしたリファクタリングを行いました。keystackを触る処理をメソッドではなくプライベートクラスとして抽出したのです。

class Lru {
    
    val map = new HashMap[String,String] 
    var keystack = new KeyStack()

    def put(key: String, value: String) = {
        keystack.touch(key)
        map += key -> value
    }

    [...]

    class KeyStack {
        
        var stack = ListBuffer[String]]()

その上で、先ほどのペンディング事項「キャッシュサイズは変えられるようにしないと」を実装しました。

@Test
def キャッシュサイズを3に変更して3つの値を出し入れできる() = {
    val lru = new Lru(3)
    [...]

Scalaのデフォルト引数機能を使うことで、これまでのテストは触らなくてすみます。

class Lru(var cacheSize:Int = 2) {

ここで前半が終了しました。


なお、どこかのタイミングでテストのリファクタリングを行い、毎回のnew Lru()をテストフィクスチャに追い出しています。

var lru: Lru = _
 
@Before
def setup = {
    lru = new Lru();
}

コードレビュー

コードレビューの最中、Ruby組の方から驚きのバグが報告されました。「入っていない値をgetしたときにスタックの順序が狂ってしまいますね」自分たちのコードのバグとしての気づきでしたが、我々のコードにも同様の問題がありました。和田さんの準備した落し穴に、気づかぬうちに落ちていたわけです。


ここでまたしても重要だと思うのが、テストは現実のさまざまなケースをパターン化し、具体的にしたものです。「本当にこれまでのテストですべてのパターンを網羅できているか?」「気づいていない操作パターンはないか?」そういうことを考えることが非常に重要だということですね。「レッドにする」ことは時として非常に高度な技術を要します。

後半:仕様変更

ここで和田さんから(うれしそうに)仕様変更のお知らせが入りました。

仕様変更

  1. キャッシュのサイズを後から変更できるようにしたい。
  2. 時間が経ったら消えるようにしたい。
  3. スレッドセーフにしてほしい。


後半のセッションの開始時、「存在しないものに対するget」のバグが悔しかった僕は、ついうっかりコードを先に書いてしまいました(和田さんが隣にいたらものすごく怒られたところです。危ない危ない)。

if(!map.contains(key)) None

書いた後で気がつきます。「ごめんね、和田さん」反省して、テストコードを書きました。

@Test 
def 値が入っていないものをGetしても状態は変わらない() {
    lru.put("KEY1", "VALUE1")
    lru.put("KEY2", "VALUE2")
    lru.get("KEY3")
      
    assertEquals(None, lru.peek("KEY3"))
    assertEquals(Some("VALUE1"), lru.peek("KEY1"))
    assertEquals(Some("VALUE2"), lru.peek("KEY2"))
}

しかし、レッド。getで相変わらず状態が変わってしまっています。しばらく眺めて気がつきました。メソッドの途中ではreturnが省略できないんですよね。

if(!map.contains(key)) return None

テストは先に書かなければいけないし、すくなくとも実装したものはテストしないといけないということですね*2


仕様変更1でちょっと難しいところは、キャッシュサイズを減らした場合にkeystackを削らなければいけない点にあります。この「削り」に関して、またしてもScalaのかっこいいAPIが炸裂します(Scala勉強会@東北のページを紹介して頂きました)。

    if (stack.size > cacheSize) {
        for(i <- stack.dropRight(cacheSize)){
            map.remove(i._1)           
        }
        stack = stack.takeRight(cacheSize)          
    }

あふれている分を取得(dropRight)してマップから削除し、stackから必要なところだけ取り出して(takeRight)再代入します。


これで、テストはグリーンになります。ここで、すこし大きめのリファクタリングを行いました。仕様変更2で結構大きな変更が想定されたので、それに耐えられるようにこのタイミングできれいにしておきたかったのです。


今書いたコードと、今まで使っていたこのコード:

if (keystack.size > cacheSize) {
    val removeKey = keystack.remove(0)
    map.remove(0)
}

「やっていることはすごく似ていないだろうか?」すこし考えて、「どちらの場合もcacheSizeに合わせて、keystackとmapを削っているだけだ」ということに気づきました。今まで使っていたコード(すぐ上)は、新しく書いたコード(2つ上)の特殊ケースにすぎないのです。それをまとめた結果、コードはかなりすっきりしました。

def touch(key: String) = {
    stack = stack.filter( _ != key ) + key
    shrinkIfNeeded()
}

def changeSize(size: Int) = {
    cacheSize = size
    shrinkIfNeeded()
}

private def shrinkIfNeeded() = {
    if (stack.size > cacheSize) {
        for(i <- stack.dropRight(cacheSize)){
            map.remove(i)           
        }
        stack = stack.takeRight(cacheSize)          
    }
}

実は、午前中に行った「KeyStackクラスの作成」というリファクタリングの恩恵に、ここであずかることができています。この変更はKeyStackクラスの中に閉じており、外側のLru本体には影響を与えていません。


さて、仕様変更2です。

関心事の分離

「古いものを消す」という仕様を入れるためには、時間の概念を導入しなければなりません。スレッドを新しく走らせる必要もあるでしょう。これをいままでのクラスに入れたくはなかったですし、テストコードも書きにくくなってしまいます。そこで、テストコードを書く前に図を書いて、設計の検討を行いました。

TimerとLruとの間にはRemovable(これも名前の検討は必要ですが)というインターフェイスを立てます。Timerのテスト(=時間の概念が入る)は、Removableをモック化して定期的にメッセージが送られていることを確認します。Lruのテストは直接Removableに定義されたメソッドを呼び出し、正しくふるまっていることを確認します。


Timerのテスト時にモックが必要になります。本当はjMockやMockitoのようなモックフレームワークがあるとよかったのですが、限られた時間の中で(しかもScala)で動かす自信がなかったので、プライベートクラスとしてごりっと実装してしまいました。

@Test
def 定期的に削除メッセージを送信する() {
    val mock = new MockRemovable()
    val timer = new LruTimer(mock)
        
    timer.start();
        
    Thread.sleep(15000)
        
    assertTrue("呼び出されている", mock.called)
}
    
private class MockRemovable extends Removable {
    var called = false;
    def remove(time:Date) {
        called = true;
    }
}

ここに重要な設計が1つあって、RemovableのメソッドがDateオブジェクトを受けとるものであることを定めています。本体はこうなりました。

class LruTimer(removable: Removable) extends Thread{
  override def run() = {
      while(true) {
          Thread.sleep(10000)
          removable.remove(new Date())
      }
  }
}

タイマーができたところで、Lru本体に取りかかります。もちろん、テストコードから。

@Test
def ある時点の前のキャッシュを削除する() {
    lru = new Lru(3) // 今見るといらないですね
    lru.put("KEY1", "VALUE1")
    lru.remove( new Date(System.currentTimeMillis() + 10000) )
    assertEquals(None, lru.peek("KEY1"))
}

「removeに渡されたDateよりも前に使われたものは消そう」と決めました。まずは、keystackに時間の概念を導入します(このとき、Lruクラスには影響がなく、変更はすべてプライベートクラスであるKeystackクラスに閉じています)。

var stack = ListBuffer[Tuple2[String,Date]]()

def touch(key: String) = {
    stack = stack.filter( _._1 != key ) + ( key -> new Date )
    shrinkIfNeeded()
}

ご覧のとおり、stackにタッチするときに、タイムスタンプを設定しています(new Dateはアレですが、それはリファクタリングできれいになるでしょう)。そして、removeが呼ばれたときにそのタイムスタンプを見て、古いものを削除することになります。

def removeBefore(time:Date) = {
    for( i <- stack.filter( ! _._2.after(time) ) ){
        map.remove(i._1)
    }
    stack = stack.filter( _._2.after(time) )
}

このあたりでタイムアップとなりました。


こうしたインターフェイスとモックを使ったTDDのやり方については、「Growing Object-Oriented Software, Guided by Tests (Addison-Wesley Signature Series (Beck))」で詳しく説明されています。講演の中で和田さんがモダンTDDのバイブルとして位置づけていた本ですね。

補足

TimerとLruを分割して個別のテストから書き始めましたが、2つを組み合わせて使おうと思うと、もう一工夫必要です。今のままで素直にファクトリメソッドを書こうと思うと:

// 試しに書いてみると・・・
def createLru():Lru = {
    val lru = new Lru()
    val timer = new LruTimer(lru)
    timer.start() // で、このタイマーのインスタンスは誰が管理するの?
    lru
}

という感じになってしまいます。「外側から内側へ」という発想を常に忘れてはいけないという教訓を示す好例です。しかも、Removableインターフェイスの仕様を事前に明確に定めていなかったため、現段階ではちょっとしたバグがあります。一定時間ごとにその瞬間より前のキャッシュが消えてしまうんですね。実際には、「○○分/秒前の値を消す」という考慮が必要でしょう。


もちろん、どちらのケースに対してもTDDのサイクルを使って切り込んでいくことができます。まずはレッドになるコード(もしかしたら、コンパイルもできないコード)を書くところから。

まとめ

TDDBC仙台で窓際にいたScala組の歩みをそれなりに正確に再現できたのではないかと思います。もちろん、すべてがこんなにスムーズに行ったわけではなく、僕が「あれ、この場合for文ってどう書くんでしたっけ?ちょっとお願いします!」とかいってキーボードを渡したシーンもありましたし、「あれ?なんで通らないんだ?」とデバッグコードを埋め込んでみたシーンもありました。逆に、想定外の落ち方をしたときにその原因にすぐ気づいてもらえたり、タイムスタンプの実装で「keystackの中身をタプルにすればいいよね」と一瞬でコードを書いてもらえたりと、強力な方々とペアプロをすることの生み出す豊かさを随所で感じることができました。非常に贅沢な時間をすごさせていただいたことを、チームを組ませて頂いた山中さん、武田さんを始め、講師の和田さん、そして開催に尽力してくださった方々に深く感謝します。まだ体験したことがないという方は、ぜひ参加されるといいと思いますよ!


最後にもう一度、TDDの黄金の回転について整理したいと思います。

レッド
「失敗するテストを書く」これは新しいパターンのテストケースを書く作業であると同時に、インターフェイスを設計する作業でもあります。いかに適切な粒度で最終形との距離を縮めていくか。多分ここに、TDDのエッセンスが凝縮されているんじゃないかと思います。
グリーン
グリーンにすること自体は、おそらく最も純粋な「プログラミング」の作業です。これが適切に行うためには、言語やアルゴリズムに対する知識が必要になりますし、それはそれで身につけるべきものです。1つつけ加えると、コードを書いていると「どのみちこれではこういう場合に落ちるな」ということに気づくことがあります。それが次のレッドの種になるわけですから、本当は手元に紙を置いておいて、書き留めてみるといいのかもしれません。
リファクタリング
リファクタリング」というと、コードを修正しないといけないような気になりますが、多分そんなことはありません。グリーンになったところでコードを見直し、「これで大丈夫かな?」と次のレッドの種を探したり、あらためて構造を頭の中で整理してみたり、言葉に出して確認してみたり、という作業が非常に有効だということに気づきました。コードを修正するかどうかは、結果にすぎないということですね。


ひどく頭を使ったせいか、終わった後は予想以上に消耗していました。XPには「40Hours / a Week」ルールがありますが(残業は週40時間まで、じゃないですよ)。このレベルで日々の仕事をやっていたら、残業なんてとてもじゃないけど身体がもちません。



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

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

プログラミングScala

プログラミングScala

*1:時間内ではテストが1つ通らなかったのですが、家に帰って見てみたら、変数の再代入が原因のなんてことないバグでした。公開しているコードでは修正しています。

*2:経験ありませんか?ちょろっとバグを直して、テストをしないでいたら、あとですげー怒られることになったという。

翻訳に必要な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」の脚注など。