1. Applicative wordcount

Applicative wordcount 

EIP 6節、「アプリカティブ・ファンクターを用いたモジュラー・プログラミング」まで飛ばす。

EIP:

アプリカティブ・ファンクターには他にもモナドに勝る利点があって、 それは複雑な反復をよりシンプルなものからモジュラーに開発できることにある。 ….

Unix でよく使われる wordcount ユーティリティである wc を例にこれを説明しよう。 `wc` はテキストファイルの文字数、語句数、行数を計算する。

この例は完全にアプリカティブ関数の合成を使って翻訳することができるけども、 この機能は現在私家版のブランチのみで公開されている。 (PR #388 は審査中)

アプリカティブなモジュラー反復 

import cats._, cats.data._, cats.syntax.all._
import Func.appFunc

wc プログラムの文字数のカウント部分は「モノイドとしての Int」のアプリカティブ・ファンクターを累積した結果となる:

以下は Int をモノイダル・アプリカティブとして使うための型エイリアスだ:

type Count[A] = Const[Int, A]

上のコードでは、A は最後まで使われないファントム型なので、Unit に決め打ちしてしまう:

def liftInt(i: Int): Count[Unit] = Const(i)
def count[A](a: A): Count[Unit] = liftInt(1)

この反復の本体は全ての要素に対して 1 を返す:

lazy val countChar: AppFunc[Count, Char, Unit] = appFunc(count)

この AppFunc を使うには、traverseList[Char] と共に呼び出す。 これは Hamlet から僕が見つけてきた引用だ:

lazy val text = ("Faith, I must leave thee, love, and shortly too.\n" +
           "My operant powers their functions leave to do.\n").toList

countChar traverse text
// res0: Count[List[Unit]] = Const(getConst = 96)

うまくいった。

行数のカウント (実際には改行文字のカウントなので、最終行に改行が無いと、それは無視される) も同様だ。 違いは使う数字が違うだけで、それぞれ改行文字ならば 1、それ以外は 0 を返すようにする。

def testIf(b: Boolean): Int = if (b) 1 else 0

lazy val countLine: AppFunc[Count, Char, Unit] =
  appFunc { (c: Char) => liftInt(testIf(c === '\n')) }

これも、使うには traverse を呼び出す:

countLine traverse text
// res1: Count[List[Unit]] = Const(getConst = 2)

語句のカウントは、状態が関わってくるため少しトリッキーだ。 ここでは、現在語句内にいるかどうかを表す Boolean 値の状態を使った State モナドを使って、 次にそれをカウントするためのアプリカティブ・ファンクターに合成する。

def isSpace(c: Char): Boolean = (c === ' ' || c === '\n' || c === '\t')

lazy val countWord =
  appFunc { (c: Char) =>
    import cats.data.State.{ get, set }
    for {
      x <- get[Boolean]
      y = !isSpace(c)
      _ <- set(y)
    } yield testIf(y && !x)
  } andThen appFunc(liftInt)

AppFunc を走査するとこれは State データ型が返ってくる:

val x = countWord traverse text
// x: Nested[IndexedStateT[Eval, Boolean, Boolean, A], Count[A], List[Unit]] = Nested(
//   value = cats.data.IndexedStateT@63327bc9
// )

この状態機械を初期値 false で実行すると結果が返ってくる:

x.value.runA(false).value
// res2: Count[List[Unit]] = Const(getConst = 17)

17 words だ。

shapecontent でやったように、アプリカティブ関数を組み合わせることで走査を 1つに融合 (fuse) できる。

lazy val countAll = countWord
  .product(countLine)
  .product(countChar)

val allResults = countAll traverse text
// allResults: Tuple2K[Tuple2K[Nested[IndexedStateT[Eval, Boolean, Boolean, A], Count[A], γ3], Count[A], α], Count[A], List[Unit]] = Tuple2K(
//   first = Tuple2K(
//     first = Nested(value = cats.data.IndexedStateT@211781c8),
//     second = Const(getConst = 2)
//   ),
//   second = Const(getConst = 96)
// )

val charCount = allResults.second
// charCount: Count[List[Unit]] = Const(getConst = 96)

val lineCount = allResults.first.second
// lineCount: Count[List[Unit]] = Const(getConst = 2)

val wordCountState = allResults.first.first
// wordCountState: Nested[IndexedStateT[Eval, Boolean, Boolean, A], Count[A], List[Unit]] = Nested(
//   value = cats.data.IndexedStateT@211781c8
// )

val wordCount = wordCountState.value.runA(false).value
// wordCount: Count[List[Unit]] = Const(getConst = 17)

EIP:

アプリカティブ・ファンクターはより豊かな合成演算子を持つため、 多くの場合モナド変換子をリプレースすることができる。 また、アプリカティブはモナド以外の計算も合成できるという利点もある。

今日はここまで。