Skip to content

Instantly share code, notes, and snippets.

@takezoe
Last active December 3, 2019 04:18
Show Gist options
  • Save takezoe/80b66f7b6e8fa0517139 to your computer and use it in GitHub Desktop.
Save takezoe/80b66f7b6e8fa0517139 to your computer and use it in GitHub Desktop.
Scalaの関数を活用するためのあれこれ

Scalaの関数を活用する

Functionオブジェクト

Scalaでは関数をファーストクラスオブジェクトとして扱うことができる。以下のような感じで関数やメソッドを変数に代入するとFunctionオブジェクトに変換される。実際は引数の数に応じてFunction0Function22というトレイトが用意されている。なので引数の数が22個以上の関数を変数に代入しようとするとエラーになる。

def hello(name: String) = s"Hello ${name}!"

// 関数を変数に代入
val f: Function1[String, String] = hello _

// 普通に呼び出せる
println(f("Scala")) // => Hello Scala!

// 実際はapplyメソッドが呼ばれているのでこれと同じ
println(f.apply("Scala")) // => Hello Scala!

関数を変数に代入するときは_が必要な点に注意。後述する部分適用するときにもこのプレースホルダを使うけど、すべての引数に対するプレースホルダと考えるとわかりやすいかも。

直接関数リテラルでFunctionオブジェクトを生成することもできる。

val f = (name: String) => s"Hello ${name}!"

このように関数をオブジェクトとして扱うことができるため、普通のオブジェクトと同じように

  • 変数で持ち回ったり
  • メソッドの引数に渡したり
  • メソッドの戻り値として返却したり

といったことができる。

余談ですが、別にScalaに限った話ではないけど、引数に関数を受け取ったり、戻り値として関数を返す関数のことを「高階関数」と言う。

関数を受け取る関数の例としてよく登場するのがLoanパターン。こんな感じの実装が紹介されることが多い。

def using[A <% { def close():Unit }, R](s: A)(f: A => R) {
  try f(s) finally s.close()
}

上記の例では引数の部分にcloseメソッドを持っている型という構造的部分型(Strucured Subtyping)が使われているが、それはまた別の話。今回着目するのは(f: A => R)という引数で関数を受け取っている部分。

これは「A型を引数にR型を戻り値として返す関数」なので以下のように記述しても同じ。

def using[A <% { def close():Unit }, R](s: A)(f: Function1[A, R]) {
  try f(s) finally s.close()
}

ちなみに関数オブジェクトをFunctionなどの型で扱うのはJava8のラムダも同じなので覚えておくとよい。いまのところScalaのFunction型とJava8のラムダの関数型は互換性がないのでJavaのライブラリでラムダを受け取るメソッドをScalaから呼び出す場合はちょっと不便かもしれない。そのうち相互に変換できるようになるでしょう。

やってみよう

以下のように引数に円周率を与えると円の面積を求めるための関数を返す関数を作ってみよう。

// 円周率を指定して円の半径を求める関数を生成
val f1 = area(3.14)
println(f1(10)) // => 314.0

// ゆとり仕様の関数を作成
val f2 = area(3.0)
println(f2(10)) // => 300.0

こんな感じ。

def area(pi: Double): Function1[Int, Double] => (r: Int) => pi * r * r

// こう書いたほうがわかりやすい?
def area(pi: Double): Function1[Int, Double] => {
  (r: Int) => pi * r * r
}

引数の部分適用

Scalaの関数は、引数のうち一部の引数のみ適用した関数を生成することができる。たとえば先ほどの円の面積を求める関数を、部分適用で同じことをやってみる。

def area(pi: Double, r: Int): Double = pi * r * r

// 第一引数piに3.14を適用した関数を生成
val f1 = area(3.14, _: Int)
println(f1(10)) // => 314.0

// ゆとり仕様
val f2 = area(3.0, _: Int)
println(f2(10)) // => 300.0

部分適用は、一部の引数を固定していると考えるとわかりやすいかも。_はプレースホルダと呼び、未確定の引数を表す。

もう少し実用的な例を。たとえば以下のようにJDBCの接続情報とSQLを受け取って実行する関数があった場合、JDBCの接続情報部分だけを部分適用した関数を生成して使い回すことで毎回同じ引数を渡す必要がなくなる。

// JDBC接続情報とSQLを受け取ってSQLを実行する関数
def executeSQL(
  // JDBC接続情報
  jdbcURL: String, jdbcUser: String, jdbcPassword: String,
  // 実行するSQL
  sql: String
) = { ... }

// JDBC接続情報を部分適用した関数を生成
val execute = executeSQL(
  // JDBC接続情報を部分適用
  "jdbc:mysql://localhost/db1", "sa", "",
  // SQLにはプレースホルダを指定
  _: String)

// SQLを渡すだけで実行できる
execute("UPDATE ...")

関数の合成

ここまで説明したように、Scalaでは関数やメソッドをオブジェクトとして扱うことができるが、さらにそれらの関数を合成することができる。

たとえばある関数の戻り値を別の関数に渡したいという場合、なにも考えずに書くと以下のようになるのではないかと。

def f1(i: Int): Int = { i + 1 }
def f2(i: Int): Int = { i * 2 }

// 関数aの実行結果にbを適用する
orintln(f2(f1(10))) .. => 22

もしくは汎用的なものであれば複数の関数を呼び出す新しい関数を定義してもいいかもしれません。

def f3(i: Int) = f2(f1(i))
println(f3(10)) // => 22

ScalaのFunctionオブジェクトには関数を合成するためのcomposeというメソッドが用意されている。

def f1 = (i: Int) => { i + 1 }
def f2 = (i: Int) => { i * 2 }

val f3 = (f2 _) compose (f1 _)
println(f3(10)) // => 22

Functionオブジェクトに変換するのにいちいち_を書くのがダルいのでこういう場合は最初からvalで関数を定義しておいた方がよいかも。そうするとこんな感じで書ける。

val f1 = (i: Int) => { i + 1 }
val f2 = (i: Int) => { i * 2 }

val f3 = f2 compose f1
println(f3(10)) // => 22

これだと関数の適用順序と見た目が逆になるので直感的じゃないよね、という場合はandThenを使うと適用する順番で書ける。

val f3 = f1 andThen f2
println(f3(10)) // => 22

これらのメソッドを使うとスマートに関数を合成できるし、引数と戻り値の型さえあっていれば動的に関数を合成することもできるのでより柔軟性の高いプログラミングが可能になる。

やってみよう

任意の数の関数を受け取り、それらを順に適用する関数を返すchainFunction関数を作ってみよう。使い方はこんな感じ。

val f1 = (i: Int) => { i + 1 }
val f2 = (i: Int) => { i * 2 }
val f3 = (i: Int) => { i - 1 }

val f4 = chainFunction(f1, f2, f3)
println(f4(10)) // => 21

実装例は以下のような感じ。こんな感じでソースコード上でハードコードで関数を組み合わせるのではなく、動的に組み合わせることができるのが関数合成のメリット。使いこなせるとカッコいいライブラリが作れるかもしれない。

def chainFunction[T](f: Function[T, T]*) = {
  f.reduce((a, b) => a andThen b)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment