2012年12月13日木曜日

Scala Tips / Streamで採番

名前に連番で番号を採番して関連付ける処理を考えます。採番する番号は、10から始めて10の間隔の昇順の数値とします。

名前と番号の対応を記述するケース・クラスNumberedNameを用意します。

case class NumberedName(name: String, number: Int)

引数で名前のシーケンスを受け取り、10から始めて10の間隔の昇順の数値を採番し、NumberedNameに名前と対にして格納する処理は、普通に考えると以下のようになるでしょう。

def f(names: Seq[String]): Seq[NumberedName] = {
  var c = 10
  for (n <- names) yield {
    val r = NumberedName(n, c)
    c += 10
    r
  }
}

手続き型プログラミング(Javaのメソッド実装を含む)では、よく出てくるロジックの形です。

実行

名前のリストを用意します。

scala> val xs = List("a", "b", "c")
xs: List[java.lang.String] = List(a, b, c)

実行結果は以下になります。

scala> f(xs)
res0: Seq[NumberedName] = List(NumberedName(a,10), NumberedName(b,20), NumberedName(c,30))

関数型のアプローチ

関数型プログラミングでは副作用をできるだけ避けるのが正しいプログラミング・スタイルになります。この目印となるのが「var」による変数宣言で、Scalaプログラミングではこの「var」が出てくると注意信号です。「「var」は明確な意図がない限り使わない」というのが有効なプログラミング戦略になります。

この方針を取るとすると、前述の関数fは明らかに方針に反しています。ただ、副作用なしでこのロジックを記述するのは案外大変です。このような簡単なロジックが捌けないようでは、副作用なし、「不変」戦略によるプログラミングを行うことできません。

このような連番採取の反復処理を関数型プログラミングで記述するのに使えるのが無限シーケンスであるStreamです。

Streamを使って記述すると以下のようになります。

def f(names: Seq[String]): Seq[NumberedName] = {
  for ((n, c) <- names zip Stream.from(10, 10)) yield {
    NumberedName(n, c)
  }
}

Streamのfromメソッドでは、第一引数に開始の番号、第二引数に連番の間隔を指定すると、数値の無限シーケンスであるStreamが返ってきます。これをSeqのzipメソッドを使って引数のシーケンスと接続すると引数のシーケンスとStreamに格納された数値を使った、名前と番号のTupleのシーケンスができあがります。Streamは無限シーケンスですが、引数のSeqが有限であれば、Tupleのシーケンスはそちらの長さになるので問題はありません。

このTupleをfor式で「(n, c)」という形の変数に受け取ってケースクラスNumberedNameに値を設定すればOKです。

実行

実行結果は以下になります。

scala> f(xs)
res1: Seq[NumberedName] = List(NumberedName(a,10), NumberedName(b,20), NumberedName(c,30))

この手の処理は「Stream+zipメソッド+for式の「(n, c)」」による記述が一つの形です。イディオムとして覚えておくとよいでしょう。

ノート

関数型プログラミングではStreamがかなり重要な部品になります。

以前「Streamで脱出」という話題でもStreamを取り上げました。

オブジェクト指向プログラミングでもjava.io.InputStreamといった形で無限を扱うためのテクニックとしてストリームの概念は出てきていますが、気軽に普段使いするようなものでもなく、IO入出力向けの用途限定テクニックという扱いでした。

一方、関数型プログラミングではごく気軽な普段使いのテクニックとして登場します。さらに、副作用を用いずプログラミングするには非常に有効なテクニックでもあり、そういう意味では関数型プログラミングの必須テクニックということができます。

諸元

  • Scala 2.9.2

0 件のコメント:

コメントを投稿