エムスリーテックブログ

エムスリー(m3)のエンジニア・開発メンバーによる技術ブログです

社内輪読会でScala関数型デザイン&プログラミング第14章の練習問題を解いたので解説します

エンジニアリンググループの松原です。 社内で4月から続いているScala関数型デザイン&プログラミング輪読会で、14章を担当したので復習も兼ねてその練習問題について解説記事としてまとめます。 ちなみにこの輪読会ですが、14章に到達した現在も誰一人として脱落することなく続いております(2人が USA 出張、1人が育休なのですが、これは仕方がないので脱落と呼ばないことにします)。 私を含めちらほら心を折られている人がいるような気配もありますが、Scala に強い人が参加していることもあって何とか続けられています。

f:id:ma2gedev:20181207205340j:plain
5月くらいの輪読会。関数型について話が盛り上がっている様子(なんか楽しそう)。

なお、この記事は エムスリーアドベントカレンダー 9 日目の記事です。

14章の練習問題について

できればお手元に本書があるとよいのですが、お持ちでない方のために少しだけ14章についての補足を書きます。

この本では1章からずっと副作用のない関数のことを純粋関数として話を進めてきたのですが、 14章ではミュータブルな処理(副作用)があったとしても、使う側から触れられなければ、純粋関数であるということを学びます。 そこで最初に出てくる例が 14.1 節のミュータブル版 quicksort になります。

上記のリンクからコードを読んでいただくと分かるように、ミュータブルな配列 arr を直接並び替える操作が行われ副作用が発生していますが、 quicksort を使う側からすると全ての副作用は quicksort 内部のローカルスコープで行われているため触れられず純粋な関数となっています。

続く 14.2 節ではこのミュータブル版の quicksort のようにローカルスコープを用いて作用の範囲を決める方法もあるが、 Scala の型システムを使って作用のスコープを適用することもできると話が進みます。 この作用のスコープを適用するために定義されるものが、局所作用モナド ST、ミュータブルな参照の代数 STRef、ミュータブルなアクションの実行のための RunnableST、ミュータブルな配列 STArray の4つです。 それぞれにリンクを貼っているので、詳細のコードはそちらをご覧ください。 これらがあることを前提に14章の練習問題は作られています。

それでは以下14章にある3問の練習問題をそれぞれ解説していきます。また私の解答は fpinscala *1リポジトリの answers ディレクトリも参考にして書いております。

練習問題1(書籍中では EXERCISE 14.1)

まずは 14.2.4 項にある EXERCISE 14.1 からです。

14.2.4 項で作ったミュータブルな配列を扱う STArray 型に、Map のデータを配列に書き込むコンビネータ def fill(xs: Map[Int,A]): ST[S,Unit] を追加する問題です。 私の解答は以下の通りです。

sealed abstract class STArray[S,A](implicit manifest: Manifest[A]) {
  // ...省略...
  def fill(xs: Map[Int,A]): ST[S,Unit] = {
    xs.foldRight(ST[S,Unit](())) {
      case ((k, v), st) => st flatMap (_ => write(k, v))
    }
  }
  // ...省略...
}

引数 xs に渡ってきたマップの全キーバリューペアを配列に設定するため foldRight で畳み込みます。 アキュムレーターとして ST[S,UNIT](()) を渡すことで、STArraywrite へキーとバリューを渡すことで返される ST を結合した結果を返すことができています。

練習問題2(EXERCISE 14.2)

次は 14.2.5 項にある EXERCISE 14.2 ですが、今度はミュータブル版ではなくイミュータブルな quicksortST などを使い構築します。 書籍中では予め STArray 用の swap と、イミュータブル版の quicksort 関数の中身は定義された状態で、quicksort 関数から呼ばれる partitionqs の純粋関数型バージョンを追加する問題です。

解答は以下になります。

object Immutable {
  def noop[S] = ST[S,Unit](())

  def partition[S](a: STArray[S,Int], l: Int, r: Int, pivot: Int): ST[S,Int] = for {
    pivotVal <- a.read(pivot)
    _ <- a.swap(pivot, r)
    j <- STRef(l)
    _ <- (l until r).foldLeft(noop[S])((s, i) => for {
      _ <- s
      curVal <- a.read(i)
      _ <- if (curVal < pivotVal) (for {
        jVal <- j.read
        _ <- a.swap(i, jVal)
        _ <- j.write(jVal + 1)
      } yield ()) else noop[S]
    } yield ())
    resultPos <- j.read
    _ <- a.swap(resultPos, r)
  } yield resultPos

  def qs[S](a: STArray[S,Int], l: Int, r: Int): ST[S, Unit] = if (l < r) for {
    pi <- partition(a, l, r, l + (r - l) / 2)
    _ <- qs(a, l, pi - 1)
    _ <- qs(a, pi + 1, r)
  } yield () else noop[S]

  def quicksort(xs: List[Int]): List[Int] =
    if (xs.isEmpty) xs else ST.runST(new RunnableST[List[Int]] {
      def apply[S] = for {
        arr    <- STArray.fromList(xs)
        size   <- arr.size
        _      <- qs(arr, 0, size - 1)
        sorted <- arr.freeze
      } yield sorted
  })
}

