前回は flatMap がどんなものなのかという基礎の部分を考えたね。
前回記事
今回は、flatMap と前回作ったBoxを活用して、処理のサンプルとしてちょうど良い FizzBuzz の実装をしてみようか。
FizzBuzzって?
知らない人はそんなにいないと思うけど、
こういうやつです。
Box型のおさらい
まず、基本のBox型をおさらいしておくよ。前回作ったBoxは、次のように定義した。
type Box<T> = {
map: <U>(fn: (value: T) => U) => Box<U>
flatMap: <U>(fn: (value: T) => Box<U>) => Box<U>
"<$>": <U>(fn: (value: T) => U) => Box<U>
">>=": <U>(fn: (value: T) => Box<U>) => Box<U>
getValue: () => T
}
const box = <T>(value: T): Box<T> => ({
map: (fn) => box(fn(value)),
flatMap: (fn) => fn(value),
["<$>"]: (fn) => box(fn(value)),
[">>="]: (fn) => fn(value),
getValue: () => value,
})
おなじみの、Box型だね!
※ <$> は map 用の演算子
今回の Box では >>= しか使わないので、最低限の実装であればこれだけでも OK です。
type Box<T> = { ">>=": <U>(fn: (value: T) => Box<U>) => Box<U> }
const box = <T>(v: T): Box<T> => ({ ">>=": (fn) => fn(v) })
FizzBuzz 処理を表現する型 F の定義
次に、FizzBuzz の各処理を表現するために、型 F を定義する。この F 型は、[string, number] のタプルを受け取り、結果を Boxに包んで返す関数の型だ。
type F = (x: [string, number]) => Box<[string, number]>
各処理の関数を定義する
それぞれの FizzBuzz 処理をF型で定義していくよ。
log関数
log関数は、FizzBuzz の結果をコンソールに出力しつつ、値を次の処理に渡す。
const log: F = ([s, n]) => box([(console.log(s), s), n])
fizz関数
fizz関数は、数値が 3 の倍数ならFizzを追加し、次に渡す関数。
const fizz: F = ([s, n]) => box([n % 3 ? s : s + "Fizz", n])
buzz関数
buzz関数は、数値が 5 の倍数ならBuzzを追加する。
const buzz: F = ([s, n]) => box([n % 5 ? s : s + "Buzz", n])
num関数
num関数は、FizzやBuzzがなかった場合、数値そのものを文字列として表示する。
const num: F = ([s, n]) => box([s || s + n, n])
FizzBuzz 処理の連結
ここまで定義した関数を連結して、FizzBuzz を表現する関数fizzbuzzを作るよ。この関数も、他の処理と同じく型 F に準拠しているのがポイントなんだ。これで、fizzbuzz自体も他の F 型の関数と同じように flatMap で扱えるようになるよ。
const fizzbuzz: F = (x) =>
box(x)
[">>="](fizz) // 各関数を >>= で連結
[">>="](buzz)
[">>="](num)
FizzBuzz を出力する
最後に、1 から 100 までの数値に対してfizzbuzzを適用し、各結果をlog関数で表示してみる。
;[...Array(100).keys()].map((i) => fizzbuzz(["", ++i])[">>="](log))
FizzBuzz の全体コード
ここまでの FizzBuzz 実装をまとめると、以下のようになる。
type F = (x: [string, number]) => Box<[string, number]>
const log: F = ([s, n]) => box([(console.log(s), s), n])
const fizz: F = ([s, n]) => box([n % 3 ? s : s + "Fizz", n])
const buzz: F = ([s, n]) => box([n % 5 ? s : s + "Buzz", n])
const num: F = ([s, n]) => box([s || s + n, n])
const fizzbuzz: F = (x) =>
box(x)
[">>="](fizz) // 各関数を >>= で連結
[">>="](buzz)
[">>="](num)
;[...Array(100).keys()].map((i) => fizzbuzz(["", ++i])[">>="](log))
すべてが F になる
型 F ですべての関数を統一させることで、>>= を通じて処理を次々に連鎖させ、一貫した流れが生まれるんだ。
こうして 「すべてが F になる」 ことで、FizzBuzz のような一連の処理もシンプルに構築できる。
7 の倍数を追加する
flatMap の良いところは、既存の処理に新しい処理をブロックのように簡単に追加できること。ここでは、7 の倍数のときにJazzを表示する処理を追加して、fizzbuzzjazzとして拡張してみよう。
jazz関数
7 の倍数のときにJazzを追加する関数を新たに定義する。
const jazz: F = ([s, n]) => box([n % 7 ? s : s + "Jazz", n])
FizzBuzzJazz の連結
flatMap では、このように新しい処理をブロックのように繋げて簡単に拡張ができる。fizzbuzzにjazzを追加して、7 の倍数も考慮するfizzbuzzjazzを作ってみよう。
const fizzbuzzjazz: F = (x) =>
box(x)
[">>="](fizz)
[">>="](buzz)
[">>="](jazz) // 追加
[">>="](num)
こうすることで、新たな処理を既存のコードに追加するのもシンプルに済ませられるね。
FizzBuzzJazz の全体コード
最後に、7 の倍数jazzを追加した FizzBuzzJazz の全体コードをまとめておくよ。
type F = (x: [string, number]) => Box<[string, number]>
const log: F = ([s, n]) => box([(console.log(s), s), n])
const fizz: F = ([s, n]) => box([n % 3 ? s : s + "Fizz", n])
const buzz: F = ([s, n]) => box([n % 5 ? s : s + "Buzz", n])
const jazz: F = ([s, n]) => box([n % 7 ? s : s + "jazz", n]) // 追加
const num: F = ([s, n]) => box([s || s + n, n])
const fizzbuzzjazz: F = (x) =>
box(x)
[">>="](fizz)
[">>="](buzz)
[">>="](jazz) // 追加
[">>="](num)
;[...Array(100).keys()].map((i) => fizzbuzzjazz(["", ++i])[">>="](log))
こうして、fizzbuzzにjazzをブロックのように繋げて拡張できた。flatMap を使うと、既存コードに新しい処理を簡単に追加できるね!
おまけ: Array でも同じように書けるよ
この FizzBuzz は Array でも flatMap を使って同じように処理できるんだ。
Box の代わりに Array を使う場合、box(x)の代わりに[x]で値を Array に持ち上げて、あとはほぼ同じ形で FizzBuzz の処理が書ける。
型 F の返す値は Box<[stirng, number]> から Array<[stirng, number]> に変わってるところがポイントだね。
Array を使った FizzBuzz の実装
type F = (x: [string, number]) => Array<[string, number]> // BoxをArrayに
const log: F = ([s, n]) => [[(console.log(s.trim()), s), n]]
const fizz: F = ([s, n]) => [[n % 3 ? s : s + "Fizz ", n]]
const buzz: F = ([s, n]) => [[n % 5 ? s : s + "Buzz ", n]]
const num: F = ([s, n]) => [[s || s + n, n]]
const fizzbuzz: F = (x) =>
[x] // box(x)を [x]にする
.flatMap(fizz) // flatMapで連結
.flatMap(buzz)
.flatMap(num)
;[...Array(100).keys()].map((i) => fizzbuzz(["", ++i]).flatMap(log))
Array に>>=を追加する
さらに Box で使ってきた>>=記法、実は Array でも同じように使えるんだ。
まず、Array.prototype[">>="]にflatMapを割り当てて、Array でも同じように>>=を使えるように設定しよう。
declare global {
interface Array<T> {
[">>="]: typeof Array.prototype.flatMap
}
}
Array.prototype[">>="] = Array.prototype.flatMap
ループの map も >>= にしてみる
せっかくだし、ついでにループ用の map も flatMap で連結できるようにするために1つ型を追加しておくよ。
type X = (s: string, n: number) => Array<[string, number]>
この X 型を使って、
const x: X = (s, n) => [[s, ++n]]
という初期値の作成用の関数 x を用意しておく。
この x を空文字の入った配列から >>= で連結するようなイメージ。
Array(10).fill("")[">>="](x)
とすることで、
[
["", 1],
["", 2],
["", 3],
["", 4],
["", 5],
["", 6],
["", 7],
["", 8],
["", 9],
["", 10],
]
のような、このあとの FizzBuzz 処理に渡すための Array<[string, number]> が生成できる。
Array で >>= を使った FizzBuzz の実装
Array.prototype[">>="] = Array.prototype.flatMap // 追加
type X = (s: string, n: number) => Array<[string, number]> // 追加
type F = (x: [string, number]) => Array<[string, number]>
const x: X = (s, n) => [[s, ++n]] // 追加
const log: F = ([s, n]) => [[(console.log(s.trim()), s), n]]
const fizz: F = ([s, n]) => [[n % 3 ? s : s + "Fizz ", n]]
const buzz: F = ([s, n]) => [[n % 5 ? s : s + "Buzz ", n]]
const num: F = ([s, n]) => [[s || s + n, n]]
const fizzbuzz: F = (x) =>
[x]
[">>="](fizz) // Arrayでも >>= 記法が使える
[">>="](buzz)
[">>="](num)
Array(100)
.fill("")
[">>="](x) // x で初期化
[">>="](fizzbuzz)
[">>="](log)
こんなふうに、Array でも Box と同じように>>=を使って連結できるんだ。
Array も Box も「値を保持し、flatMap で連結できる」という共通の概念を持っているから、同じような記法で書けるというわけだね。
まとめ
今回の FizzBuzzJazz を通して、flatMap や Box を使った処理の拡張が見えてきたと思う。flatMap はシンプルに処理を繋げつつ、必要に応じて新たなロジックを追加しやすい特徴があるんだ。
このように、BoxやArrayといった「値を包んだ構造」を使うことで、さまざまな処理が型や構造で整理され、関数の連携がシンプルになる。
ぜひ、これを応用して flatMap の活用をさらに探求してみてほしいね。