LoginSignup

This article is a Private article. Only a writer and users who know the URL can access it.
Please change open range to public in publish setting if you want to share this article with other users.

More than 5 years have passed since last update.

第8章<br />関数とクロージャー

Last updated at Posted at 2017-03-03
1 / 27

自己紹介

  • 五十嵐 智哉
  • クーガー株式会社
  • AI・Robotics関連の開発
    • DeepLearning向け3DCG学習データ生成シミュレーター開発
    • ロボット向けクラウドデータベース及び認識エンジンのためのクライアント及びサーバーサイド開発
  • Scala/Play Framework, C#/Unity, C++, Python

この章では、Scalaが提供する様々なバリエーションの関数や記述方法についてみていく。

Agenda

  1. メソッド
  2. ローカル関数
  3. First-class Function
    • 関数リテラルと関数値(値としての関数)
  4. 関数リテラルの短縮形
  5. 部分適用された関数
  6. クロージャー
  7. 関数呼び出しの特殊な形態
  8. 末尾再帰

メソッド

オブジェクトのメンバーとして定義される関数のこと。

REPL
class Hoge {
  def foo(x: Int) = x + 1
}
val hoge = new Hoge
hoge.foo(1) // 2

最も一般的な関数定義のやり方。


ローカル関数

関数内に関数を定義すること。

REPL
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(一人前の存在としての関数)と呼ばれる。

REPL
val f = (x: Int) => x + 1
f(1) // 2
f.apply(1)
  • 内部的にはFunctionNトレイトを拡張する何らかのクラスになっている。

First-class Function

多くのScalaライブラリーは、First-class Functionとして使えるように設計されている。

REPL
val xs = Seq(1, 2, 3)
xs.foreach((x: Int) => println(x))
val p = (x: Int) => println(x)
xs.foreach(p)
REPL
val xs = Seq(1, 2, 3)
xs.filter((x: Int) => x > 1)

やってみよう

"Now I need a drink, alcoholic of course,"という文を単語に分解し,各単語の文字数を先頭から出現順に並べたリストを作成してください。

言語処理100本ノック 2015より

ヒント

  • 単語の分解にはString#splitが使えます。
  • カンマが含まれていることに気をつけてください。(String#countChar#isLetterが使えます。)

やってみよう 解答編

REPL
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)) // 次ページで詳しく。。。

関数リテラルの短縮形

  • パラメーターの型の省略(ターゲットによる型付け)
REPL
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]と分かっているのでパラメータの型を省略することができる。
さらに型推論されたパラメーターのカッコも省略することができる。


関数リテラルの短縮形

  • プレースホルダー構文
REPL
val xs = Seq(1, 2, 3)
xs.filter(x => x > 1)
xs.filter(_ > 1)

パラメーターが関数リテラル内で1度しか使われない場合、プレースホルダーとしてアンダースコアを使うことができる。

REPL
// val f = _ + _ (compile error)
val f = (_: Int) + (_: Int)

また型推論できない場合、コロンを使って型を指定することができる。


部分適用された関数

関数が必要とするすべての引数を渡していない関数呼び出し式のこと。

REPL
val xs = Seq(1, 2, 3)
xs.foreach(println(_))
xs.foreach(println _)

println(_)println _との違いは、単一のパラメーターに対するプレースホルダーか、println関数のパラメーターリスト全体に対するプレースホルダーか、という点である。
またこのことを、1引数必要な関数に0個の引数を適用した部分関数式という。


部分適用された関数

同じように2引数以上の引数を持つ関数に、1つ以上の引数を適用した関数も作ることができる。

REPL
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

部分適用された関数

また、関数呼び出しが必要な場所では_も省略することができるが、それ以外の場所では省略することができない。

REPL
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 _

クロージャー

自由変数の束縛を取り込んで、関数リテラルを閉じること。

  • 自由変数
    関数パラメーターで定義された以外の変数
REPL
// val f = (x: Int) => x + more (compile error)
var more = 1
val f = (x: Int) => x + more
f(10) // 11

今までの関数リテラルは、自由変数を含まないので厳密にはクロージャーと呼ばない。


クロージャー

Scalaでは変数自体をつかんでいるので、クロージャー作成後に変数の内容を変更するとその変更が反映される。

REPL
var more = 1
val f = (x: Int) => x + more
f(10) // 11
more = 9999
f(10) // 10009

クロージャー

また、クロージャーの外から、クロージャーがつかんだ変数に対して加えた変更を見ることもできる。

REPL
val xs = Seq(1, 2, 3)
var sum = 0
xs.foreach(sum += _)
sum // 6

クロージャー

実行とともに実体が変わっていく変数をつかんだ場合、作成された時にアクティブだったインスタンスをつかむ。

REPL
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は生存している。


関数呼び出しの特殊形態

  • 連続パラメーター(可変長引数リスト)
REPL
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コンパイラーに連続パラメーターであることを指示する必要がある。


関数呼び出しの特殊形態

  • 名前付き引数

名前付き引数で関数呼び出しすると引数の定義順と異なる順番で呼び出すことができる。

REPL
def hoge(a: Int, b: Int) = a - b
hoge(1, 2) // -1
hoge(b = 2, a = 1) // -1

関数呼び出しの特殊形態

  • パラメーターのデフォルト値

前ページの名前付き引数と組み合わせて使われることが多い。

REPL
def hoge(a: Int = 1, b: Int = 1) = a - b
hoge() // 0
hoge(a = 2) // 1
hoge(b = 2) // -1

末尾再帰

再帰関数の中でも、最後の処理として自分自身を呼び出すこと。

REPL
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コンパイラーにより末尾呼び出し最適化が実行される。なので、スタックオーバーフローが発生しない。

REPL
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では以下のように末尾再帰か否かがわかります。

Screen_Shot_2017-03-02_at_15_11_56.png


やってみよう

n! \\
(e.g.) \space 5 \times 4 \times 3 \times 2 \times 1 = 120

再帰を用いて階乗を実装してください。
できれば末尾再帰で実装してみてください。


やってみよう 解答編

REPL
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を使うように作られているので、ぜひマスターしましょう!

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up