40
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

グロービスAdvent Calendar 2018

Day 24

【Swift】クロージャとカリー化と部分適用と高階関数

Posted at

#TL;DR
Swiftのクロージャを積極的に使って行けるようになるための基礎から応用まで。
クロージャからカリー化高階関数までを理解する。

##1.クロージャ

  • 動作を表しているデータのこと
  • 実行可能なコードと、それが記述された箇所の環境を取り込んで、後から評価できるように保存したもの。
  • 変数に関数を代入できる。
  • 関数の引数に関数を指定できる。
  • 関数の戻り値に関数を指定できる。
  • Swiftの言語機能の中心的な存在。
  • クロージャはインスタンス

###クロージャと関数の違い

  • プログラムの振る舞いを動的に変更することができること。
  • 常に同じ機能のインスタンスが作られるわけではない。
  • インスタンスが生成される際、クロージャの外側にある変数の値を取り込んでインスタンスの一部とし、インスタンスが呼び出される時にはいつでも値を取り出して使うことができる。

###使いどころ

  • ユーザが引数を与えてカスタマイズ可能な**自由度の高い「関数」**を生成したい場合
  • 前回、呼び出されて実行されたときの演算結果(値)を内部で保存して、次に呼び出されたときに、前回の結果(値)に対して、さらに同じ処理(演算)を行う関数を生成したい場合
書式
{
  (仮引数: 型) -> 戻り値の型 in
    処理
}
凡例
//変数にクロージャを代入する。
var c1 = { () -> () in print("hello") }
//クロージャを実行する
c1() // "hello"と表示

・c1に代入されているのがクロージャのインスタンス
・代入の右辺はクロージャ式と呼ばれる。
・全体は1つのコードブロックのように囲む
・短いものは1行、関数のように複数行に渡ってもOK
・実行する文を記述する前に「in」というキーワードが必要
・クロージャのインスタンスが格納された変数に実引数列を適用すると実行できる。*実引数列はここで言うc1()の**()**のこと
・クロージャ式を書いた直後に実引数列を適用することは不可

直後に実行は不可
{ () -> () in print("hi")}()

・クロージャを評価した結果を変数に代入するのはOK

代入がある場合に実行は可
var pi: Double = { () -> Double in atan(1.0)*4.0 }()

###クロージャの仮引数と返り値の方宣言
####省略できるケース
(1)ブロック内に文が1つしか存在しない場合

ブロック内に文が1つしか存在しない場合の省略(全て同義)
var c1 = { () -> () in print("hi") }
var c1 = { () -> Void in print("hi") }
var c1 = { () in print("hi") }
var c1 = { print("hi") }
同義
var pi = { () -> Double in atan(1.0)*4.0 }()
var pi = { atan(1.0)*4.0 }()

(2)返すべき型が推論できる場合、省略可能

let c2 = { (a: Int, b: Int) -> Double in
  if b == 0 { return 0.0 }
  return Double(a) / Double(b)
}

var c3: Int = { //Int型を返すクロージャであると推論される
  return Int(c2(9, 4))
}() //クロージャ式を直接評価する

※クロージャのreturnの値から推論することはできない。クロージャが使われている外部の状況から推論が行われる。


***** ####クロージャと関数の型 ・クロージャと関数は同じように実行することができる。 ・クロージャと関数を区別する必要はない。→同等のものとして扱うことで柔軟なプログラミングが行える。
定数に関数を代入
func f2(a: Int) -> Double { return Double(a) / 100.0 }
let c3: (Int) -> Double = f2
let c4: Int -> Double = f2

*クロージャのインスタンスは参照型。メモリ管理もARCを使って行われる。


####クロージャの複雑な型宣言

クロージャのオプショナル宣言
var cp1: (Int, Int) -> Double!

引数がInt型2つでDouble型のオプショナル型を返すクロージャの意味。変数はオプショナルではない。

変数がオプショナルのクロージャ宣言
var cp2: ((Int, Int) -> Double)!

cp2がオプショナルでnilを代入できる。

・クロージャはインスタンスなので配列に格納することも可能。

クロージャを配列に格納
var ca3 = [(Int, Int) -> Double]() //エラーになる

//typealiasを使ってクロージャを配列に格納
typealias MyClosure = (Int, Int) -> Double
var ca3 = [MyClosure]()

***** ###変数のキャプチャ ####キャプチャとは インスタンスが生成される際、クロージャの外側にある変数の値を取り込んでインスタンスの一部とし、インスタンスが呼び出される時にはいつでも値を取り出して使うことができる。=>キャプチャ

