함수형 프로그래밍 패러다임과 클로저의 관계
- 스위프트에서 함수형 프로그래밍 패러다임을 접할 때 첫걸음으로 꼭 알아야할 클로저!
- 클로저와 제네릭, 프로토콜, 모나드 등의 결합으로 스위프트는 훨씬 강력한 언어가 되었다.
함수와 클로저?
- 함수는 클로저의 한 형태!
클로저?
변수나 상수가 선언된 위치에서 참조를 획득하고 저장할 수 있다.
이를 변수나 상수의 클로징(잠금)이라고 하며, 여기서 착안된 이름 '클로저'
클로저의 형태
1. 이름이 있으면서 어떤 값도 획득하지 않는 전역함수의 형태
2. 이름이 있으면서 다른 함수 내부의 값을 획득할 수 있는 중첩된 함수의 형태
3. 이름이 없고 주변 문맥에 따라 값을 획득할 수 있는 축약 문법으로 작성한 형태
1️⃣ 클로저의 표현방법 (위치를 기준으로)
1. 기본 클로저
클로저 표현
{ (매개변수들) -> 반환 타입 in
실행 코드
}
정렬을 위한 함수 전달 (sorted(by:) 매서드 이용)
func backwards(first: String, second: String) -> Bool {
return first > second
}
let names: [String] = ["yejin", "zejin", "hejin"]
let reversed: [String] = names.sorted(by: backwards)
print(reversed)
// ["zejin", "yejin", "hejin"]
sorted(by:)메서드에 클로저 전달
let names: [String] = ["yejin", "zejin", "hejin"]
// backwards(first:second:) 함수 대신에 sorted(by:) 매서드의 전달인자로 클로저를 직접 전달
let reversed: [String] = names.sorted(by: {
(first: String, second: String) -> Bool in
return first > second
})
print(reversed)
➰클로저도 함수와 마찬가지로 입출력 매개변수를 사용할 수 있다. 매개변수 이름을 지정한다면 가변 매개변수 또한 사용 가능! 다만, 클로저는 매개변수 기본값을 사용할 수 없다.
➰전달인자를 함수로 보낸다?
- 함수를 매서드의 전달인자로 보내는 일은 함수형 프로그래밍 패러다임에서는 아주 당연한 일!
2. 후행 클로저
후행클로저의 사용
- 클로저가 조금 길어지거나 가독성이 조금 떨어진다 싶을 때 사용!
후행 클로저는 맨 마지막 전달인자로 전달되는 클로저에만 해당되므로 전달인자로 클로저 여러 개를 전달할 때는 맨 마지막 클로저만 후행 클로저로 사용할 수 있다.
또한, sorted(by:) 매서드처럼 하나의 클로저만 전달인자로 전달하는 경우에는 소괄호를 생략해줄 수도 있다.
후행 클로저를 사용한 sorted(by:)메서드
let names: [String] = ["yejin", "zejin", "hejin"]
// 후행 클로저의 사용
let reversed: [String] = names.sorted(){
(first: String, second: String) -> Bool in
return first > second
}
// sorted(by:) 메서드의 소괄호까지 생략 가능
let reversed: [String] = names.sorted {
(first: String, second: String) -> Bool in
return first > second
}
2️⃣ 클로저 표현 간소화
1. 문맥을 이용한 타입 유추 : 타입 생략 가능
2. 단축 인자 이름
- 위의 코드에서 first와 second가 매우 의미 없음!! 우리는 이를 생략해주고 $0, $1 과 같이 표현할 수 있고, 매개변수 및 반환 타입과 실행코드를 구분하기 위해 사용했던 in 키워드를 생략해줄 수 있다.
3. 암시적 반환 표현 : return 키워드 또한 생략 가능
4. 연산자 함수 (>)
클로저로서의 연산자 함수 사용
let reversed: [String] = names.sorted(by: >)
➰ 덧셈 클로저 간소화하기 전
func calculate(a: Int, b: Int, method: (Int, Int) -> Int) -> Int {
return method(a, b)
}
result = calculate(a: 10, b: 10, method: {(left: Int, right: Int) -> Int in
return left + right
}
➰ 덧셈 클로저 간소화 후
result = calculate(a: 10, b: 10) { $0 + $1 }
3️⃣ 값 획득
incrementer라는 함수를 중첩함수로 표현하는 makeIncrementer
func makeIncrementer(forIncrement amount: Int) -> (() -> Int) {
var runningTotal = 0
func incrementer() -> Int {
runningTotal += amount
return runningTotal
}
return incrementer
}
() -> Int ?
makeIncrementer의 반환 타입으로, 함수 객체를 반환한다는 의미가 된다!
상수에 함수 할당
let incrementByTwo: (()-> Int) = makeIncrementer(forIncrement:2)
let first: Int = incrementByTwo() // 2
let second: Int = incrementByTwo() //4
4️⃣ 클로저는 참조 타입!
값 획득 부분의 예제는 상수 타입이다.
함수와 클로저는 상수타입!
함수나 클로저를 상수나 변수에 할당할 때마다 사실은 상수나 변수에 함수나 클로저의 참조를 설정하는 것이다.
let incrementByTwo: (()-> Int) = makeIncrementer(forIncrement:2)
let sameWithIncrementByTwo: (()-> Int) = incrementByTwo
let first: Int = incrementByTwo() // 2
let second: Int = sameWithIncrementByTwo() //4
-> 두 상수는 같은 클로저를 참조하기 때문에 동일한 클로저가 동작하는 것이 확인된다.
5️⃣ 탈출 클로저
@escaping 키워드
typealias VoidVoidClosure = () -> Void
let firstClosure: VoidVoidClosure = {
print("Closure A")
}
let secondClosure: VoidVoidClosure = {
print("Closure B")
}
// first와 second 매개변수 클로저는 함수의 반환 값으로 사용될 수 있으므로 탈출 클로저이다.
func returnOneClosure(first: @escaping VoidVoidClosure, second: @escaping VoidVoidClosure, shouldReturnFirstClosure: Bool) -> VoidVoidClosure {
// 전달인자로 전달받은 클로저를 함수 외부로 다시 반환하기 때문에 함수를 탈출하는 클로저이다.
return shouldReturnFirstClosure ? first : second
}
// 함수에서 반환한 클로저가 함수 외부의 상수에 저장되었다.
let returnedClosure: VoidVoidClosure = returnOneClosure(first: firstClosure, second: secondClosure, shouldReturnFirstClosure: true)
returnedClosure() // Closure A
var closures: [VoidVoidClosure] = []
// closure 매개변수 클로저는 함수 외부의 변수에 저장될 수 있으므로 탈출 클로저
func appendClosure(closure: @escaping VoidVoidClosure) {
// 전달인자로 전달받은 클로저가 함수 외부의 변수 내부에 저장되므로 함수를 탈출
closures.append(closure)
}
클래스 인스턴스 메서드에 사용되는 탈출, 비탈출 클로저
self 키워드
- 탈출하는 클로저에서 값 획득을 하기 위해서는 self 키워드를 이용하여 프로퍼티에 접근해야한다!
typealias VoidVoidClosure = () -> Void
func functionWithNoneescapeClosure(closure: VoidVoidClosure){
closure()
}
func functionWithEscapeClosure(completionHandler: @escaping VoidVoidClosure) -> VoidVoidClosure {
return completionHandler
}
class SomeClass{
var x = 10
func runNoescapeClosure(){
// 비탈출 클로저에서 self 키워드 사용은 선택 사항!
functionWithNoneescapeClosure {
x = 200
}
}
func runEscapeClosure() -> VoidVoidClosure {
// 탈출 클로저에서는 명시적으로 self를 사용해야한다.
return functionWithEscapeClosure {
self.x = 100
}
}
}
let instance: SomeClass = SomeClass()
instance.runNoescapeClosure()
print(instance.x) // 200
let returnedClosure: VoidVoidClosure = instance.runEscapeClosure()
returnedClosure()
print(instance.x) // 100
withoutActuallyEscaping
비탈출 클로저가 탈출 클로저인 척 해야할 경우 사용함
오류가 발생하는 hasElements 함수
배열의 lazy 컬랙션에 있는 filter 메서드의 매개변수로 비탈출 클로저를 전달한다. 그런데, lazy 컬랙션은 비동기 작업을 할 때 사용하기 때문에 filter메서드가 요구하는 클로저는 탈출 클로저이다. 따라서, 탈출 클로저 자리에 비탈출 클로저를 전달할 수 없다는 오류와 마주함!!
func hasElements(in array: [Int], match predicate: (Int)-> Bool) -> Bool {
return (array.lazy.filter{predicate($0)}.isEmpty == false)
}
// 오류!
withoutActuallyEscaping(_:do:) 함수의 활용
import Foundation
let numbers: [Int] = [2,4,6,8]
let evenNumberPredicate = { (number: Int) -> Bool in
return number % 2 == 0
}
let oddNumberPredicate = { (number: Int) -> Bool in
return number % 2 == 1
}
func hasElements(in array: [Int], match predicate: (Int) -> Bool) -> Bool {
return withoutActuallyEscaping(predicate, do: {escapablePredicate in
return (array.lazy.filter {escapablePredicate($0)}.isEmpty == false)
})
}
let hasEvenNumber = hasElements(in: numbers, match: evenNumberPredicate)
let hasOddNumber = hasElements(in: numbers, match: oddNumberPredicate)
print(hasEvenNumber) // true
print(hasOddNumber) // false
withoutActuallyEscaping(_:do:)
- 첫번째 전달인자로 탈출 클로저인 척 해야하는 클로저가 전달
- do 전달인자는 이 비탈출 클로저를 또 매개변수로 전달받아 실제로 작업을 실행할 탈출 클로저를 전달한다.
- 이 함수를 활용하여 비탈출 클로저를 탈출 클로저처럼 사용할 수 있다.
6️⃣ 자동 클로저
- 전달인자를 갖지 않음
- 클로저가 호출되기 전까지 클로저 내부의 코드가 동작하지 않는다 => 연산을 지연시킬 수 있다!
클로저를 이용한 연산 지연
// 대기 중인 손님들
var customerInLine: [String] = ["yejin", "hejin", "qejin"]
print(customerInLine.count) // 3
// 클로저를 만들어두면 클로저 내부의 코드를 미리 실행(연산)하지 않고 가지고만 있는다.
let customerProvider: () -> String = {
return customerInLine.removeFirst()
}
print(customerInLine.count) // 3
// 실제로 실행한다.
print("Now serving \(customerProvider())!") // "Now serving yejin!"
print(customerInLine.count) // 2
removeFirst() : 자신의 첫요소를 제거하면서 그 요소를 반환해주는 메서드
Reference
Swift 스위프트 프로그래밍 - 야곰
'Swift' 카테고리의 다른 글
[Swift] 맵, 필터, 리듀스 (0) | 2022.03.22 |
---|---|
[Swift] 옵셔널 체이닝과 빠른 종료 (guard, assert) (0) | 2022.03.20 |
[Swift] 접근제어 : 읽기 전용 구현 (0) | 2022.03.19 |
[Swift] 함수를 사용한 프로퍼티 기본값 설정 (0) | 2022.03.19 |
[Swift] self 프로퍼티 (feat. mutating, 타입메서드) (0) | 2022.03.19 |