Applicative wordcount 

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

EIP:

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

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

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

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

scala> import cats._, cats.data._, cats.implicits._
import cats._
import cats.data._
import cats.implicits._

scala> import Func.{ appFunc, appFuncU }
import Func.{appFunc, appFuncU}

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

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

scala> type Count[A] = Const[Int, A]
defined type alias Count

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

scala> def liftInt(i: Int): Count[Unit] = Const(i)
liftInt: (i: Int)Count[Unit]

scala> def count[A](a: A): Count[Unit] = liftInt(1)
count: [A](a: A)Count[Unit]

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

scala> val countChar: AppFunc[Count, Char, Unit] = appFunc(count)
countChar: cats.data.AppFunc[Count,Char,Unit] = cats.data.Func$$anon$6@5100269

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

scala> val text = ("Faith, I must leave thee, love, and shortly too.\n" +
                  "My operant powers their functions leave to do.\n").toList
text: List[Char] =
List(F, a, i, t, h, ,,  , I,  , m, u, s, t,  , l, e, a, v, e,  , t, h, e, e, ,,  , l, o, v, e, ,,  , a, n, d,  , s, h, o, r, t, l, y,  , t, o, o, .,
, M, y,  , o, p, e, r, a, n, t,  , p, o, w, e, r, s,  , t, h, e, i, r,  , f, u, n, c, t, i, o, n, s,  , l, e, a, v, e,  , t, o,  , d, o, .,
)

scala> countChar traverse text
res0: Count[List[Unit]] = Const(96)

うまくいった。

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

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

scala> val countLine: AppFunc[Count, Char, Unit] =
         appFunc { (c: Char) => liftInt(testIf(c === '\n')) }
countLine: cats.data.AppFunc[Count,Char,Unit] = cats.data.Func$$anon$6@7e8afb9d

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

scala> countLine traverse text
res1: Count[List[Unit]] = Const(2)

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

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

scala> val countWord =
         appFuncU { (c: Char) =>
           import cats.data.State.{ get, set }
           for {
             x <- get[Boolean]
             y = !isSpace(c)
             _ <- set(y)
           } yield testIf(y && !x)
         } andThen appFunc(liftInt)
countWord: cats.data.AppFunc[[γ$3$]cats.data.Nested[[X]cats.data.StateT[cats.Eval,Boolean,X],Count,γ$3$],Char,Unit] = cats.data.Func$$anon$6@2a6e6330

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

scala> val x = countWord traverse text
x: cats.data.Nested[[X]cats.data.StateT[cats.Eval,Boolean,X],Count,List[Unit]] = Nested(cats.data.StateT@62f02f86)

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

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

17 words だ。

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

scala> val countAll = countWord product countLine product countChar
countAll: cats.data.AppFunc[[α]cats.data.Prod[[α]cats.data.Prod[[γ$3$]cats.data.Nested[[X]cats.data.StateT[cats.Eval,Boolean,X],Count,γ$3$],Count,α],Count,α],Char,Unit] = cats.data.Func$$anon$6@1a7c722

scala> val allResults = countAll traverse text
allResults: cats.data.Prod[[α]cats.data.Prod[[γ$3$]cats.data.Nested[[X]cats.data.StateT[cats.Eval,Boolean,X],Count,γ$3$],Count,α],Count,List[Unit]] = Prod(Prod(Nested(cats.data.StateT@39a34574),Const(2)),Const(96))

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

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

scala> val wordCountState = allResults.first.first
wordCountState: cats.data.Nested[[X]cats.data.StateT[cats.Eval,Boolean,X],Count,List[Unit]] = Nested(cats.data.StateT@39a34574)

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

EIP:

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

今日はここまで。

Contents