####クロージャが個別にローカル変数をキャプチャする場合

変数のキャプチャを調べる例
var globalCount = 0

//()->Int という型を持つクロージャのインスタンスを作って返す関数
fun maker(a: Int, _ b: Int) -> () -> Int {
  var localvar = a
  return { () -> Int in
    globalCount++
    localvar += b
    return localvar
  }
} 

//呼び出し
var m1 = maker(10, 1)
print("m1() = \(m1()), glocalCount = \(glocalCount)")
print("m1() = \(m1()), glocalCount = \(glocalCount)")
globalCount = 1000
print("m1() = \(m1()), glocalCount = \(glocalCount)")

var m2 = maker(50, 2)
print("m2() = \(m2()), glocalCount = \(glocalCount)")
print("m1() = \(m1()), glocalCount = \(glocalCount)")
print("m2() = \(m2()), glocalCount = \(glocalCount)")
//結果
//m1() = 11, glocalCount = 1
//m1() = 12, glocalCount = 2
//m1() = 13, glocalCount = 1001
//m2() = 52, glocalCount = 1002
//m1() = 14, glocalCount = 1003
//m2() = 54, glocalCount = 1004

・localvarとbがキャプチャされている。
・globalCountはm1とm2で呼び出すたびに増加している(共有されている)。

####複数のクロージャが同じローカル変数をキャプチャする場合

変数のキャプチャを調べる例2
var m1: (() -> ())! = nil //クロージャを代入する変数
var m2: (() -> ())! = nil //クロージャを代入する変数

fun makerW(a: Int) {
  var localvar = a
  m1 = { localvar += 1; print("m1: \(localvar)") }
  m2 = { localvar += 5; print("m2: \(localvar)") }
  m1()
  print("--: \(localvar)")
  m2()
  print("--: \(localvar)")
}

//実行
makerW(10) //クロージャを生成する
m1() //クロージャの呼び出し
m2()
m1()
//結果
//m1: 11
//--: 11
//m2: 16
//--: 16
//m1: 17
//m2: 22
//m1: 23

・関数makerWは2つのクロージャのインスタンスを生成してそれぞれ変数m1とm2に代入している。
・関数makerW内でクロージャを代入した変数m1とm2を1回ずつ呼び出している。


####クロージャが参照型の変数をキャプチャする場合
・クロージャが参照型の値を強い参照で保持することの確認。

変数のキャプチャを調べる例(3)
class MyInt {
	var value = 0
	init(_ v: Int) { value = v }
	deinit{ print("deinit: \(value)") } //開放時に表示
}
func makerZ(a: MyInt, _ s: String) -> () -> () {
	let localvar = a
	return { print("\(s): \(++localvar)") }
}

//実行
var obj: MyInt! = MyInt(10) //クラスのインスタンス
var m1: (() -> ())! = makerZ(obj, "m1") //クロージャを変数m1に代入
m1() //[m1: 11]と出力
var m2: (() -> ())! = makerZ(obj, "m2") //クロージャを変数m2に代入
obj = nil //MyIntのインスタンスはまだ解放されていない
m2() //[m2: 12]と出力
m1() //[m1: 13]と出力
m1 = nil
m2() //[m2: 14]と出力
m2 = nil //[deinit: 14]と出力。MyIntのインスタンスが解放された。

***** ####ネスト関数とクロージャ ・ネスト関数(内部定義関数)
ネスト関数が変数をキャプチャする例
func maker(a: Int, b: Int) -> () -> Int {
    var localvar = a
    func localfunc() -> Int {   //ネスト関数
        globalCount++           //globalCountは参照されるだけ
        localvar += b           //localvar, bがキャプチャされる
        return localvar
    }
    return localfunc
}

***** ####メソッドとクロージャ
インスタンスメソッドを持つ簡単なクラスの例
class Person {
  let name: String
  init(_ s: String) { name = s }
  deinit{ print("deinit", name) }
  func greet(msg:String) {
    print("\(msg), \(name)さん")
  }
}

//実行
var p: Person! = Person("織田")
var clo: ((String) -> ())! = p.greet //クロージャとして扱う
p = nil //変数pからインスタンスへの参照をやめる
clo("こんばんは")
clo = nil //変数cloからクロージャへの参照をやめる

//結果
こんばんは, 織田さん
deinit 織田

