2012年8月9日木曜日

クラウド温泉3.0 (17) / SQL

クラウド温泉3.0@小樽のセッション「Monadicプログラミング・マニアックス」で使用するスライドのネタ検討その17です。

「Monadicプログラミング・マニアックス」のネタを要素技術のボトムアップで積み上げてきましたが、なかなか本丸に到達しないままクラウド温泉も来週末に迫って来ました。

来週はお盆休みでブログはお休みする予定なので、今日と明日の二回は応用からのトップダウンでネタを整理したいと思います。

SQLと関数型言語

関数型プログラミンをやっていて感じるのは、SQLなどの問い合わせ言語と関数型プログラミングの相性がとてもよいことです。現状はScalaとSQLが直接接続できるというわけではありませんが、データフローの実現で関数型プログラムとSQLを組合せる場合、同じセマンティクスの土俵の上で一気通貫に処理を考えることができます。これは一般的に問い合わせ言語が一種の関数型言語であるのが理由だと思います。その上で、プラットフォーム上の制約を考えて実装をプログラムとSQLとに振り分けるという形になります。

このアーキテクチャ上ではORMはちょっと中途半端な感じで、問題を複雑化させる要因になるかと思います。

現状のORMの問題の一つは、実現がER図的な共通ドメイン・モデル側に寄りすぎていて、ユースケース・スコープのドメイン・モデルはうまくハンドリングできない点です。この問題も関数型プログラミング&SQLでうまくさばくことができます。

またORMの実現方式にもよりますが以下のような問題もあります。

  • 性能チューニングや処理の記述でSQLの力を十分に活かせない。
  • データベーススキーマの変更や業務の変更に弱い。

こういう問題があるので、今までもプロ筋はiBatis/MyBatisを好んで使う傾向があったと思います。

問い合わせ言語はSQLだけではなく、各種のNoSQLにもそれぞれの問い合わせ言語がありますし、XMLではXQueryだけでなくXSLT、XPathも一種の問い合わせ言語と考えることができます。いずれも関数型言語としてとらえなおしてみると、関数型プログラミングとの併用で面白いユースケースが見つかりそうです。

SQLと関数型言語の接続

問い合わせ言語としてのSQLの特徴の一つは、問い合わせ結果が表形式になるということです。問い合わせの結果得られる情報は色々な構造を持つわけですが、一つの表にそういった構造がエンコードされて入ってきます。これをデコードするのが、プログラム側の最初の処理になります。

エンコードされる情報は木構造だったり、グラフ構造だったりするわけですが、これをデコードするコーディングはイディオム化できます。このイディオムを覚えておけば、SQLと関数型プログラミングを自由自在に接続することができるわけです。

組織階層図

例題としてRDBMSの上の部門マスタから組織ツリーを生成するプログラムを考えてみましょう。以下のようなSQLで部門マスタから部門情報を取り出すとします。

select C.部門ID as 部門ID, C.部門名 as 部門名,
       P.部門ID as 親部門ID, P.部門名 as 親部門名
  from 部門マスタ as C
    left outer join 部門マスタ as P on C.親部門ID=P.部門ID

プログラム

プログラムはSQLのアクセスは省略して、SQLの結果がMapのListとして返ってきた所から始めることにします。PlayのAnormがこういったアクセス法の代表例です。

変数recordsにデータが格納されています。

package tryout

import scalaz._, Scalaz._

