ios개발/함수형 프로그래밍

<함수형 프로그래밍> 3.5편 클로저(Closure)

studying develop 2020. 4. 18. 03:38

스위프트에서 클로져가 클로저를 감싸는 주변 콘텍스트?(변수,상수)를 캡쳐한다는데 캡쳐한다는게 항상 무슨 의미인지 정확히 이해가 안됬다. 함수가 스택에서 제거되도 지우지 않고 남겨논다는 건가? 원래 제거되면 제거되는거지 도대체 왜 남는거지??

 


클로저 표현식 문법(Closure Expression Syntax)

클로저 표현식 문법의 일반 형식은 다음과 같음.

 

{ (parameters) -> return type in
    statements
}

클로저의 내용은 in 키워드로 시작하며 이 키워드는 클로저 인자와 반환 타입의 정의가 끝났고 클로저 내용이 시작함을 가르킴.

 


Capturing Values

A closure can capture constants and variables from the surrounding context in which it is defined. The closure can then refer to and modify the values of those constants and variables from within its body, even if the original scope that defined the constants and variables no longer exists.

클로저는 감싸는 주변 콘텍스트의 상수와 변수들을 캡쳐한다. 클로저는 자기 안에서 그런 상수,변수의 값들을 참조하고 수정할수 있다. 비록 그런 상수와 변수를 정의한 원래 스코프가 사라져도 할수 있따.

 

In Swift, the simplest form of a closure that can capture values is a nested function, written within the body of another function. A nested function can capture any of its outer function’s arguments and can also capture any constants and variables defined within the outer function.

흔한 형태는 네스티드 함수이다. 

 

Here’s an example of a function called makeIncrementer, which contains a nested function called incrementer. The nested incrementer() function captures two values, runningTotal and amount, from its surrounding context. After capturing these values, incrementer is returned by makeIncrementer as a closure that increments runningTotal by amount each time it is called.

 

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

 

func incrementer() -> Int {
    runningTotal += amount
    return runningTotal
}

The incrementer() function doesn’t have any parameters, and yet it refers to runningTotal and amount from within its function body. It does this by capturing a reference to runningTotal and amount from the surrounding function and using them within its own function body. Capturing by reference ensures that runningTotal and amount do not disappear when the call to makeIncrementer ends, and also ensures that runningTotal is available the next time the incrementer function is called.

 

사실 위에서 인크리멘탈 함수가 파라미터가 없는게 매우 특이하다. runningTotal 그리고 amount를 캡처링함으로서 참조하고 본인 함수 내부에서 사용하는게 가능하다. 레퍼런스로 캡처링은 runningTotal과 amount가 makeIncremental 호출이 종료되어도 사라지지 않는 것을 보장한다, 그리고 incrementer 함수가 다음에 호출될때 runningTotal이 사용가능함이 보장된다.

 

그럼 캡처는 도대체 언제 사라지는거지?? 그리고 왜 다시 호출될때 동일한거지 둘이....??

 

incrementByTen()
// returns a value of 10
incrementByTen()
// returns a value of 20
incrementByTen()
// returns a value of 30

let incrementBySeven = makeIncrementer(forIncrement: 7)
incrementBySeven()
// returns a value of 7

incrementByTen()
// returns a value of 40

 

음 이건 함수 리턴 타입에 관한 글이다.

Function Types as Return Types

You can use a function type as the return type of another function. You do this by writing a complete function type immediately after the return arrow (->) of the returning function.

The next example defines two simple functions called stepForward(_:) and stepBackward(_:). The stepForward(_:) function returns a value one more than its input value, and the stepBackward(_:) function returns a value one less than its input value. Both functions have a type of (Int) -> Int:

 

func stepForward(_ input: Int) -> Int {
    return input + 1
}
func stepBackward(_ input: Int) -> Int {
    return input - 1
}
func chooseStepFunction(backward: Bool) -> (Int) -> Int {
    return backward ? stepBackward : stepForward
}
var currentValue = 3
let moveNearerToZero = chooseStepFunction(backward: currentValue > 0)
// moveNearerToZero now refers to the stepBackward() function

여기서는 moveNearerToZero 함수로, 즉 특정 함수가 특정 함수를 refer 할수 있도록 구현할수 있는게 신기하다. 왠지 함수형 프로그래밍에서 자주 사용되지 않을까?

 

 

Closures Are Reference Types

In the example above, incrementBySeven and incrementByTen are constants, but the closures these constants refer to are still able to increment the runningTotal variables that they have captured. This is because functions and closures are reference types.