基本的には14.1節に出てくるミュータブル版の quicksort 実装にある partition および qs を、 STArray および STRef を用いて書き直していく作業となるので、コードを比較して見ていくとわかりやすいです。 解答を見て私が気になったことは以下の3点です。

  • qs, partition の第1引数に STArray が追加されている
    • ローカルスコープを使うわけではないので、このように STArray を引き回しています
  • 元のコードで if に対する else 節がない箇所でも、else が必要になっている
    • これは ST を結合していく必要があるため、何もしない処理である noop[S] 即ち ST[S,Unit](()) を返す必要があります
  • 元のコードにある partitionfor による繰り返し処理が foldLeft になり、表記も煩雑化
    • 元のコードにある for で繰り返されている処理を、ST として結合するために foldLeft に変更する必要があります
    • 値の読み込みなども STRefread を経由して ST にする必要があり結果煩雑になっています

14.2節の注釈にもありますが、効率も表記の簡便性もともに大きく落ちていますね。 Scala の型システムを使うことでこういった手段も取れるという事例なので、これが有効なパターンがあるときに使うのが望ましそうです。

練習問題3(EXERCISE 14.3)

3問目は scala.collection.mutable.HashMap について、STRefSTArray と同じように STMap を実装しましょうという問題です。 ハッシュマップを作成して操作するための最小限のプリミティブコンビネータを考える問題なのですが、 私の時間がなかったこともあり輪読会では必要なインターフェースについてはリポジトリの解答を見てから実装の中身だけを考えました。 解答によると最小限というのは、STMapsize, apply, get, +=, -=, empty, fromMap を実装したものということで、 HashMap の生成ができ、取得や追加、削除など一通りの操作ができるものを指すようです。 これらインターフェースの型をもとに実装したものが以下となります。

import scala.collection.mutable.HashMap

sealed trait STMap[S,K,V] {
  protected def map: HashMap[K,V]

  def size: ST[S,Int] = ST(map.size)

  // Get the value under a key
  def apply(k: K): ST[S,V] = ST(map(k))

  // Get the value under a key, or None if the key does not exist
  def get(k: K): ST[S,Option[V]] = ST(map.get(k))

  // Add a value under a key
  def +=(kv: (K, V)): ST[S,Unit] = {
    ST(map += kv)
  }

  // Remove a key
  def -=(k: K): ST[S,Unit] = {
    ST(map -= k)
  }
}

object STMap {
  def empty[S,K,V]: ST[S, STMap[S,K,V]] =
    ST(new STMap[S,K,V] {
      lazy val map = HashMap.empty[K,V]
    })

  def fromMap[S,K,V](m: Map[K,V]): ST[S, STMap[S,K,V]] =
    ST(new STMap[S,K,V] {
      lazy val map = HashMap.empty[K,V] ++ m
    })
}

こちらの実装は STArray を参考に対応すれば良いので、そこまで難しくないと思います。 求められている STMap 型のインターフェースに合うように、HashMap のインターフェースを繋げるようなイメージで作ることができます。 console で試しに使ってみると以下のようになります*2

scala> val p = new RunnableST[(Option[String], String, String)] {
     |   def apply[S] = for {
     |     map <- STMap.fromMap(Map(1 -> "x", 2 -> "y"))
     |     _ <- map += (3 -> "z")
     |     _ <- map -= (1)
     |     val1 <- map.get(1)
     |     val2 <- map(2)
     |     val3 <- map(3)
     |   } yield (val1, val2, val3)
     | }
p: fpinscala.localeffects.RunnableST[(Option[String], String, String)] = $anon$1@361e138

scala> val v = ST.runST(p)
v: (Option[String], String, String) = (None,y,z)

まとめ

ということで14章についてはこの3問が練習問題でした。本書後半の方にしては比較的分量も少なめの章なので、何とか担当しきることができたと思います。

4月に始まったこの輪読会も残すところ15章のみとなりました。 自分の担当があることで担当分をやらざるを得ませんし、なんとか続けることができたかなと思います。 完走に向けて後もう少し頑張ります。

参考リンク

  • fpinscala/fpinscala: Code, exercises, answers, and hints to go along with the book "Functional Programming in Scala" https://github.com/fpinscala/fpinscala
    • Scala関数型デザイン&プログラミングの練習問題、解答、ヒントが掲載されています。これがあることで全く解答が分からない場合でも、解答のコードをもとに理解を進めることができたりするので助かりました。
  • 書籍: FP in Scala を100倍楽しむ方法 | DevelopersIO https://dev.classmethod.jp/server-side/scala/how_to_read_the_fp_in_scala/
    • 輪読会をするにあたって実行環境を作成するために参考にしました。学んで、手を動かして、結果を見てというプロセスを回せて理解がかなり深まりました。
  • なぜfoldRightが美しいのか - エムスリーテックブログ https://www.m3tech.blog/entry/2018/07/25/162459
    • この輪読会を主催する冨岡の記事です。こちらの記事でも輪読会の雰囲気が書かれています。よろしければご覧になってください。

We're Hiring

エムスリーでは、共に医療の課題を解決していく仲間を募集中です。 社内勉強会や輪読会に参加したい方も、自分が開催したいという方も、ぜひエムスリーも検討していただけたらと思います。 カジュアル面談や Tech Talk への参加も歓迎しておりますので、以下フォームよりご応募お待ちしております。

*1:本書に対応するリポジトリで練習問題、ヒント、解答が一通り揃っています

*2:あまりクールなサンプルじゃないのでセンスが問われる...