object SqlSample {
  val records = List(Map('部門ID -> 1,
                         '部門名 -> "営業統括",
                         '親部門ID -> None,
                         '親部門名 -> None),
                     Map('部門ID -> 11,
                         '部門名 -> "東日本統括",
                         '親部門ID -> Some(1),
                         '親部門名 -> "営業統括"),
                     Map('部門ID -> 12,
                         '部門名 -> "西日本統括",
                         '親部門ID -> Some(1),
                         '親部門名 -> "営業統括"),
                     Map('部門ID -> 13,
                         '部門名 -> "首都圏統括",
                         '親部門ID -> Some(1),
                         '親部門名 -> "営業統括"),
                     Map('部門ID -> 111,
                         '部門名 -> "北海道支店",
                         '親部門ID -> Some(11),
                         '親部門名 -> "東日本統括"),
                     Map('部門ID -> 112,
                         '部門名 -> "東北支店",
                         '親部門ID -> Some(11),
                         '親部門名 -> "東日本統括"),
                     Map('部門ID -> 113,
                         '部門名 -> "北陸支店",
                         '親部門ID -> Some(11),
                         '親部門名 -> "東日本統括"),
                     Map('部門ID -> 114,
                         '部門名 -> "中部支店",
                         '親部門ID -> Some(11),
                         '親部門名 -> "東日本統括"),
                     Map('部門ID -> 121,
                         '部門名 -> "近畿支店",
                         '親部門ID -> Some(12),
                         '親部門名 -> "西日本統括"),
                     Map('部門ID -> 122,
                         '部門名 -> "中国支店",
                         '親部門ID -> Some(12),
                         '親部門名 -> "西日本統括"),
                     Map('部門ID -> 123,
                         '部門名 -> "四国支店",
                         '親部門ID -> Some(12),
                         '親部門名 -> "西日本統括"),
                     Map('部門ID -> 124,
                         '部門名 -> "九州支店",
                         '親部門ID -> Some(12),
                         '親部門名 -> "西日本統括"),
                     Map('部門ID -> 125,
                         '部門名 -> "沖縄支店",
                         '親部門ID -> Some(12),
                         '親部門名 -> "西日本統括"),
                     Map('部門ID -> 131,
                         '部門名 -> "東京支店",
                         '親部門ID -> Some(13),
                         '親部門名 -> "首都圏統括"),
                     Map('部門ID -> 132,
                         '部門名 -> "北関東支店",
                         '親部門ID -> Some(13),
                         '親部門名 -> "首都圏統括"),
                     Map('部門ID -> 133,
                         '部門名 -> "南関東支店",
                         '親部門ID -> Some(13),
                         '親部門名 -> "首都圏統括"))

  def main(args: Array[String]) {
    val a = build部門(records)
    val b = buildTree(a)
    showTree(b)
  }

  def build部門(records: Seq[Map[Symbol, Any]]): Map[Int, 部門] = {
    records.foldRight(Map[Int, 部門]())((x, a) => {
      val 部門ID = x('部門ID).asInstanceOf[Int]
      val 部門名 = x('部門名).asInstanceOf[String]
      val 親部門ID = x('親部門ID).asInstanceOf[Option[Int]]
      val 親部門名 = x.get('親部門名).asInstanceOf[Option[String]]
      a + (部門ID -> 部門(部門ID, 部門名, 親部門ID, 親部門名))
    })
  }

  def buildTree(sections: Map[Int, 部門]): Tree[部門] = {
    def build(sec: 部門): Tree[部門] = {
      val children = sections collect {
        case (k, v) if v.親部門ID == sec.部門ID.some => v
      }
      node(sec, children.toStream.sortBy(_.部門ID).map(build))
    }

    build(sections(1))
  }

  def showTree(tree: Tree[部門]) {
    println(tree.drawTree(showA[部門]))
  }
}

case class 部門(部門ID: Int, 部門名: String,
                親部門ID: Option[Int], 親部門名: Option[String])
ドメイン・モデル

検索結果をアプリケーションのドメイン・モデルに写像するわけですが、ドメイン・オブジェクトとして「部門」を定義しました。

業務アプリケーションの場合、ドメイン・モデルとのインピーダンス・ミスマッチを極力排除するためにドメイン・モデルが日本語の場合はプログラムの識別子にも同じ日本語を使うべき、と考えているので業務アプリケーションらしくこのサンプルもそうしています。DDDにおけるユビキタス言語の実践の一つですね。

build部門メソッドはrecordsの値(SQLでの検索結果)から部門一覧のMapを作成します。このメソッドが、SQLの結果とドメイン・モデルを写像する前半分の処理になります。

build部門メソッドでは、関数型プログラミングでおなじみのfoldRightメソッドを使って、問い合わせ結果を部門のMapに畳み込んでいます。Mapに対する畳込みがこの際のイディオムですね。

木構造の実現

buildTreeメソッドは部門一覧の表から部門の木構造を生成します。このメソッドが、SQLの結果とドメイン・モデルを写像する後半分の処理になります。

部門ツリーはScalazの代表的な永続データ構造であるTreeを用いて木構造を構築しています。

木構造の表示

部門の木構造の表示はshowTreeメソッドで行なっています。ScalazのTreeの機能を用いるので簡単に表示を行うことができます。

実行

プログラムを実行すると、以下のように組織ツリーが表示されました。

部門(1,営業統括,None,Some(None))
|
+- 部門(11,東日本統括,Some(1),Some(営業統括))
|  |
|  +- 部門(111,北海道支店,Some(11),Some(東日本統括))
|  |
|  +- 部門(112,東北支店,Some(11),Some(東日本統括))
|  |
|  +- 部門(113,北陸支店,Some(11),Some(東日本統括))
|  |
|  `- 部門(114,中部支店,Some(11),Some(東日本統括))
|
+- 部門(12,西日本統括,Some(1),Some(営業統括))
|  |
|  +- 部門(121,近畿支店,Some(12),Some(西日本統括))
|  |
|  +- 部門(122,中国支店,Some(12),Some(西日本統括))
|  |
|  +- 部門(123,四国支店,Some(12),Some(西日本統括))
|  |
|  +- 部門(124,九州支店,Some(12),Some(西日本統括))
|  |
|  `- 部門(125,沖縄支店,Some(12),Some(西日本統括))
|
`- 部門(13,首都圏統括,Some(1),Some(営業統括))
   |
   +- 部門(131,東京支店,Some(13),Some(首都圏統括))
   |
   +- 部門(132,北関東支店,Some(13),Some(首都圏統括))
   |
   `- 部門(133,南関東支店,Some(13),Some(首都圏統括))

2012年8月8日水曜日

クラウド温泉3.0 (16) / Kleisli

クラウド温泉3.0@小樽のセッション「Monadicプログラミング・マニアックス」で使用するスライドのネタ検討その16です。

関数型プログラミングでは、引数が1つの関数が非常に重要です。極端に言うと引数が1つの関数を合成して、プログラムを構築していきます。

もちろん引数が2以上の関数も使いますが、この場合はカリー化や部分適用を使って引数が1つの関数に持っていくのが重要なテクニックになっています。

引数の数が1の関数

引数の数が1つの関数として、3倍する関数mul3と5を加える関数plus5を定義します。

val mul3 = (_: Int) * 3
val plus5 = (_: Int) + 5

引数1の関数は以下のようにmapメソッドといった高階関数に指定して処理を合成することができます。多くの高階関数は引数の数が1の関数を入力にするので、引数の数が1の関数は再利用可能な部品として利用価値が高くなります。

scala> List(1, 2, 3).map(mul3)
res19: List[Int] = List(3, 6, 9)

A→M[B]

FunctorのmapメソッドはA→Bという形の関数を引数に取りますが、MonadのflatMapはA→M[B]という形の関数を引数に取ります。MはMonadなので、AからBをMonadでくるんだものを返す関数ということになります。A→M[B]も引数の数が1の関数の一種ですが、Monadを返すところが特殊化されています。

Monadは関数型プログラミングに非常に重要な構成要素です。つまり、A→M[B]の関数も再利用可能な部品として利用価値が高いと考えられます。

先ほど定義したmul3とplusをMをListとしてA→M[B]化した関数mul3lとplus5lは以下になります。狙いが分かりやすいようにmul3lは0以上の値、plus5lは偶数を有効として扱うように機能追加しました。

val mul3l = (x: Int) => if (x >= 0) List(x * 3) else Nil
val plus5l = (x: Int) => if (x % 2 == 0) List(x + 5) else Nil

mul3lをListのmapメソッドに適用すると以下のようになります。

scala> List(1, -2, 3, 4).map(mul3l)
res33: List[List[Int]] = List(List(3), List(), List(9), List(12))

さて、mul3lをListのflatMapメソッドに適用すると以下になります。-2に対応する要素が結果から取り除かれていますが、これがモナドの効果です。

scala> List(1, -2, 3, 4).flatMap(mul3l)
res34: List[Int] = List(3, 9, 12)

mul3lとplus5lを連続してflatMapで適用すると以下になります。

scala> List(1, -2, 3, 4).flatMap(mul3l).flatMap(plus5l)
res32: List[Int] = List(17)

Kleisli

mul3lとplus5lの組み合わせが頻出する場合、これを一つの関数に合成しておくと便利です。ただし、mul3lやplus5lは引数と返却値の型が違うので単純な関数合成はできません。

scala> mul3l andThen plus5l
<console>:16: error: type mismatch;
 found   : Int => List[Int]
 required: List[Int] => ?
              mul3l andThen plus5l
                            ^

そこで登場するのがKleisliです。

"プログラムとはクライスリ圏の射である(program is arrow of Kleisli category)"という背景があり、クライスリ圏の射に対する演算をプログラミング言語で扱うメカニズムがScalaやHaskllのモナドということになるかと思います。

そして、クライスリ圏の射はScalaではA→M[B]の関数が対応します。A→M[B]の関数はMonadのflatMapメソッドに適用できますが、単体で扱いたい場合には特別なサポートがあると便利です。このような目的で使用するのがScalazのKleisli(以下単にKleisliと呼びます)です。

KleisliはA→M[B]の関数をくるんで、クライスリ圏の射として操作するためのメカニズムを提供します。

mul3lとplus5lをKleisli化すると以下になります。mul3lkとplus5lkがKleisli化したmul3lとplus5lです。

scala> val mul3lk = kleisli(mul3l)
mul3lk: scalaz.Kleisli[List,Int,Int] = scalaz.Kleislis$$anon$1@6119d27b

scala> val plus5lk = kleisli(plus5l)
plus5lk: scalaz.Kleisli[List,Int,Int] = scalaz.Kleislis$$anon$1@38ab3c51

mul3lkとplus5lkはKleisliなので演算子>=>で合成することができます。

scala> val m3p5lk = mul3lk >=> plus5lk
m3p5lk: scalaz.Kleisli[List,Int,Int] = scalaz.Kleislis$$anon$1@76f4a74b

scala> m3p5lk(1)
res40: List[Int] = List()

scala> m3p5lk(4)
res41: List[Int] = List(17)

合成したKleisliであるm3p5lkをflatMapに適用すると以下のようになります。このm3p5lkのようにA→M[B]をKleisli化して合成することでMonad向けの部品を整備することができます。

scala> List(1, -2, 3, 4).flatMap(m3p5lk)
res39: List[Int] = List(17)

なおKleisliは引数1の関数でもあるのでmapメソッドに適用することもできます。

scala> List(1, -2, 3, 4).map(m3p5lk)
res43: List[List[Int]] = List(List(), List(), List(), List(17))

パイプライン・プログラミング

Kleisliをパイプライン・プログラミングの観点でみてみましょう。

まずListからmapやflatMapメソッドを適用する形も処理が左から右に流れていくのでパイプライン・プログラミングといえます。これがMonadを使ったパイプライン・プログラミングです。

scala> List(1, -2, 3, 4).flatMap(mul3lk).flatMap(plus5lk)
res46: List[Int] = List(17)

さらに関数の合成としてのパイプライン・プログラミングとしてはパイプライン演算子とKleisliの合成を使って以下のような処理が可能です。

scala> 4 |> mul3lk >=> plus5lk
res45: List[Int] = List(17)

2012年8月7日火曜日

クラウド温泉3.0 (15) / Arrow

クラウド温泉3.0@小樽のセッション「Monadicプログラミング・マニアックス」で使用するスライドのネタ検討その15です。

今回のテーマはArrowです。Arrowは圏論の射(arrow, morphism)を型クラス化したもので、圏(category)を型クラス化したCategoryと組合せて圏論的な操作ができると思われますが、具体的なユースケースはボクもよくわかりません。

しかし、引数1の関数であるFunction1を合成するという用途では一定の利用価値があります。

Scalazでの型クラスは定義は以下のような形になります。

関数の合成

まず3倍する関数mul3と5を加える関数plus5を定義します。

scala> val mul3 = (_: Int) * 3
mul3: Int => Int = <function1>

scala> val plus5 = (_: Int) + 5
plus5: Int => Int = <function1>

Scala純正のcomposeメソッドで合成することができます。

scala> val p5m3 = mul3 compose plus5
p5m3: Int => Int = <function1>

scala> p5m3(3)
res147: Int = 24

Scala純正のandThenメソッドで合成することもできます。andThenメソッドの方が左側の関数→右側の関数の順に関数が実行されるので、よりパイプライン的です。ただandThenというメソッド名が少し重たい感じです。

scala> val p5m3 = plus5 andThen mul3
p5m3: Int => Int = <function1>

scala> p5m3(3)
res147: Int = 24

ScalazのArrowを使うと関数合成を以下のように>>>演算子で記述することができます。

scala> val p5m3 = plus5 >>> mul3
p5m3: Int => Int = <function1>

scala> p5m3(3)
res147: Int = 24

パイプライン演算子|>と組み合わせると以下のようになります。まさにパイプライン処理という感じですね。

scala> 3 |> plus5 >>> mul3
res150: Int = 24

パイプラインの複線化

&&&演算子でパイプラインを複線化、***演算子でパイプラインの並行実行を行うことができます。また、firstメソッドで一番目のパイプラインのみの実行、secondメソッドで二番目のパイプラインのみの実行を行うこともできます。

scala> 3 |> (plus5 &&& mul3)
res43: (Int, Int) = (8,9)

scala> 3 |> (plus5 &&& mul3) >>> (plus5 *** mul3)
res44: (Int, Int) = (13,27)

scala> 3 |> (plus5 &&& mul3) >>> (plus5 *** mul3) >>> mul3.first
res45: (Int, Int) = (39,27)

詳しくは「関数型とデータフロー(2)」を参照してください。

Monadicプログラミング

本セッションではMonadicプログラミングは以下のものを指すことにしました。

  • Monadを中心としつつも、旧来からあるFunctorによるプログラミング、関数合成、コンビネータなども包含しつつ、パイプライン・プログラミングのスタイルとしてまとめたもの。

Arrowによる関数合成も「パイプライン・プログラミング」であるMonadicプログラミングの重要な構成要素になります。

2012年8月6日月曜日

クラウド温泉3.0 (14) / Applicative

クラウド温泉3.0@小樽のセッション「Monadicプログラミング・マニアックス」で使用するスライドのネタ検討その14です。

Functorよりも強くMonadよりも弱い性質を持つ型クラスにApplicativeがあります。正確にはApplicative Functorという名前がついていますが、型クラス名に合わせてここではApplicativeと呼ぶことにします。

圏論系の技術の中での位置付けは以下のような感じです。

Scalazでの型クラスは定義は以下のような形になります。

Scala Tipsの中では以下の記事が分かりやすいかもしれません。

Applicative

ApplicativeはFunctorやMonadと同様にオブジェクトを格納するコンテナかつ計算文脈です。Applicativeの集まりに対して、1つのApplicativeを1つのパラメタとして演算を実行することができます。

演算子|@|がScalazが提供する代表的なApplicative演算子です。

scala> (1.some |@| 2.some)(_ + _)
res89: Option[Int] = Some(3)

ApplicativeのListに対して以下のようにfoldによる畳込みを行うことができます。

scala> List(1.some, 2.some).foldRight(0.some)((x, a) => (x |@| a)(_ + _))
res104: Option[Int] = Some(3)

ApplicativeはTraversableとの組み合わせで畳込み処理を行うこともできます。ApplicativeはTraversableの対象となっているのが重要な性質の一つです。

scala> List(1.some, 2.some).sequence
res92: Option[List[Int]] = Some(List(1, 2))

scala> List(1.some, 2.some).sequence.map(_.foldRight(0)(_ + _))
res111: Option[Int] = Some(3)

ListやOptionあるいはValidationなど、Monad的なオブジェクトはMonadかつApplicativeであることがほとんどなので、プログラミング上はMonadの機能の一つと考えておくと分かりやすいでしょう。

使用例

ApplicativeはValidationと併用するのが典型的な使用例です。

ノート

Applicativeで行う処理の多くは条件が合えばMonoidでも実現することができます。Monoidを使った方が簡潔になるので、可能であればMonoidを使用するのがよいでしょう。

scala> 1.some |+| 2.some
res88: Option[Int] = Some(3)

scala> List(1.some, 2.some).sequence.map(_.sum)
res96: Option[Int] = Some(3)

scala> List(1.some, 2.some).foldMap(identity)
res100: Option[Int] = Some(3)

scala> List(1.some, 2.some).sumr
res102: Option[Int] = Some(3)

ApplicativeとMonoidの使い分け、Monoidが使用できる条件判定をプログラミング時に瞬時に判断できるようにしておくのが大事です。

2012年8月3日金曜日

クラウド温泉3.0 (13) / Monad

クラウド温泉3.0@小樽のセッション「Monadicプログラミング・マニアックス」で使用するスライドのネタ検討その13です。

Monadicプログラミングをテーマにしているので、主役はやはりMonadです。

Monadについては、Scala Tipsでも中心的なテーマとして取り上げてきたので、この中から今回のセッションの趣旨にそったものを集めてくる感じになります。

Monadicプログラミングをパイプライン・プログラミングと見立てると、以下のような記述方式が候補です。

この中でMonadを使っているのは3, 4, 5となります。パイプラインという観点からは、Monadを使っていないものを排除しても実用的なメリットはないので、合わせてMonadicプログラミングと呼ぶのが本セッションでの定義です。

Scalaにおけるモナドについては、以下の記事が参考になるかもしれません。

Monadの実装に用いられているScalazの型クラスは以下の記事で取り上げています。

Monadの具体的な使用方法として、以下のクラスを取り上げてきました。

モナドが提供する計算文脈を使うことで、計算文脈の提供するサービスを暗黙的に利用しながら動作する処理を、パイプラインのセマンティクスで処理を簡潔に記述することができます。

パイプラインの表層を流れるデータと演算、さらにパイプラインの裏側で暗黙に行われるデータと演算が疎結合になっているのが、モナドの醍醐味だと思うので、そのあたりにフォーカスしていきたいと思います。

ノート

パイプライン・プログラミングという観点からは重要な切り口ではないので、セッションでは取り上げないと思いますが、Scala TipsではfoldとMonadの組み合わせについても取り上げています。その他、Monadを対象としたメソッドが色々あるので、Scala Tipsで随時取り上げていきたいと思います。

2012年8月2日木曜日

クラウド温泉3.0 (12) / Monoid

クラウド温泉3.0@小樽のセッション「Monadicプログラミング・マニアックス」で使用するスライドのネタ検討その12です。

関数型プログラミングで最重要な構成要素としてまずあげられるのがFunctorです。具体的にはmapメソッドを提供しているコンテナが相当します。これは空気のようなものですね。

そして、言うまでもなく最近の話題の中心はMonad。Scalaも言語仕様に取り入れています。セッションのテーマであるMonadicプログラミングも、このMonadをいかに活用するのかというのがひとつの軸になります。

この2つに加えて、個人的にこれも最重要構成要素に入れてよいのではと考えているがMonoidです。

Monoidは関数型プログラミングに頻出する抽象です。閉じた2項演算(つまりA☆A→A)でMonoidの規則を満たすものがあれば、Monoidとして定義しておくことでMonoid向けのさまざまなロジックをそのまま適用することができます。その代表的なものの一つがfoldによる畳込みです。

Monoidは残念ながらScalaの基本ライブラリでは提供されていないので、ScalazのMonoidを使うことになります。Scalazでは型クラスMonoidを提供しており、任意の型をMonoidとして定義することができます。

今まで記事でMonoidについて様々な観点から取り上げてきたので、この中から情報をピックアップしてスライドに集約する形になります。どれを持ってくるか悩みどころですね。

fold

Monoidの重要なユースケースであるfoldについて。foldとMonoidの組み合わせは関数型プログラミングのハイライトの一つかもしれません。Monadicプログラミングという観点からも、一つスライドを割り当てて取り上げることになると思います。

Reducer/Generator

Monoidの応用であるReducer, Generatorについて。Reducer自身は利用するシーンはあまり多くはないかもしれませんが、Monoidをどのような形で伸ばしていくのかというアプローチは参考になります。

2012年8月1日水曜日

クラウド温泉3.0 (11) / reduce, collect, flatMap

クラウド温泉3.0@小樽のセッション「Monadicプログラミング・マニアックス」で使用するスライドのネタ検討その11です。

前回はFunctorが提供する計算文脈におけるパイプラインで使用できるmapメソッド、filterメソッド、foldメソッドを取り上げました。この3つが基本中の基本になりますが、他にも有用なものがあります。

reduce

reduceメソッドはコンテナ内の要素に対して二項演算を繰り返し適用し、ひとつの要素にまとめる処理を行います。foldメソッドの用途限定版です。

scala> List(1, 2, 3).reduce(_ + _)
res2: Int = 6

collect

collectメソッドは部分関数(partial function)を使って、要素の選択を変換を同時に行います。filterメソッドとmapメソッドを一つにまとめたような動きになります。

scala> List(1, 2, 3).collect {
     |  case x if x % 2 == 1 => x.toString
     | }
res5: List[java.lang.String] = List(1, 3)

flatMap

flatMapメソッドはモナドの動作を行うメソッドです。mapメソッドに指定する関数が要素の変換を行うのに対して、flatMapメソッドに指定する関数は要素からコンテナへの変換を行います。このコンテナの集まりを"コンテナの流儀"でひとつのコンテナにまとめるのがモナドの肝となる動きになります。

scala> List(1, 2, 3).flatMap(x => {
     | if (x % 2 == 1) List(x.toString, (x * 10).toString) else Nil
     | })
res7: List[java.lang.String] = List(1, 10, 3, 30)

Listの場合は、Listの集まりを順序を維持したまま一つのListにまとめます。このため、flatMapの実行でListの要素数を増やしたり減らしたりといったことが可能です。上記の例では、リストの要素数が3から4に増えています。

パイプラインの構成部品

パイプラインの構成部品の表にreduce, collect, flatMapを追加しました。

メソッド動作動作イメージコンテナ型要素型要素数
mapコンテナ内の要素に関数を適用して新しいコンテナに詰め直す。M[A]→M[B]変わらない変わる変わらない
filterコンテナ内の要素を選別して新しコンテナに詰め直す。M[A]→M[A]変わらない変わらない減る
foldコンテナをまるごと別のオブジェクトに変換する。M[A]→N変わる--
reduceコンテナの内容を一つにまとめる。M[A]→A変わる--
collectコンテナ内の要素に部分関数を適用して選択と変換を行う。M[A]→M[B]変わらない変わる減る
flatMapコンテナ内の要素ごとにコンテナを作成する関数を適用し最後に一つのコンテナにまとめる。M[A]→M[B]変わらない変わる増える/減る |

メソッドの追加に加えて、以下の点を変更しています。

  • コンテナ→コンテナ型、要素→要素型
  • コンテナ型が「変わる」の時は要素型、要素数は意味を持たないので「-」とした。

パイプラインで有用な部品のパターンは概ねカバーできたと思います。

ノート

スライド的には、この表で一つスライドを起こして、各メソッドを例題付きで簡単に説明する感じになりそうです。

foldとreduce

reduceはfoldの特殊な形なのでfoldファミリと考えるとよいでしょう。fold系の処理の中で条件によってreduceを選ぶという形になります。

foldファミリは、Monoidと相性が良いので、Monoidとどう組合せていくのかというのも重要な論点になります。これはMonoidで一つスライドを起こして説明したいと思います。

foldMap

flatMapメソッドは、まさにMonadicプログラミングの核となるメソッドです。

この比較表ではflatMapメソッドの威力はあまり見えてきません。これはFunctor視点の比較表になるためですね。flatMapについては別枠で説明することにすることになります。