自己紹介
- 五十嵐 智哉
- GitHub
- クーガー株式会社
- AI・Robotics関連の開発
- DeepLearning向け3DCG学習データ生成シミュレーター開発
- ロボット向けクラウドデータベース及び認識エンジンのためのクライアント及びサーバーサイド開発
- Scala/Play Framework, C#/Unity, C++, Python
この章では、Scalaが提供する様々なバリエーションの関数や記述方法についてみていく。
Agenda
- メソッド
- ローカル関数
- First-class Function
- 関数リテラルと関数値(値としての関数)
- 関数リテラルの短縮形
- 部分適用された関数
- クロージャー
- 関数呼び出しの特殊な形態
- 末尾再帰
メソッド
オブジェクトのメンバーとして定義される関数のこと。
class Hoge {
def foo(x: Int) = x + 1
}
val hoge = new Hoge
hoge.foo(1) // 2
最も一般的な関数定義のやり方。
ローカル関数
関数内に関数を定義すること。
class Hoge {
def foo(x: Int) = {
def bar = x + 1
bar
}
}
val hoge = new Hoge
hoge.foo(1) // 2
関数内のスコープで閉じているので名前空間を汚染しない。
また外側の関数パラメーターにアクセスできるので、関数定義が簡潔になる。
First-class Function
def
で定義するのではなく、関数リテラルを記述し、インスタンス化して関数値として扱うことができる。
その他の値(IntやString、Vectorなど
)と同じように関数を扱えることから**First-class Function(一人前の存在としての関数)**と呼ばれる。
val f = (x: Int) => x + 1
f(1) // 2
f.apply(1)
- 内部的には
FunctionN
トレイトを拡張する何らかのクラスになっている。
First-class Function
多くのScalaライブラリーは、First-class Functionとして使えるように設計されている。
val xs = Seq(1, 2, 3)
xs.foreach((x: Int) => println(x))
val p = (x: Int) => println(x)
xs.foreach(p)
val xs = Seq(1, 2, 3)
xs.filter((x: Int) => x > 1)
やってみよう
"Now I need a drink, alcoholic of course,"という文を単語に分解し,各単語の文字数を先頭から出現順に並べたリストを作成してください。
ヒント
- 単語の分解には
String#split
が使えます。 - カンマが含まれていることに気をつけてください。(
String#count
、Char#isLetter
が使えます。)
やってみよう 解答編
val xs = "Now I need a drink, alcoholic of course,".split(" ")
val ys = xs.map((str: String) => str.count((c: Char) => c.isLetter))
val zs = xs.map(_.count(_.isLetter)) // 次ページで詳しく。。。
関数リテラルの短縮形
- パラメーターの型の省略(ターゲットによる型付け)
val xs = Seq(1, 2, 3)
xs.filter((x: Int) => x > 1)
xs.filter((x) => x > 1)
xs.filter(x => x > 1)
ScalaコンパイラーがSeq[Int]
と分かっているのでパラメータの型を省略することができる。
さらに型推論されたパラメーターのカッコも省略することができる。
関数リテラルの短縮形
- プレースホルダー構文
val xs = Seq(1, 2, 3)
xs.filter(x => x > 1)
xs.filter(_ > 1)
パラメーターが関数リテラル内で1度しか使われない場合、プレースホルダーとしてアンダースコアを使うことができる。
// val f = _ + _ (compile error)
val f = (_: Int) + (_: Int)
また型推論できない場合、コロンを使って型を指定することができる。
部分適用された関数
関数が必要とするすべての引数を渡していない関数呼び出し式のこと。
val xs = Seq(1, 2, 3)
xs.foreach(println(_))
xs.foreach(println _)
println(_)
とprintln _
との違いは、単一のパラメーターに対するプレースホルダーか、println
関数のパラメーターリスト全体に対するプレースホルダーか、という点である。
またこのことを、1引数必要な関数に0個の引数を適用した部分関数式という。
部分適用された関数
同じように2引数以上の引数を持つ関数に、1つ以上の引数を適用した関数も作ることができる。
def sum(a: Int, b: Int, c: Int) = a + b + c
sum(1, 2, 3) // 6
val a = sum _
a(1, 2, 3) // 6
val b = sum(1, _: Int, 5)
b(3) // 9
b(9) // 15
部分適用された関数
また、関数呼び出しが必要な場所では_
も省略することができるが、それ以外の場所では省略することができない。
val xs = Seq(1, 2, 3)
xs.foreach(println _)
xs.foreach(println)
def sum(a: Int, b: Int, c: Int) = a + b + c
// val a = sum (compile error)
val a = sum _
クロージャー
自由変数の束縛を取り込んで、関数リテラルを閉じること。
- 自由変数
関数パラメーターで定義された以外の変数
// val f = (x: Int) => x + more (compile error)
var more = 1
val f = (x: Int) => x + more
f(10) // 11
今までの関数リテラルは、自由変数を含まないので厳密にはクロージャーと呼ばない。
クロージャー
Scalaでは変数自体をつかんでいるので、クロージャー作成後に変数の内容を変更するとその変更が反映される。
var more = 1
val f = (x: Int) => x + more
f(10) // 11
more = 9999
f(10) // 10009
クロージャー
また、クロージャーの外から、クロージャーがつかんだ変数に対して加えた変更を見ることもできる。
val xs = Seq(1, 2, 3)
var sum = 0
xs.foreach(sum += _)
sum // 6
クロージャー
実行とともに実体が変わっていく変数をつかんだ場合、作成された時にアクティブだったインスタンスをつかむ。
def makeIncreaser(more: Int) = (x: Int) => x + more
val inc1 = makeIncreaser(1)
inc1(10) // 11
val inc9999 = makeIncreaser(9999)
inc9999(10) // 10009
つかんだmore
はスタックではなくヒープに再配置されるため、makeIncreaser
呼び出し後でもmore
は生存している。
関数呼び出しの特殊形態
- 連続パラメーター(可変長引数リスト)
def echo(args: String*) = for (arg <-args) println(arg)
echo()
echo("hoge")
echo("foo", "bar")
val arr = Array("hoge", "foo", "bar")
// echo(arr) (compile error)
echo(arr: _*)
関数の内部では、args: String*
はArray[String]
型になっているが、引数にArray[String]
型を渡すことはできない。
代わりにxxx: _*
という記法でScalaコンパイラーに連続パラメーターであることを指示する必要がある。
関数呼び出しの特殊形態
- 名前付き引数
名前付き引数で関数呼び出しすると引数の定義順と異なる順番で呼び出すことができる。
def hoge(a: Int, b: Int) = a - b
hoge(1, 2) // -1
hoge(b = 2, a = 1) // -1
関数呼び出しの特殊形態
- パラメーターのデフォルト値
前ページの名前付き引数と組み合わせて使われることが多い。
def hoge(a: Int = 1, b: Int = 1) = a - b
hoge() // 0
hoge(a = 2) // 1
hoge(b = 2) // -1
末尾再帰
再帰関数の中でも、最後の処理として自分自身を呼び出すこと。
def boom(x: Int): Int = {
if (x == 0) throw new Exception("boom!")
else boom(x - 1) + 1
}
def bang(x: Int): Int = {
if (x == 0) throw new Exception("bang!")
else bang(x - 1)
}
#末尾再帰
Scalaコンパイラーにより末尾呼び出し最適化が実行される。なので、スタックオーバーフローが発生しない。
boom(3)
/* callstackに関数が積まれている。
java.lang.Exception: boom!
at .boom(<console>:12)
at .boom(<console>:13)
at .boom(<console>:13)
at .boom(<console>:13)
... 42 elided
*/
bang(3)
/* 末尾呼び出し最適化により、callstackの関数が一つだけ。
java.lang.Exception: bang!
at .bang(<console>:12)
... 42 elided
*/
末尾再帰
また、Scalaコンパイラーによりvar, whileループ
と同程度の速度で実行されるコードがコンパイルされるので、リーダブルになる末尾再帰で書くことをおすすめ。
- ちなみにIntelliJでは以下のように末尾再帰か否かがわかります。

やってみよう
n! \\
(e.g.) \space 5 \times 4 \times 3 \times 2 \times 1 = 120
再帰を用いて階乗を実装してください。
できれば末尾再帰で実装してみてください。
やってみよう 解答編
def fact(x: Int) = {
def factTail(x: Int, acc: BigInt): BigInt =
if (x == 0) acc else factTail(x - 1, x * acc)
factTail(x, 1)
}
fact(5)
まとめ
Scalaでは関数表現を多様化させるために、様々な方法で関数を扱えるようなっている。
また、多数のScala標準ライブラリーがFirst-class Functionを使うように作られているので、ぜひマスターしましょう!