[함수형 프로그래밍과 swift] 클로저(Closure)

silverCastle·2022년 7월 22일
0
post-thumbnail

클로저(Closure)는 일정 기능을 하는 코드를 하나의 블록으로 모아놓은 것을 말한다.
변수나 상수가 선언된 위치에서 참조(Reference)를 획득(Capture)하고 저장할 수 있다.

💡 클로저(Closure)

획득(Capture) 때문에 메모리에 부담이 가지 않을까 걱정할 수도 있지만, swift는 스스로 메모리를 관리한다.
클로저의 몇 가지 모양 중 하나가 함수이다. 클로저의 세 가지 형태는 다음과 같다.

  • 이름이 있으면서 어떤 값도 획득하지 않는 전역변수의 형태
  • 이름이 있으면서 다른 함수 내부의 값을 획득할 수 있는 중첩된 함수의 형태
  • 이름이 없고 주변 문맥에 따라 값을 획득할 수 있는 축약 문법으로 작성한 형태

swift에서 클로저 표현은 최적화되어서 간결하고 명확하다. 최적화의 의미는 다음과 같다.

  • 문맥(context)에서 인자 타입(parameter type)과 반환 타입(return type)의 추론
  • 단일 표현 클로저에서의 암시적 반환
  • 축약된 인자 이름
  • 후위 클로저 문법

✍️ 클로저 표현(Closure Expressions)

클로저 표현은 인라인 클로저를 명확하게 표현하는 방법으로 문법에 초점이 맞춰져 있다.

let names = ["bronzeCastle", "silverCastle", "goldCastle"]

예를 들어, 위와 같이 문자열을 원소로 가지는 배열이 있다고 가정하자. 이 배열을 정렬하기 위해서 swift의 표준 라이브러리에 있는 sorted(by:)라는 메소드를 사용하면 된다. by에 어떤 방법으로 정렬을 수행할 것인지에 대해 기술한 클로저를 넣으면 되는데 예시를 보면서 이해해보자.

func backward(_ s1: String, _ s2: String) -> Bool {
    return s1 > s2
}

var reversedNames = names.sorted(by: backward)

결과

["silverCastle", "goldCastle", "bronzeCastle"]

backward 클로저를 만들어 원본 배열을 내림차순으로 정렬한 것을 알 수 있다. 비교하는 클로저를 사용하는데 긴 코드를 사용하였는데 앞으로 클로저의 다양한 문법을 사용하면서 익숙해보자.

🔍 클로저 표현 방법(Closure Expression Syntax)

클로저 표현 문법은 일반적으로 아래의 형태를 띤다.

{  (parameters) -> return type in
    statements
}

parameters는 인자로 넣을 곳, statements는 인자 값으로 처리할 내용을 기술할 곳이라고 생각하면 된다.
앞에서 사용했던 backward 클로저를 다음과 같은 클로저 표현을 이용해 바꿀 수 있다.

var reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
    return s1 > s2
})

🔍 문맥에서 타입 추론(Inferring Type From Context)

sorted(by:)의 메소드에서 이미 (String, String) -> Bool 타입이 인자로 들어올 것을 알기 때문에 타입을 추론하여 생략할 수 있다.

var reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 })

🔍 단일 표현 클로저에서의 암시적 반환(Implicit Returns From Single-Express Closures)

단일 표현 클로저에서는 return 키워드를 생략할 수 있다.

var reversedNames = names.sorted(by: { s1, s2 in s1 > s2 })

🔍 인자 이름 축약(Shorthand Arguments Names)

swift는 인라인 클로저에 자동으로 축약 인자 이름을 제공한다. 인자 값 순서대로 $0, $1, $2...으로 사용할 수 있다.
축약 인자 이름을 사용하면 인자 값과 그 인자로 처리할 때 사용하는 인자가 같다는 것을 알기 때문에 인자를 입력받는 부분과 in 키워드를 생략할 수 있다.

var reversedNames = names.sorted(by: { $0 > $1 })

🔍 후위 클로저(Trailing Closures)

만약 함수의 마지막 인자로 클로저를 넣고 그 클로저가 길다면 후위 클로저를 사용할 수 있다. 이런 형태의 함수와 클로저가 있다면,

func fun(closure: () -> Void) {
    // function body goes here
}

위 클로저의 인자 값 입력 부분과 반환 형 부분을 생략해 다음과 같이 표현할 수 있고

fun(closure: {
    // closure's body goes here
})

이것을 후위 클로저로 표현하면 아래와 같이 표현할 수 있다.
이런 형태를 우린 자주 사용했는데 사실 이런 일반적인 전역함수 형태가 클로저를 사용하고 있던 것이었다.

fun() {
    // trailing closure's body goes here
}

✍️ 값 캡쳐(Capturing Values)

클로저는 특정 문맥의 상수나 변수의 값을 캡쳐할 수 있다. 캡쳐라는 의미가 생소할 수 있는데 말 그대로 원본 값이 사라져도 그 값을 포획을 하여 클로저의 body 안에서 활용할 수 있다. swift에서 값을 캡쳐하는 가장 단순한 형태는 중첩 함수(nested function)이다.

func makeIncrementer(forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    func incrementer() -> Int {
        runningTotal += amount
        return runningTotal
    }
    return incrementer
}

클로저의 인자 값은 (forIncrement amount: Int)이고, 반환 값은 () -> Int이다.
즉, 반환 값을 인자가 없고 Int형의 클로저를 반환한다는 의미이다. 이 중첩 함수를 실행해보겠다.

let incrementByTen = makeIncrementer(forIncrement: 10)

incrementByTen()
incrementByTen()
incrementByTen()

결과

10
20
30

함수가 각각 실행되지만 실제로는 변수 runningTotal과 amount가 캡쳐링되어서 그 변수를 공유하기 때문에 계산이 누적됨을 알 수 있다.
여기서 무언가 이상함을 눈치챘을 수도 있다.
incrementByTen은 var가 아닌 let으로 선언되었기 때문에 상수인데 어떻게 runningTotal 변수를 계속해서 증가시킬 수 있었을까?
바로, 함수와 클로저는 참조 타입이기 때문이다. 함수와 클로저를 상수나 변수에 할당할 때 실제로는 상수와 변수에 해당 함수나 클로저의 참조(reference)가 할당됨을 잊지 말자.

0개의 댓글