devthewild

Coding Dojo #2 후기

저번에 이은 Coding Dojo에 다녀왔다. 이번 범위는 함수에서 클로저까지.

1. 스코프 내의 변수 잡기

클로저는 사용자의 코드 안에서 전달되거나 사용할 수 있는 기능을 포함한 독립적인 블록(block)입니다. Swift에서의 클로저는 C 및 Objective-C 의 blocks와 유사하며, 다른 언어의 람다(lambda)와도 유사합니다. 클로저는 자신이 정의된 컨텍스트(context)로부터 임의의 상수 및 변수의 참조(reference)를 획득(capture)하고 저장할 수 있습니다.

09 클로저 (Closures) by inureyes

for-loop(for var...)의 스코프에 있는 변수를 사용하는 함수를 만드는데, 해당하는 변수의 값이 스코프 내에서 계속 변하는 경우에 loop가 끝난 뒤 당연히 마지막값을 기준으로 함수가 실행되는 상황에서 현재값을 저장할 수 있는 방법에 대한 문제였다.

해결책은

  1. 변수를 다시 캡처한다. var _i = i
  2. for-loop를 돌 때 for var...가 아니라 for-in으로 돌면 상수let가 되어 따로 캡처할 필요가 없다.
  3. 값을 받아서 그 값을 캡처하는 함수를 리턴하는 함수를 만든다.
1
2
3
4
5
6
7
func capture(i:Int, closure:Int->Int) -> ()->Int {  
func sth() -> Int {
return closure(i)
}
return sth
}
// capture(i){ $0*$0 }

3번의 경우를 더 간단하게 만들기 위해서 JavaScript의 즉시실행함수(IIFE)를 흉내내고 싶었는데, Swift의 버그 때문에 현재 단순히 값만 리턴하는 IIFE도 var는 안되고 let을 써야 타입 추론이 가능하며, let을 쓰더라도 함수를 리턴하는 경우에도 에러가 나고, IIFE 속에서 조건문(conditional statement)이 존재할 경우 경우의 수에 대한 superset을 추론하는게 아니라 그냥 에러가 난다.

2. 부분함수와 합성함수의 단계적 구현

2.1. 기본 구현

Swift에서도 함수형 프로그래밍 스타일을 지원하도록 Array에 filter, map, reduce 등의 메소드들이 존재해서 array.filter{}.map{}.reduce(){} 같은 스타일로 작성할 수 있다. 그래서 특정 조건까지의 배열을 뽑아서(take) filter와 map을 체이닝해서 변이(tramsform)된 배열을 구하는 문제부터 시작했다. 앞/뒤에서 특정 길이만큼 읽는 함수는 Swift의 기본 라이브러리에서 prefix와 suffix를 통해 지원하고 함수형 프로그래밍에서는 보통 take/takeRight라는 이름으로 지원된다. 특정 조건까지 읽는 함수는 takeWhile이라고 보통 부르는데, Swift에서 구현되어있지 않아서 도장에서는 ExSwift에 있는 takeWhile의 소스가 제공되었다.

2.2. Pythonic solution

