Scalaの関数型の書き方の紹介
はじめに
普段使用しているScalaの関数型の書き方について紹介です。
関数型の言語を勉強するにはHaskellの「すごいHaskellたのしく学ぼう! 」が有名ですが、Scala向けに書かれたscala関数型デザイン&プログラミングもとても素晴らしい内容です。今回はこの「scala関数型デザイン&プログラミング」を題材にScalaの関数型の書き方について紹介したいと思います。
参照透過性と置換モデル
参照透過性とは、ある関数の計算に対して必ず同じ結果が返る性質のことです。
純粋関数により計算そのもののロジックが”結果を処理する方法”や”入力を取得する方法”から切り離され、それらの部分がブラックボックスになるからです。入力は”関数への引数”と言う方法でのみ取得されます。また、出力は計算され返されるだけです。
関心を切り離すことで、計算ロジックの再利用性が高まります。
変数はイミュータブルで宣言されます。
scala> val x = "Hello, World"
x: String = Hello, World
文字を反転させていますが、イミュータブルなので前回の関数の実行が2回目の実行に影響を与えてないことがわかります。
scala> val r1 = x.reverse
r1: String = dlroW ,olleH
scala> val r2 = x.reverse
r2: String = dlroW ,olleH
もちろんx
を”Hello, World”に置き換えても、結果は同じです。
scala> val r1 = "Hello, World".reverse
r1: String = dlroW ,olleH
scala> val r2 = "Hello, World".reverse
r2: String = dlroW ,olleH
この結果からr1
, r2
は参照透過ですね。
次に参照透過ではないJavaのjava.lang.StringBuilder
を試してみましょう。
scala> val a = new StringBuilder("Hello")
a: StringBuilder = Hello
scala> val b = a.append(", World")
b: StringBuilder = Hello, World
scala> val r1 = b.toString
r1: String = Hello, World
scala> val r2 = b.toString
r2: String = Hello, World
r1
とr2
は同じですね。
scala> val a = new StringBuilder("Hello")
a: StringBuilder = Hello
scala> val r1 = b.append(", World")
r1: StringBuilder = Hello, World, World
scala> val r2 = b.append(", World")
r2: StringBuilder = Hello, World, World, World
r1
とr2
が異なっています、参照とかではないようです。
java.lang.StringBuilder
のようにappend
を繰り返すと戻り値は変化してしまします。数行程度のコードなら問題ないかもしれませんが、数十行や数百行になったらどうでしょう?そのような変数の中身を検証するのが難しくなってきませんか?このように参照透過ではない副作用のある関数はコードが大きくなってくると扱いが難しくなってきます。
高階関数 (higher-order function)
Scalaの関数はfirst class citizenなので関数に関数を渡す扱い方ができます。このように扱えることをhigher-order functionと言いますが、Scalaでの書き方について紹介したいと思います。
関数型のループ
whileのようなループを関数で実現することを考えてみます。
ちなみに、ローカル関数として定義するヘルパー関数は go または loop という名前を使用することが慣例となっているようです。
nの階乗を計算する関数を書いてみます。
def factorial(n: Int): Int = {
@annotation.tailrec
def go(n: Int, acc: Int): Int =
if (n <= 0) acc
else go(n-1, n * acc)
go(n, 1) }
このような感じですね。
そして、関数の末尾でヘルパー関数の呼び出しだけにすると 末尾呼び出し と言う最適化が行われます。
もしこれに 1 + go(n, 1)
などとしてしまうとgo関数が呼び出された後に1+
の計算がされてしまい、末尾再帰にはなりません。
次に、絶対値と階乗を返す関数と、それをフォーマットして表示する関数を作ってみます。
object MyModule {
def abs(n: Int): Int =
if (n < 0) -n
else n
def factorial(n: Int): Int = {
def go( n: Int, acc: Int): Int =
if (n <= 0) acc
else go( n-1, n* acc)
go( n, 1) }
private def formatAbs(x: Int) = {
val msg = "The absolute value of %d is %d"
msg.format( x, abs(x))
}
private def formatFactorial(n: Int) = {
val msg = "The factorial of %d is %d."
msg.format( n, factorial(n))
}
}
なにやらformatAbs
とformatFactorial
はabt(x)
を実行するかfactorial(n)
を実行するかの違いだけなので、1つにまとめられそう?
まとめてみます。
def formatResult(name: String, n: Int, f: Int => Int) = {
val msg = "The %s of %d is %d."
msg.format(name, n, f(n))
}
3番目の引数でInt型を受け取ってInt型を返す関数を受け取れるようにする。
それでf(n)
とすることでその関数を実行する。
The absolute value of -42 is 42.
できました!
高階関数のパラメータには f, g, h のような名前をつけるのが慣例になっているようです。
これまでやってきた関数は 単相関数(monomorphic function) というやつです。
単相関数とは1つのデータ型を操作する関数。今まで書いたコードはInt型だけを操作します。
でも、Int型だけだと汎用的でないのでInt型以外も操作できるようにしたら便利じゃないですか?もちろんあります。。
他の型も受け取れるようにした関数を 多相関数(polymorphic function) と言います。
polymophic functionは 総称関数(generic function) と呼ばれることもあるようです。
Array[String]でStringを検索する関数です。
def findFirst(ss: Array[String], key: String): Int = {
@annotation. tailrec
def loop(n: Int): Int =
if (n >= ss. length) -1
else if (ss(n) == key) n
else loop(n + 1)
loop(0)
}
String以外の任意の型Aも検索できたら便利ですね!
Array[A]でAを検索したい!
型Aを受け取れるようにする。等価チェックをハードコーディングせずに配列の各要素を評価する関数を受け取れるようにします。
def findFirst[A](as: Array[A], p: A => Boolean): Int = {
@annotation.tailrec
def loop(n: Int): Int =
if (n >= as.length) -1
else if (p(as(n))) n
else loop(n + 1)
loop( 0)
}
key: String
をp: A => Boolean
に変更して透過チェックを関数から切り離しました。
これでString以外もチェックできるようになりました。
関数リテラル
関数リテラルはapplyメソッドをもつオブジェクトです。(a, b) => a <b> Boolean
と記述されます。
型に従う実装
特定のpolymophic functionには実装が1つだけになることがあるようです。
partial1関数を定義する
def partial1[A, B, C](a: A, f: (A, B) => C): B => C = ???
戻り値は B => CなのでB型の引数を受け取る関数リテラルが入ることになります。
def partial1[A, B, C](a: A, f: (A, B) => C): B => C = (b: B) => ???
C型の値を返したいけど、どうすればいいでしょう?
f: (A, B) => C
から取得するしかなさそうです。
???
に f: (A, B) => C
を入れてみます。
def partial1[A, B, C](a: A, f: (A, B) => C): B => C = (b: B) => f(a, b)
お、できましたね。どうもこの関数は(b: B) => f(a, b)
で実装できるようです。
また、b
の型はpartial1
関数の戻りの型から自明なのでb => f(a, b)
と書くのが一般的なようです。
def partial1[A, B, C](a: A, f: (A, B) => C): B => C = b => f(a, b)
curry
次にカリー化する関数を作ってみます。
def curry[A, B, C](f: (A, B) => C): A => (B => C) = ???
このような定義があるようです。
関数fとA型を受け取って(B型を受け取ってC型を返す関数)を返す関数を実装しなければならないようです。
文章で書くとよくわからんですね。
まずは、必ずA型を引数に取る関数なので
def curry[A, B, C](f: (A, B) => C): A => (B => C) = a => ???
でしょうか。
次に(B => C)です。同じようにBを必ず受け取るので
def curry[A, B, C](f: (A, B) => C): A => (B => C) = (a: A) => (b: B) => ???
でしょうか。
そして=> C
の部分です。どこからかC型を取ってくる必要があります。f: (A, B) => C
が使えそうです。
def curry[A, B, C](f: (A, B) => C): A => (B => C) = (a: A) => (b: B) => f(a, b)
できたっぽい?
最後に(a: A) => (b: B)
のA型、B型は自明なので省略します。
def curry[A, B, C](f: (A, B) => C): A => (B => C) = a => b => f(a, b)
scala> def curry[A, B, C](f: (A, B) => C): A => (B => C) = a => b => f(a, b)
curry: [A, B, C](f: (A, B) => C)A => (B => C)
試しにaとbを足し算する(a: Int, b: Int) => a + b
をcurryに与えてみます。
scala> val a = curry((a: Int, b: Int) => a + b)
a: Int => (Int => Int) = $$Lambda$1718/1192001678@dccf83f
お、Int型を受け取って(Int => Int)を返す関数を取得できたようです。
この関数aに 1 を与えてみます。
scala> val b = a(1)
b: Int => Int = $$Lambda$1720/1647083250@18954775
今度はIntを受け取ってIntを返 す関数を取得できました。
a => b => f(a, b)
の部分をもし状態として表すなら1 => b => f(1, b)
ですね。
さらにbに 2 を与えてみます。
scala> b(2)
res3: Int = 3
足し算ができていますね。
この時の状態は1 => 2 => f(1, 2)
です f は a + bなので正しい結果が取得できたようです。
uncurry
逆にカリー化を逆向きに行うことを考えてみます。
def uncurry[A, B, C](f: A => B => C): (A, B) => C = ???
A
とB
を受け取ってC
を返せばいいだけですね。
def uncurry[A, B, C](f: A => B => C): (A, B) => C = (a, b) => f(a)(b)
compose
次は関数を合成することを考えてみます。
def compose[A, B, C](f: B => C, g: A => B): A => C = ???
f と g を合成してC
を返せばいいようです。
g はB
を返して f は B
を引数に取るので合成できそうですね。
def compose[A, B, C](f: B => C, g: A => B): A => C = a => f(g(a))
compose
の帰り型はA => C
なのでa =>
が必要です。そして戻り値でf(g(a))
を計算しています。f(g(a))
はC
を返すので、これで完成です。
おわりに
関数型でコードを書いていると副作用のあるコードがいかに扱いにくいかが分かってきて、副作用の扱い方を意識するようになってきます。
Scalaに触れたことのある人向けの内容ですが、関数型の面白さが少しでもわかっていただけたら嬉しいです。
今回の題材の元であるscala関数型デザイン&プログラミングは特にオススメです!