乱数の生成についての簡単な例を見てみよう。scala.util.Random が用意されているのでそれを使えば簡単だ。
scala> val rng = new scala.util.Random
rng: scala.util.Random = scala.util.Random@5e28ea8b
scala> rng.nextInt
res0: Int = -1998760920
scala> rng.nextInt
res1: Int = 1591304297
scala> rng.nextInt
res2: Int = -321914793
nextInt メソッドは呼び出しのたびに返ってくる値が異なる。呼び出しのたびに rng の状態が遷移していることが想像出来る。たとえば、ランダム性を利用したメソッドのテストを書こうとした場合、テストを再現可能にする必要があるが scala.util.Random では難しそうである。仮に乱数ジェネレータを直接扱うとしても、ジェネレータの状態を揃えてあげる必要がある。こういった状態に対処するために、副作用を使用しないという原点に立ち返ろう。
純粋関数型の乱数生成
状態を遷移させるのではなく、新しい状態を返すという気持ちでやっていく。乱数ジェネレータの場合は、ジェネレータの nextInt を呼び出すと、値と新しいジェネレータを返すという感じになる。もちろん、もとのジェネレータは状態遷移しない。したがって、もとのジェネレータの nextInt を何回呼び出しても、同じ値と新しいジェネレータが返ってくる。コードで表現すると次のようになる。
trait RandomGenerator {
def nextInt: (Int, RandomGenerator)
}
case class SimpleRandomGenerator(seed: Long) extends RandomGenerator {
def nextInt: (Int, RandomGenerator) = {
val newSeed = (seed * 0x5DEECE66DL + 0xBL) & 0xFFFFFFFFFFFFL
val nextGenerator = SimpleRandomGenerator(newSeed)
((newSeed >>> 16).toInt, nextGenerator)
}
}
こいつは以下のような挙動を示す。
scala> val gen = SimpleRandomGenerator(12345)
gen: SimpleRandomGenerator = SimpleRandomGenerator(12345)
scala> gen.nextInt
res0: (Int, RandomGenerator) = (454757875,SimpleRandomGenerator(29803012144720))
scala> gen.nextInt
res1: (Int, RandomGenerator) = (454757875,SimpleRandomGenerator(29803012144720))
scala> res1._2.nextInt
res2: (Int, RandomGenerator) = (-866467965,SimpleRandomGenerator(224690132215835))
scala> res1._2.nextInt
res3: (Int, RandomGenerator) = (-866467965,SimpleRandomGenerator(224690132215835))
scala> gen.nextInt
res4: (Int, RandomGenerator) = (454757875,SimpleRandomGenerator(29803012144720))
見ての通り、nextInt は純粋な関数になった。
ステートフルな API と向き合う
乱数生成の例で見たようなことと同じような方法で、すべてのステートフル API は純粋化できる。基本的には状態を遷移させるのではなく、値と新しい状態を返してやるという方針になる。ただし普通にこれをやってみるとボイラープレートが多くて辛い気持ちになるだろう。たとえば RandomGenerator に対していろんな便利関数を定義してみる。同じことばっかりやっているように見えないだろうか?
object RandomGenerator {
def nextIntPair(rng: RandomGenerator): ((Int, Int), RandomGenerator) = {
val (i1, rng1) = rng.nextInt
val (i2, rng2) = rng1.nextInt
((i1, i2), rng2)
}
def nonNegativeInt(rng: RandomGenerator): (Int, RandomGenerator) = {
val (i, r) = rng.nextInt
val j = if (i < 0) -(i + 1) else i
(j, r)
}
def double(rng: RandomGenerator): (Double, RandomGenerator) = {
val (i, r) = nonNegativeInt(rng)
val j = if (i == 0) i else i.toDouble / Int.MaxValue
(j, r)
}
def intDouble(rng: RandomGenerator): ((Int, Double), RandomGenerator) = {
val (i1, rng1) = rng.nextInt
val (d1, rng2) = double(rng1)
((i1, d1), rng2)
}
}
すべて RandomGenerator => (A, RandomGenerator) な関数になっている。「状態を遷移させるのではなく、値と新しい状態を返してやる」という方針が関数に明確に現れてきている。この部分は繰り返し出現するのですこし抽象化しよう。type を使って RandomGenerator => (A, RandomGenerator) に Rand[A] というエイリアスを張り付ける。すると見なれた map や map2 などの高階関数が定義できて、かつそれを用いて上で定義した関数などを実装できるようになる。
object RandomGenerator {
type Rand[+A] = RandomGenerator => (A, RandomGenerator)
def int: Rand[Int] = _.nextInt
def unit[A](a: A): Rand[A] = rng => (a, rng)
def map[A, B](r: Rand[A])(f: A => B): Rand[B] = rng => {
val (a1, r1) = r(rng)
(f(a1), r1)
}
def map2[A, B, C](ra: Rand[A], rb: Rand[B])(f: (A, B) => C): Rand[C] = rng => {
val (a, r1) = ra(rng)
val (b, r2) = rb(r1)
(f(a, b), r2)
}
def both[A, B](ra: Rand[A], rb: Rand[B]): Rand[(A, B)] = map2(ra, rb)((_, _))
def sequence[A](fs: List[Rand[A]]): Rand[List[A]] =
fs.foldRight(unit(List[A]()))((f, acc) => map2(f, acc)(_ :: _))
def flatMap[A, B](r: Rand[A])(f: A => Rand[B]): Rand[B] = rng => {
val (a1, rng1) = r(rng)
f(a1)(rng1)
}
def nextIntPair: Rand[(Int, Int)] = both(int, int)
/*
def nextIntPair(rng: RandomGenerator): ((Int, Int), RandomGenerator) = {
val (i1, rng1) = rng.nextInt
val (i2, rng2) = rng1.nextInt
((i1, i2), rng2)
}
*/
def nonNegativeInt: Rand[Int] = map(int)(i => if (i < 0) -(i + 1) else i)
/*
def nonNegativeInt(rng: RandomGenerator): (Int, RandomGenerator) = {
val (i, r) = rng.nextInt
val j = if (i < 0) -(i + 1) else i
(j, r)
}
*/
def double: Rand[Double] = map(nonNegativeInt)(i => if (i == 0) i else i.toDouble / Int.MaxValue)
/*
def double(rng: RandomGenerator): (Double, RandomGenerator) = {
val (i, r) = nonNegativeInt(rng)
val j = if (i == 0) i else i.toDouble / Int.MaxValue
(j, r)
}
*/
def intDouble: Rand[(Int, Double)] = both(int, double)
/*
def intDouble(rng: RandomGenerator): ((Int, Double), RandomGenerator) = {
val (i1, rng1) = rng.nextInt
val (d1, rng2) = double(rng1)
((i1, d1), rng2)
}
*/
}
Pinned Articles
About
ウェブ界隈でエンジニアとして労働活動に励んでいる @gomi_ningen 個人のブログです
Tags
JavaScript
PowerShell
kibana
elasticsearch
fluentd
nginx
イベント
五十嵐裕美
村川梨衣
logrotate
IoT
Scala
Java
C言語
iputils
ICMP
WUG
mastodon
Swift
AWS
Clock
Windows
アーキテクチャ
PoEAA
iOS
DeviceFarm
プログラミング言語
OS
StepFunctions
Lambda
Serverless
terraform
ポエム
RHEL
ネットワーク
GraphQL
CloudWatch
Linux
Coreutils
network
nc
telnet
LinuxKernel
fpinscala
ELB
IAM
AppSync
EFS
Gradle
english