Lambdaカクテル

京都在住Webエンジニアの日記です

Invite link for Scalaわいわいランド

Scalaで一定のコレクションを無限に繰り返すようなストリームを作る

このような、一定の要素を無限に繰り返したリストが欲しいことがある。無限に続くリストは通常のリストでは扱えないため、ストリームを使わなければならない。Scala 2.12まではストリームのためにStreamを使い、2.13以降はLazyListを使うことになっている*1*2

val xs = 1 :: 2 :: 3 :: Nil
val xss = ???

xss.take(10) // => LazyList(1, 2, 3, 1, 2, 3, 1, 2, 3, 1)

このようなストリームはどうやったら作れるのだろう?

tl;dr

LazyList.continually(1 :: 2 :: 3 :: Nil).flatten

環境

この記事ではScala 3.3.1を利用しています。2.13系でもだいたい同じように書けるはず。

要素が1つの場合

単一の要素を繰り返したいだけの場合は、LazyList.continuallyを利用する:

val xs = LazyList.continually(42)                                                                                                                       
val lis = xs.take(10).toList
// => List(42, 42, 42, 42, 42, 42, 42, 42, 42, 42)

シンプルでとても簡単だ。

複数要素の場合(手でループさせる)

冒頭の例のような複数の要素を繰り返す無限リストを作るには、まず「基底」になるLazyListを作成する:

val xs = 1 :: 2 :: 3:: Nil
val xs1 = LazyList.from(xs0) // => LazyList[Int](1, 2, 3)

そして、LazyListがループするように新たにLazyListを構成する:

// 定義に自己を含むのでdefにする
def xsInfinite: LazyList[Int] = xs1 #::: xsInfinite

こうすると、xs1が終わるとまた最初からループするようなLazyList[Int]が作られる。

xsInfiniteは自由にtakeなりzipなりして利用できる。

xsInfinite.take(10).toList
// => List(1, 2, 3, 1, 2, 3, 1, 2, 3, 1)
xsInfinite.zip("foo" :: "bar" :: Nil).toList
// => (1,foo) :: (2,bar) :: Nil

個人的にはLazyListのオブジェクトメソッドとして標準ライブラリに含まれてほしいと思う。もしかして既にある??

// こんなかんじになっててほしい
LazyList.repeat(1 :: 2 :: 3 :: Nil)

複数要素の場合(flattenを使うテク)

flattenを使うことでもう少し楽に記述できる。

val xs = LazyList.continually(1 :: 2 :: 3 :: Nil).flatten
xs.take(10).toList
// => List(1, 2, 3, 1, 2, 3, 1, 2, 3, 1)

これは、まずcontinuallyLazyList(List(1,2,3), List(1,2,3), ...)が作られ、さらにflattenされることでLazyList(1,2,3,1,2,3, ...)になるという仕組み。一見flattenするときに処理が止まらなそうに見えるが、LazyListなのでうまく処理が遅延されて、必要に応じて必要な部分だけflattenされるようだ。

ちなみにHaskellではcycleという関数を用意している。

hackage.haskell.org

*1:厳密には2.13以降でもStreamをdeprecatedで使うことはできる

*2:Streamは先頭の1要素は正則評価されるので完全に遅延リストではないが、LazyListは全要素が遅延評価されるという違いがある。基本的に後者を使えばよい

★記事をRTしてもらえると喜びます
Webアプリケーション開発関連の記事を投稿しています.読者になってみませんか?