본문 바로가기

MOBILE/ios

[swift] PLS에서의 closure, swift에서의 closure

swift는 함수형 언어이다.

함수형 언어라는 것은 함수가 일급 객체 (first-class object)라는 말이다.

그렇기 때문에 swift에서의 함수는 일급함수이며 일급 객체가 가지는 특성을 모두 가진다.

그렇다면 어떤 객체가 일등급이 되어 서울대에 갈 수 있는가(?)

 

일급 객체의 조건

1. 런타임에도 객체 생성 가능해야 함

2. 파라미터로 객체를 전달할 수 있어야 함

3. 반환값으로 객체를 전달할 수 있어야 함

4. 데이터 구조 안에 객체를 저장할 수 있어야 함

5. 할당에 사용된 이름과 관계없이 고유한 구별이 가능해야 함

 

swift에서의 함수는 다행히도 위의 조건을 모두 만족한다.

즉, 다음과 같은 코드들이 가능하다는 것이다.

 

func function()
{
    print("first class object: function!")
}

// 함수의 파라미터로 함수를 사용 가능
let f = function
var f2: () -> Void = function
f()
f2()

// 함수의 return value로 함수를 사용 가능
func firstFunc() -> () -> ()
{
    return function
}
let returnVal = firstFunc()
returnVal()

// 함수를 변수나 상수에 저장 가능
func paramFunc(_ def: () -> ())
{
    def()
}
paramFunc(function)

이렇게 swift에서 함수는 올 일등급으로 서울대 수시 쓰는 인재와 마찬가지인 것이다.

 

또한 다른 언어와 마찬가지로 swift에서도 익명함수를 제공한다.

예를 들어, python에서의 람다함수, c++에서의 람다함수와 같은 익명함수를 swift에서는 closure 클로져라고 한다.

근데 swift에서의 클로져는 느낌이 다르다.

왜냐면 사실상 클로져는 모든 함수를 의미하기 때문이다(?)

 

swift에서의 클로져를 알기 위해서는 먼저 sw 아키텍쳐, 즉 프로그래밍 언어 구조론적으로 closure가 무엇인지 알아야 한다.

pls 수업시간에 배웠던 closure 개념을 다시 가져오자.

 

PLS에서의 closure

programming language structure 적으로 closure란 함수가 중첩되어 있을 때

"내부 함수 + referencing environment" 이다. 이때 referencing environment란 쉽게 말해서 함수가 참조하는 객체가 선언된 곳이다.

다음과 같은 상황에서도 python은 클로져를 생성한다.

 

이를 swift 버전으로 분석해보자.

다음과 같이 중첩된 함수가 있다고 하자.

 

func outerFunc(_ val: Int) -> (Int) -> (Int)
{
    let value = val + 10
    
    func innerFunc(_ val2: Int) -> Int
    {
        return value + val2
    }
    
    return innerFunc(_:)
}

let function = outerFunc(10)
let returnVal = function(10)

print(returnVal) // 예상 결과는?

이 경우 예상 결과는 얼마인가?

일단 outerFunc(10)을 function 이라는 변수에 저장했다. 이는 앞에서 살펴본 swift에서의 일급 함수의 특성을 사용한 것이다.

그 후 returnVal 이라는 변수에 function(10)의 결과값을 저장하고, 이를 print 한것이다.

 

이를 자세하게 살펴보자면,

먼저 outerFunc(10)을 function 이라는 변수에 저장했다. outerFunc() 은 중첩된 함수이며, 내부 함수를 return 하게 된다.

따라서 outerFunc()이 끝나고 나면 function이라는 변수에는 내부함수인 innerFunc()이 저장된다.

이때 outerFunc()은 참조가 끝났으므로 생명주기가 끝난다. 당연히 지역 변수인 value도 생명주기가 끝난다.

그 후 function()을 실행하면 내부 함수인 InnerFunc() 을 실행한 것과 같은 결과이다.

따라서 value에는 20이 저장되어 있고, function(10)으로 10을 파라미터로 주었으니 20+10으로 returnVal의 값은 30이 된다.

 

근데 여기서 중요한 점은 innerFunc() 에서 outerFunc()의 지역 변수인 value를 참조하고 있다는 것이다.

근데 문제는 outerFunc()은 참조가 끝나 생명 주기가 다했고, 이에 따라 지역 변수도 생명주기가 다 했는데

어째서 사라진 지역변수를 참조하는 innerFunc()가 오류가 안나는 것일까?

 

정답은 innerFunc()은 클로져이기 때문이다.

즉, innerFunc()의 referencing environment인 value도 포함하기 때문에 오류가 나지 않는 것이다.

정확하게 말하면 value 객체가 아닌 value의 값을 포함하는 것이다. 이를 capture라고 한다. 그때의 값을 "캡쳐"떠서 저장하는 것이다.

따라서 클로져가 만들어질려면 일단 referencing environment가 만들어져야하기 때문에 outerFunc()가 호출되어야 한다.

 