Python, underscore.js처럼 filter/map/reduce라는 함수에 sequence를 인자로 넘기는 식으로 개발을 하다보면 체이닝이 아니라 reduce(map(filter(... 처럼 전역함수를 이용해 구현할 수도 있는데, 이럴 경우에는 실행되는 순서와 함수호출의 순서가 역순이고, 값을 평가하는 함수를 인자로 넘길 때 인자로 받는 전역함수와의 거리가 멀어져서 가독성이 떨어지는 문제가 생긴다. 이 문제를 부분 함수(partial function)와 합성 함수(compose function)을 이용해서 인자를 받는 순서를 바꾸면 훨씬 가독성이 올라간다.

2.3. Functional Programming

함수형 프로그래밍으로 구현하기 위해서는 일단 1급 시민(first-class citizen, 혹은 1급 함수나 1급 객체 등으로 불린다)이라는 개념에 대해 이해가 먼저 필요하다. Java의 경험이 있다면 함수형 언어로 가는 길 (중편) - 일급객체, JavaScript의 경험이 있다면 Javascript : 함수(function) 다시 보기라는 자세한 설명의 글들이 있고 영문으로는 더 자세하고 풍부한 자료들이 있다. 그리고 함수형 프로그래밍에 대한 패러다임의 이해도 필요한데, 가장 좋은 방법은 함수형 프로그래밍을 지원하는 언어를 배우는 것이다. Twitter에서 만든 Scala School이라는 Scala 입문 강의가 있는데 한글 번역도 존재한다. 혹은 Java 경험이 있다면 자바 개발자를 위한 함수형 프로그래밍라는 eBook을, JavaScript 경험이 있다면 함수형 자바스크립트라는 좋은 책들도 있다.

2.4. Solution

다시 도장 이야기로 돌아가서, 이 문제를 해결하기 위해 영후님은 F#이라는 언어의 pipe-forwarding(|>)이라는 연산자를 구현하셨다. Swift에서의 pipe-forwarding |>의 구현이라는 글을 읽어보면 구현체와 그 설명이 자세히 나와있다.

도장에서 나온 문제와 일치하지는 않지만 위에 나온 과정들을 종합해보면 이런 예제를 만들 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
infix operator |>   { precedence 50 associativity left }

public func |> <T,U>(lhs: T, rhs: T -> U) -> U {
return rhs(lhs)
}

func ifilter<T>(closure: T->Bool) -> [T]->[T] {
return { filter($0, closure) }
}

func imap<T,S>(closure: T->S) -> [T]->[S] {
return { map($0, closure) }
}


let list = [1,2,3,4,5]
|> ifilter { $0%2 == 0 }
|> imap { $0*2 }
|> imap { String($0) }

println(list) // ["4", "8"]

3. Accumulator

누산기(accumulator)는 함수 하나를 리턴하는데, 그 함수는 인자를 하나 받을 때마다 그 값들이 누산된 결과를 리턴한다. 클로저에서 값을 캡처해서 리턴하는 것은 1번 문제에서 했고, 캡처된 값을 변경하도록 하는 것과 파라미터로 정의된 값을 다시 변수로 사용해서 소스코드를 짧게 만들도록 구현하는게 목적이었다.

4. Jensen's Device

이번에도 알고리즘 문제가 하나 나왔다. Jensen's Device라는 문제로 한 변수를 캡처하고 있는 클로저를 계속 이용해서 따로 변수 선언없이 파라미터만으로 문제를 해결하도록하는 문제였다. 인자로 넘길 때 스코프 명시({})없이 쓸 수 있도록 해주는 @auto_closure라는 키워드에 대해 배웠다.

5. Conclusion

1회의 난이도가 1이었다면 이번 난이도는 10쯤 된다. 다음의 난이도는 얼마나 될지 모르겠다.

Coding Dojo #1 후기

OSXDev에서 열린 Coding Dojo에 다녀왔다. 보통 Dojo가 붙은 사이트들을 생각해서 이해하지 못하면 어쩌나 긴장했는데 다행히 난이도는 예상보다 낮았다. 타겟은 "책을 읽었다"와 "이해했다" 사이의 사람이 대상인 것 같다. 그러니까 책은 읽었는데 어떻게 써야할지 감이 안오는 사람들을 위한 '체득'의 시간 정도? 문제를 풀어보고 해석하고의 반복으로 진행되어 네 문제를 풀었다.

진행했던 문제들과 소스보다는 어떤 의도의 문제들이었고 어떤 것을 얻었나를 정리해본다.


1. if문에서의 Optional과 Boolean의 차이

트위터에 예전에 올라왔던 문제인데 이 문제와 같은 의도였다.

팁, 경우에 따라 다르겠지만 Boolean만 처리하지 말고 Optional에서 nil일 때도 처리해주는게 좋다.

2. switch에서의 pattern matching

복잡한 케이스에 대해서 switch-case를 단순히 objective-c에서 사용했던 것처럼 쓰거나 if-else문을 반복하는 것보다 tuple로 묶어서 한번에 처리하면 깔끔하고, where절(guard)을 통해 조건을 처리하면 더 깔끔하게 처리된다.

팁, pattern matching에서는 분기가 길어지면 case가 switch문과 거리가 멀어져서 어떤 의미인지 알기 어려운데, 특히 case에 true/false를 사용하는 경우 의미를 바로 알기 어려우므로 enum을 사용해서 의미있는 값을 변수로 만들어서 표현하면 좋다.

3. 간단한 알고리즘 풀이

중첩된 for문을 어떻게 사용하는지에 대한 문제. 참고로 100 doors 문제였다.

4. repeatString

String에 대한 기본 사용법(concat)과 loop 등을 사용한 정해진 횟수의 문자열 반복.


진도가 Control Flow까지만여서 그 안에서만으로 해결할 수 있는 문제들을 짧은 시간 내에 제출하느라 고생하셨으리라 생각된다.

덧 1, 4번 문제의 경우 제약 때문에 함수로 구현했지만 operator overloading을 통해 쓰기 좋게 만들 수도 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func repeatString(count n:Int,string s:String) -> String {  
return Array(count:n, repeatedValue:s).reduce("",+)
}

@infix func * (left: String, right: Int) -> String {
return repeatString(count: right, string: left)
}

@infix func * (left: Int, right: String) -> String {
return repeatString(count: left, string: right)
}

println("a" * 2) // "aa"
println(2 * "a") // "aa"

덧 2, swift 프로젝트에서 다른 파일에 구현을 해도 하나로 묶어서 해석하므로 같은 이름의 함수를 구현하면 에러가 난다는 질문이 있었는데, 얼마 전에 추가된 접근자(access control)을 통해 해결할 수 있을 줄 알고 시도해봤지만 안된다. c처럼 static 키워드가 생기거나 다른 방법을 찾아봐야겠다.

Sublime Text에서 swift파일 빌드하기

조건

  • Sublime Text와 Package Control이 설치되어 있을 것
  • Swift 개발 환경이 설정되어있을 것

순서

  • Package Control을 통해 Swift 패키지 설치
  • Tools > Build System > New Build System... 에서 다음과 같이 입력한다.
1
2
3
4
{
"cmd": ["swift", "-i", "$file"],
"selector": "source.swift"
}
  • 빌드 정보를 저장한다. (예, Swift.sublime-build)
  • cmd + B로 빌드한다.

부연 설명

Swift 패키지를 설치하면 문법 강조는 기본으로 지원하고 각종 Snippet을 지원해서 Xcode만큼은 아니더라도 편하게 작성할 수 있다.

Swift 패키지 프로젝트에서 패키지 파일들을 확인하면 보다 자세한 정보를 알 수 있다.