Personインスタンスは、変数cloがメソッドを参照するのをやめるまでは存在している。
*クラスのインスタンスメソッドはクロージャのように扱うことができる。
・インスタンスメソッドがクロージャのインスタンスとして変数に格納されている間は対応するクラスのインスタンスは解放されない。
・構造体などのインスタンスメソッドもクロージャとして扱えるがデータ型のため循環参照問題は起こらない。


***** ####イニシャライザとクロージャ 構造体やクラスのイニシャライザもクロージャとして扱うことができる。
イニシャライザとクロージャ
func newPerson(s: String) -> Person { return Person(s) }

let gen1: (String) -> Person = newPerson
let gen2: (String) -> Person = Person.init
let p1 = gen1("秀吉")
let p2 = gen2("官兵衛")
p1.greet("それがし、")
p2.greet("それがし、")

/**結果
それがし、秀吉
それがし、官兵衛
*/

*****

###クロージャの使い方と記法

引数リストの省略
var clos: (Int, Int) -> String = { (a: Int, b: Int) -> String in return "\(a)/\(b)" }

var clos = { (a: Int, b: Int) -> String in "\(a)/\(b)" }

var clos = { (a: Int, b: Int) in "\(a)/\(b)" }

//引数が2つ渡させるクロージャ
var clos: (Int, Int) -> String = { "\($0)/\($1)" }

####配列要素の選択
例)ある条件に合致した要素だけを配列から取り出して新しい配列を作る方法。filterメソッドで引数としてクロージャを1つ指定する。

末尾に「.csv」があるものだけを抽出
let source = list.filter( { $0.hasSuffix(".csv")} )
選択したいファイルの拡張子が動的に決まる場合
let ext = 条件 ? ".swift" : ".csv"
let source = list.filter({ $0.hasSuffix(ext) })
条件に当てはまる要素の配列とそれ以外の要素の配列
func select(list: [String], _ filter: String -> Bool) -> ([String], [String]) {
  var sel = [String]()
  var des = [String]()
  for s in list {
    if filter(s) {
      sel.append(s)
    } else {
      des.append(s)
    }
  }
  return (sel, des)
}

//実行(「.」が含まれているものとそうでないものに分ける)
let list = ["test.pdf", "filedata.csv", "Zzz", "hogehoge.csv"]
let t = select(list, {
  for ch in $0.characters {
    if ch == "." { return true }
  }
  return false
})

####接尾のクロージャ
クロージャを関数やメソッドの引数として渡す場合、クロージャ式が引数の最後であれば下記のような特別な記述方法が利用出来る。

接尾クロージャサンプル
let t = select(list) {
  for ch in $0.characters {
    if ch == "." { return true }
  }
  return false
}

*引数がクロージャ式だけという場合には、引数リスト自体を省略できる。下記は同じ意味になる。

let source = list.filter({ $0.hasSuffix(ext) })
let source = list.filter{ $0.hasSuffix(ext) }

この記述方法を接尾クロージャまたはトレイリングクロージャと呼ぶ。

####エラーを投げるクロージャ

実行中にエラーを投げるクロージャ
let qux = {(a: Int) throws -> Double in
  guard a != 0 else { throw XError.Zero }
  return Double(a) + 0.1
}
if let d = try? qui(1) { print(d) } //1.1が表示される

####rethrowのある関数
エラーのあるクロージャを引数とする関数が、そのクロージャを実行した結果エラーを伝播させることができる。
関数がエラーを再スローするという。

func apply(inout arg: [Int], _ map: Int throws -> Int) rethrows {
  for i in 0 ..< arg.count {
    arg[i] = try map(arg[i]) //ここでエラーが発生する可能性。
  }
}
var list = [1, 2, 3, 4, 5]
let bell = { (a: Int) throws -> Int in
  let q = a * a
  guard q < 256 else { throw XError.Byte } 
  return q
}
if let _ = try? apply(&list, bell) {
  print(list)
} //[1, 4, 9, 16, 25]を表示。

throwの他にrethrowという宣言が存在する理由。
再スローのある関数は引数にエラーのないクロージャを指定すると、エラーのない関数として動作する

var list = [1, 4, 9, 16, 25]
let aiz = { (a:Int) -> Int in
  return (a * a) & 0xff 
}
apply(&list, aiz)
print(list) //[1, 16, 81, 0, 113]を表示。

####配列要素に対する操作

[map]
配列の各要素に次々にクロージャを適用し、その結果を新たな配列として返すメソッド。

let dat = [2, 80, 45, 13]

//Int型の配列を文字列に変更する
let str = dat.map{ "\($0)" } //["2", "80", "45", "13"]が返る