그런데 swift에서의 클로져는 의미가 하나 더 있다.

바로 익명함수이다.

 

 

swift에서의 closure

swift에서의 closure는 일회성으로 사용되는 익명함수(anonymous function)을 의미한다.

아니 왜 함수가 익명으로 사용되어야 하는 것인가? 이는 코드의 간결성을 위한 것이다.

 

예를 들어, 다음과 같이 일급 함수의 특징을 활용하여 인자로 받아온 함수를 실행시켜주는 함수가 있다고 하자.

이때, 더하기 기능을 넣기 위해서 sum() 이라는 함수를 파라미터로 전달하고자 하면 다음과 같은 코드를 작성해야 한다.

func sum(a: Int, b: Int)
{
    print("sum: \(a+b)")
}

func Function(_ a: Int, _ b: Int, def: (Int, Int) -> Void) -> Void
{
    def(a, b)
}
Function(5, 6, def: sum) // 예상 결과는?

 

하지만 sum() 이라는 함수가 딱 한번만 사용되고 더 이상 사용되지 않는다면, 굳이 꾸역꾸역 코드 안에 자리를 잡을 필요가 없다.

다음과 같이 일회성 함수로 만들어주면 되기 때문이다.

 

func Function(_ a: Int, _ b: Int, def: (Int, Int) -> Void) -> Void
{
    def(a, b)
}
Function(5, 6, def: {(a:Int, b:Int) -> Void in print("sum : \(a+b)")})

 

이렇게 함수의 역할을 하지만 이름이 없는 함수를 익명 함수라고 하며, 일회성으로 사용된다.

이때 swift에서는 이를 closure라고 부르는 것이다.

 

그렇다고 swift에서의 closure가 pls에서의 closure를 버린 것은 아니다.

즉, swift에서의 closure는 익명함수이자 pls closure 인 것이다. 즉 두개의 기능을 모두 포함한다.

 

그렇기 때문에 swift에서의 closure는 함수를 포함하는 개념인 것이다.

 

 

closure가 이름이 있는데, capture 할 값이 없는 경우: 전역 함수 (일반적인 함수)

closure가 이름이 있는데, 중첩 함수의 inner function이며 capture 가능한 경우 : pls에서의 closure

closure가 이름이 없고, 익명 함수로서 capture 가능한 경우 : closure expression (클로저 표현식)

 

 

closure expression

// 표현식
// { (매개 변수) -> 반환 타입 in 실행 구문 }

우리가 익명함수라고 부르는 클로져 표현식은 위와 같은 형태이다.

즉, 경량화된 문법을 제공하기 때문에 코드의 간결성에 도움을 준다.

 

closure 표현식은 그 자체로 함수이기 때문에 이를 변수나 상수에 직접 대입하여 실행하는 것도 가능하다.

let value = { () -> Void in print("this is closure expression")}
value()

 

closure 문법은 이와 같이 다양하게 생략가능하다.

이 중에서 가장 특이한 trailing closure를 살펴보자

 

trailing closure

trailing closure란 함수의 마지막 파라미터가 closure인 경우, 이를 함수 block 형태로 작성가능한 문법을 말한다.

즉 다음과 같다는 것이다.

 

func Function(_ a: Int, _ b: Int, def: (Int, Int) -> Void) -> Void
{
    def(a, b)
}
Function(5, 6)
{
    (a, b) -> Void in print("sum : \(a+b)")
}

위에서 살펴본 Function() 함수를 호출하는 것이다.

이때 함수의 마지막 인자는 closure이다. 따라서 trailing closure를 적용하면 인자로 처리하지 않고 함수 모양처럼 쓸 수 있다.

이때 closure의 파라미터의 인자 타입은 생략 가능하다. 즉

(a, b) -> Void in print("sum : \(a+b)")
(a:Int, b:Int) -> Void in print("sum : \(a+b)")

 

이 둘은 같다는 것이다.

왜냐하면 Function의 파라미터에서 정의한 함수 타입이 무조건 (int, int)를 받도록 되어있기 때문에 타입 추론에 의해 자동으로 변환해주기 때문이다.

이 처럼 trailing closure를 이용하면 closure를 사용함에도 코드의 가독성을 높일 수 있다. 하지만 조심해야 하는 점이 있다.

무조건 closure가 마지막 인자일때만 적용이 가능하다는 것이다. 

 

예를 들어, 다음과 같은 상황에서는 적용이 불가능하다.

func Function(def: (Int, Int) -> Void, _ a: Int, _ b: Int) -> Void
{
    def(a, b)
}

// 이건 불가능
Function(5, 6)
{
    (a, b) -> Void in print("sum : \(a+b)")
}

 

왜냐하면 closure가 첫번째 인자로 전달되었기 때문에 트레일링 클로저를 사용할 수 없는 것이다.

이러한 문법 차이를 잘 알고 있어야 적용이 쉽다.