위 예시에서 클로저는 상수값이지만, 클로저는 레퍼런스 타입이다.

 

Whenever you assign a function or a closure to a constant or a variable, you are actually setting that constant or variable to be a reference to the function or closure. In the example above, it is the choice of closure that incrementByTen refers to that is constant, and not the contents of the closure itself.

This also means that if you assign a closure to two different constants or variables, both of those constants or variables refer to the same closure.

함수 또는 클로저를 대입시, 우리는 함수 또는 클로저 레퍼런스 값을 대입한는 것이다. 아래 예시가 좋은듯.

 

let alsoIncrementByTen = incrementByTen
alsoIncrementByTen()
// returns a value of 50

incrementByTen()
// returns a value of 60

음 이거 보고 생각한건데, 클로저가 캡처한 값들 갖고 따로 메모리에 할당되나 보다.

 

 

Escaping Closures

A closure is said to escape a function when the closure is passed as an argument to the function, but is called after the function returns. When you declare a function that takes a closure as one of its parameters, you can write @escaping before the parameter’s type to indicate that the closure is allowed to escape.

클로저는 함수의 인자로 전달되면 이스케이프를 한다함, 하지만 함수 리턴후에 호출된다. 너가 클로저를 하나의 파라미터로 갖는 함수를 선언하면, 우린 @escaping을 클로저가 escape할수 있다는 의미로 파라미터의 타입앞에 적을수 있다.

 

One way that a closure can escape is by being stored in a variable that is defined outside the function. As an example, many functions that start an asynchronous operation take a closure argument as a completion handler. The function returns after it starts the operation, but the closure isn’t called until the operation is completed—the closure needs to escape, to be called later. For example:

클로저가 탈출할수 있는 한 방법은 함수밖에서 정의된 변수에 저장하는 것이다. 한 예시는, 많은 비동기 작업으로 시작하는 함수는 클로저를 종료 핸들러의 인자로 받는다. 함수는 작동후 바로 리턴한다, 하지만 클로저를 작업 종료전에 호출하지 않는다 - 클로저는 탈출해서 나중에 불러져야 한다. 

 

var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
    completionHandlers.append(completionHandler)
}

The someFunctionWithEscapingClosure(_:) function takes a closure as its argument and adds it to an array that’s declared outside the function. If you didn’t mark the parameter of this function with @escaping, you would get a compile-time error.

마크 안하면 컴파일 타임 에러 발생함;

 

Marking a closure with @escaping means you have to refer to self explicitly within the closure. For example, in the code below, the closure passed to someFunctionWithEscapingClosure(_:) is an escaping closure, which means it needs to refer to self explicitly. In contrast, the closure passed to someFunctionWithNonescapingClosure(_:) is a nonescaping closure, which means it can refer to self implicitly.

@이스케이핑으로 마킹한 클로저는 클로저 내부에 self를 명시해야 한다. 

반면 논이스케이핑 클로저는 , 명시 안해도 된다.

 

음 왜 그러지? 이유는 잘 모르겠네

func someFunctionWithNonescapingClosure(closure: () -> Void) {
    closure()
}

class SomeClass {
    var x = 10
    func doSomething() {
        someFunctionWithEscapingClosure { self.x = 100 }
        someFunctionWithNonescapingClosure { x = 200 }
    }
}

let instance = SomeClass()
instance.doSomething()
print(instance.x)
// Prints "200"

completionHandlers.first?()
print(instance.x)
// Prints "100"

이스케이핑이 의미하는게 뭘까, 일단 비동기할때 종료 핸들러로 나도 사용해보긴 했다. 

 


Autoclosures

An autoclosure is a closure that is automatically created to wrap an expression that’s being passed as an argument to a function. It doesn’t take any arguments, and when it’s called, it returns the value of the expression that’s wrapped inside of it. This syntactic convenience lets you omit braces around a function’s parameter by writing a normal expression instead of an explicit closure.

오토 클로저는 함수의 인자로 전달되는 표현식을 wrap하기 위해 자동으로 생성되는 클로저다. 오토클로저는 아무 인자도 갖지 않는다, 그리고 호출되면, 표현식 내부에 wrap이된 표현식의 값을 반환한다. 이런 syntactic(통사론적) 편리함은 우리가 normal expression을 명시적인 클로저 대신에 사용함으로서 함수 파라미터 주변에서 괄호를 생략하게 해준다. 

 