//10以上のデータを取り出し、昇順に並べ替えて文字列に変換する。
let str = dat.filter{ $0 >= 10 }.sort( < ).map{ "\($0)" }

[reduce]
配列の各要素に次々にクロージャを適用し、最終的に一つの値を得るメソッド。

//1から10までの整数が格納された配列に対して和を求める
let numbers = [Int](1...10) //1...10
let sum = numbers.reduce(0){ $0 + $1 } //和を求めるため初期値は0。結果は55

####インスタンス列を使ったプログラミングスタイル
SequenceTypeプロトコル(配列やリストなど)に適合した型に対しては、要素の列に対する操作をパイプラインのように結合して、全体としてより複雑な処理を実現することができる。
ループを作ってアルゴリズムを記述するよりも比較的容易に見通しよく機能を実現することができる。

メソッド名 内容
filter -> [T] selfの持つ要素に引数のクロージャを適用して、trueを返す要素だけを取り出して新しい配列として返す
map -> [U] selfの持つ要素に引数のクロージャを適用して、U型のデータを生成し、これらを新しい配列として返す
sort(T, T) -> [T] T型の要素間で大小比較するクロージャを引数として、selfの持つ要素を並べ直してできた新しい配列を返す
sort() -> [T] 要素の型TがComparableに適合する場合、この形式が利用できる
reduce(U, T) -> U 第一引数を初期値として、その値とselfの第一要素にクロージャを適用して新しい値を作る。

##カリー化(currying)
・カリー化 = 引数の一部を与えることによって関数を返す関数を定義すること。
・関数の引数の一部分にだけ実引数を適用し、残りの引数を後から適用することができるようなクロージャを得る計算手法を部分適用と呼ぶ。
・引数a、bを持ち、結果rを返す関数があった時、その関数をカリー化した関数とは、引数がaで、「引数bを持ち、結果を返す関数」を返すような関数。
・複数の引数を1つに減らすこと
・カリー化された関数によって、簡単に別の機能を持つ関数を作れる。

例1
//カリー化前
func times(a: Double, _ b: Double) -> Double {
  return a * b
}

//カリー化後
func timesCurried(a: Double) -> Double -> Double {
  func times(b: Double) -> Double {
    return a * b
  }
  return times
}

//カリー化進化
func timesCurried(a: Double)(_ b: Double) -> Double {
  return a * b
}

//カリー化実行
print(times(2.54, 8.0)) //20.32を表示
let f = timesCurried(2.54) //fはクロージャ
print(f(8.0)) //20.32を表示

//簡略化した場合の実行
var inch = timesCurried(2.54)
print(inch(20.0)) //50.8を表示
例2
//カリー化前
func addTwoNumbers(x: Int, y: Int) -> Int {
  return x + y
}

//カリー化後
func add(x: Int) -> (Int -> Int) {
  func addToX(y :Int) -> Int {
    return x + y
  }
  return addToX
}

//カリー化進化
func add(x: Int) -> (Int -> Int) {
  return { (y: Int) -> Int in x + y }
}

//カリー化実行
let add7 = add(7)
add7(3)   // 10
add7(5)   // 12

//簡単に別の機能を持つ関数を作れる
let add7 = add(7)
let add10 = add(10)
let add100 = add(100)
例3(引数が3つ以上)
//カリー化前
func m3(a: Int, _ b: Int, _ c: Int) -> Int {
  return (a+b)*c
}

//カリー化後
func m3curried(a: Int)(_ b: Int, _ c: Int) -> Int {
  return (a+b)*c
}

//実行
var m3with10 = m3curried(10) //クロージャインスタンス生成
print(m3with10(5, 4))

##高階関数
・クロージャはデータの一種
・クロージャーは引数としてクロージャーを受け取ることも可能
・引数としてクロージャーを受け取るクロージャーや、戻り値としてクロージャーを返すクロージャーは「高階関数」と呼ばれる。

クロージャーを2回呼び出す関数
func twice(c: Int -> Int, n: Int) -> Int {
  return c(c(n))
}

//実行
twice({ $0 * $0 }, n: 3)
//結果
81
加算をするクロージャーを返す関数
func add(n: Int) -> Int -> Int {
  return { n + $0 }
}

//実行
add(5)(3)
//結果
8
クロージャーを合成する関数
func compose(a: Int -> Int, b: Int -> Int) -> Int -> Int }
  return { a(b($0)) }
}
//実行
compose({ $0 + 77 }, b: { $0 * 100 })(42)
//結果
4277
40
17
0

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
40
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?