読者です 読者をやめる 読者になる 読者になる

Scalaのfor式とflatMapについて

scala

自分は職場では良くjava7でプログラミングするのでjava8のStream APIなどを触ることがないのですが、そのような状態だとscalaのfor式やflatMapに抵抗があったので簡単に使い方だけでもおさらいしてみたいと思います。

まず、動作確認で使うクラスは以下になります。Todo管理用のクラスを用意し、特定の作業者のタスクを検索するプログラムを試します。

  case class Todo(
    TodoId: Int, 
    CategoryId: Int, 
    Title: String, 
    Text: String, 
    Order: Int, 
    worker: List[String]){
    override def toString = Text
  }
  case class TodoCategory(
    CategoryId: Int, 
    Category: String, 
    Order: Int, 
    TodoList: List[Todo])

それから動作確認ようデータは以下になります。

  val collecting_doc = 
    Todo(1, 1, "collect data", "資料を集める", 1, 
      List("sato", "nakata"))
  val prepare_tool = 
    Todo(2, 1, "prepare tool", "ツールを準備する", 2, 
    List("kato", "nakata"))
  val investigate = 
    Todo(3, 2, "investigate", "調査する", 1, 
    List("sato"))
  val summarize_result = 
    Todo(4, 2, "summarize result", "調査結果をまとめる", 2, 
    List("kato", "nakata"))
  val review = 
    Todo(5, 2, "review", "レビューを受ける", 3, 
    List("sato", "nakata"))
  val presentation = 
    Todo(6, 3, "presentation", "発表する", 1, 
    List("sato"))
  val improve = 
    Todo(7, 3, "improve", "改善する", 2, 
    List("sato"))

  val done = TodoCategory(1, "完了", 1, 
    List(collecting_doc, prepare_tool))
  val doing = TodoCategory(2, "実施中", 2, 
    List(investigate, summarize_result, review))
  val next = TodoCategory(3, "待ち", 3, 
    List(presentation, improve))

  val allTodo: List[TodoCategory] = List(done, doing, next)

for式を使い特定の作業者のタスクを検索する場合は以下のようになります。

  def workerTaskFor(name: String): List[Todo] ={
    for(
      category <- allTodo;
      todo <- category.TodoList;
      tw <- todo.worker
      if tw eq name
    ) yield todo
  }

"category <- allTodo;"の部分でallTodoの一つずつの要素がcategoryには入ります。同様にtw <- todo.workerではtodoに設定されている一人一人のwoerkerが入り if tw eq nameにより名前での絞り込みを行っています。最後にyield todoの部分で絞り込まれたtodoでListを作成しています。


次にflatmapを使った場合は以下のようになります。

  def workerTaskFlatmap(name: String): List[Todo] = {
    allTodo flatMap(category =>
      category.TodoList flatMap( todo =>
        todo.worker withFilter (worker => worker eq name) map( 
          worker =>
            todo
        )
      )
    )
  }

やっていることはfor式と同じなのですがworkerの絞り込みにwithFilterというコレクションAPIを使用しています。コレクションAPIはTraverable を実装するコレクションクラスで利用することができ、これに慣れておくとデータの操作周りがすごい快適になると思います。自分の実感からもこの辺りになれるかどうかでScalaに感じる抵抗がぐっと変わってくるのかなという気がします。

最後にプログラムの全体は以下のようになりました。

object MyStudy {
  case class Todo(TodoId: Int, CategoryId: Int, Title: String, Text: String, Order: Int, worker: List[String]){
    override def toString = Text
  }
  case class TodoCategory(CategoryId: Int, Category: String, Order: Int, TodoList: List[Todo])

  val collecting_doc = Todo(1, 1, "collect data", "資料を集める", 1, List("sato", "nakata"))
  val prepare_tool = Todo(2, 1, "prepare tool", "ツールを準備する", 2, List("kato", "nakata"))
  val investigate = Todo(3, 2, "investigate", "調査する", 1, List("sato"))
  val summarize_result = Todo(4, 2, "summarize result", "調査結果をまとめる", 2, List("kato", "nakata"))
  val review = Todo(5, 2, "review", "レビューを受ける", 3, List("sato", "nakata"))
  val presentation = Todo(6, 3, "presentation", "発表する", 1, List("sato"))
  val improve = Todo(7, 3, "improve", "改善する", 2, List("sato"))

  val done = TodoCategory(1, "完了", 1, List(collecting_doc, prepare_tool))
  val doing = TodoCategory(2, "実施中", 2, List(investigate, summarize_result, review))
  val next = TodoCategory(3, "待ち", 3, List(presentation, improve))

  val allTodo: List[TodoCategory] = List(done, doing, next)


  def main(args: Array[String]): Unit ={

    workerTaskFor("nakata") foreach(println)

    workerTaskFlatmap("nakata") foreach(println)

  }

  def workerTaskFor(name: String): List[Todo] ={
    for(
      category <- allTodo;
      todo <- category.TodoList;
      tw <- todo.worker
      if tw eq name
    ) yield todo
  }

  def workerTaskFlatmap(name: String): List[Todo] = {
    allTodo flatMap(category =>
      category.TodoList flatMap( todo =>
        todo.worker withFilter (worker => worker eq name) map( 
          worker =>
            todo
        )
      )
    )
  }
}