It’s common to call functions that take autoclosures, but it’s not common to implement that kind of function. For example, the assert(condition:message:file:line:) function takes an autoclosure for its condition and message parameters;

오토클로저를 갖는 함수를 호출하는건 흔한 일이다, 하지만 그런 함수를 구현하는건 자연스러운게 아니다. 

 

its condition parameter is evaluated only in debug builds and its message parameter is evaluated only if condition is false.

An autoclosure lets you delay evaluation, because the code inside isn’t run until you call the closure. Delaying evaluation is useful for code that has side effects or is computationally expensive, because it lets you control when that code is evaluated. The code below shows how a closure delays evaluation.

조건 파라미터들은 디버그 빌드에서만 평가된다 그리고 메시지 파라미터는 콘디션이 거짓일때만 계산한다. 오토클로저는 계산을 딜레이 시켜준다, 왜냐면 안에 코드는 클로저를 호출하기 전까지 실행되지 않는다. 평가를 늦추는 것은 부작용 있는 코드나 연산 비용상에서 이점을 갖는다, 왜냐면 코드가 언제 계산될지 정할수 있기 때문이다.

 

var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
print(customersInLine.count)
// Prints "5"

let customerProvider = { customersInLine.remove(at: 0) }
print(customersInLine.count)
// Prints "5"

print("Now serving \(customerProvider())!")
// Prints "Now serving Chris!"
print(customersInLine.count)
// Prints "4"

 

 

Even though the first element of the customersInLine array is removed by the code inside the closure, the array element isn’t removed until the closure is actually called. If the closure is never called, the expression inside the closure is never evaluated, which means the array element is never removed. Note that the type of customerProvider is not String but () -> String—a function with no parameters that returns a string.

커스토머인라인 배열을 제거해도 클로저를 실제 호출하기 전까지 제거되지 않는다. 여기서 customerProvider 함수가 문자열이 아니라, ()->String 타입인 것을 알아야 한다 - 인자는 없는데 문자열을 반환하는 함수.

 

 

You get the same behavior of delayed evaluation when you pass a closure as an argument to a function.

// customersInLine is ["Alex", "Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: () -> String) {
    print("Now serving \(customerProvider())!")
}
serve(customer: { customersInLine.remove(at: 0) } )
// Prints "Now serving Alex!"

The serve(customer:) function in the listing above takes an explicit closure that returns a customer’s name. The version of serve(customer:) below performs the same operation but, instead of taking an explicit closure, it takes an autoclosure by marking its parameter’s type with the @autoclosure attribute. Now you can call the function as if it took a String argument instead of a closure. The argument is automatically converted to a closure, because the customerProvider parameter’s type is marked with the @autoclosure attribute.

 

// customersInLine is ["Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: @autoclosure () -> String) {
    print("Now serving \(customerProvider())!")
}
serve(customer: customersInLine.remove(at: 0))
// Prints "Now serving Ewa!"

If you want an autoclosure that is allowed to escape, use both the @autoclosure and @escaping attributes. The @escaping attribute is described above in Escaping Closures.

// customersInLine is ["Barry", "Daniella"]
var customerProviders: [() -> String] = []
func collectCustomerProviders(_ customerProvider: @autoclosure @escaping () -> String) {
    customerProviders.append(customerProvider)
}
collectCustomerProviders(customersInLine.remove(at: 0))
collectCustomerProviders(customersInLine.remove(at: 0))

print("Collected \(customerProviders.count) closures.")
// Prints "Collected 2 closures."
for customerProvider in customerProviders {
    print("Now serving \(customerProvider())!")
}
// Prints "Now serving Barry!"
// Prints "Now serving Daniella!"

In the code above, instead of calling the closure passed to it as its customerProvider argument, the collectCustomerProviders(_:) function appends the closure to the customerProviders array. The array is declared outside the scope of the function, which means the closures in the array can be executed after the function returns. As a result, the value of the customerProvider argument must be allowed to escape the function’s scope.

 

음 정확히 오토클로저랑 이스케잎클로저를 왜 사용하는지 모르겠다. 왜 꼭 필요한가를 생각해보자.....[https://devmjun.github.io/archive/07-Swift_Programming_Language-Closures] 보다 참고한 번역글;

 

 


Swift’s closure capturing mechanics

[https://www.swiftbysundell.com/articles/swifts-closure-capturing-mechanics/]

 

스위프트 클로저가 어떻게 캡쳐되는건지 궁금하다.