├── .gitignore ├── 1.1.md ├── 1.2.md ├── 1.3.md ├── 2.1.md ├── 2.2.md ├── Chapter1.md ├── Chapter2.md ├── README.md └── images ├── 1.1.7.png ├── 1.1.8.png ├── 1.1.png ├── 1.2.png ├── 1.3.png ├── 1.4.png ├── 1.5.png ├── 2.1.png ├── 2.2.png ├── 2.3.png ├── 2.4.png ├── equation1.4.png ├── equation2.1.4.png ├── example1.8.png └── example2.9.png /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | -------------------------------------------------------------------------------- /1.1.md: -------------------------------------------------------------------------------- 1 | ### 이전 목차 2 | 3 | [1장 프로시저로 요약하는 방식](https://github.com/godrm/SICP-Swift/blob/master/Chapter1.md) 4 | 5 | ## 1.1 프로그래밍할 때 필요한 것들 The Elements of Programming 6 | 7 | 프로그램을 표현하기 좋은 언어는 단지 컴퓨터가 할 일을 표현하는 것이 아니다. 프로그래밍 언어가 표현할 수 있는 방법이 다양하면, 컴퓨터가 처리할 프로세스에 대한 개발자의 생각을 적절하게 표현할 수 있다. 간단한 생각들을 모아서 복잡한 생각을 표현하는 방식으로 프로그래밍 언어 활용하는 것에 집중해보자. 8 | 9 | 훌륭한 언어들은 다음과 같은 매커니즘을 제공한다. 10 | 11 | - 기초 표현 primitive expressions : 언어에서 가장 단순한 것들을 표현 12 | 13 | - 조합 수단 means of combination : 간단한 것들을 합쳐서 복잡한 것을 만듦 14 | 15 | - 추상화 수단 means of abstraction : 복잡한 것에 이름을 붙여 요약 16 | 17 | 우리는 프로그래밍 과정에서 `프로시저`와 `데이터`를 다룬다. 현대 언어에서는 엄밀하게 두 개를 구분할 필요가 없어지고 있는 추세다. 특히 스위프트나 코틀린, 러스트처럼 최근에 만들어진 훌륭한 언어들이 많아졌다. 18 | 19 | 데이터와 프로시저 모두 기초 표현으로 활용할 수 있고, 데이터나 프로시저 여러 개를 합쳐서 조합할 수도 있다. 1장에서는 주로 프로시저 관섬에서 데이터를 다루는 방식을 설명한다. 20 | 21 | ### 1.1.1 표현식 Expressions 22 | 23 | 프로그래밍을 배우기 시작하기 좋은 방법으로 그 언어 개발 환경을 직접 실행해보는 것이다. 터미널에서 바로 스위프트 코드를 넣으면 실행할 수 있는 환경이나 웹 브라우저에서도 바로 코드를 실행할 수 있는 환경이 많아있다. 직접 스위프트 표현식을 넣으면, 표현식을 계산 evaluating 해서 결과를 표시해 준다. 24 | 25 | 10진수 같은 숫자 값을 표현하는 식도 기초 표현 중에 하나다. 스위프트 REPL에 숫자 값을 넣으면 값을 그대로 출력한다. Xcode Playground 나 iPad Playground 에서도 출력값을 확인할 수 있다. 26 | 27 | ```swift 28 | 486 29 | $R0: Int = 486 30 | ``` 31 | 32 | 숫자 값을 나타내는 표현식에 `+`, `-` 같은 사칙 연산을 위한 기호를 덧붙여서 조금더 복잡한 조합을 표현할 수 있다. `숫자에 연산 기호를 적용 application한다`는 의미로 읽는다. 33 | 34 | ```swift 35 | 137 + 349 36 | $R1: Int = 486 37 | 38 | 1000 - 334 39 | $R2: Int = 666 40 | 41 | 5 * 99 42 | $R3: Int = 495 43 | 44 | 10 / 5 45 | $R4: Int = 2 46 | 47 | 2.7 + 10 48 | $R5: Double = 12.699999999999999 49 | ``` 50 | 51 | 스위프트에서는 이렇게 연산할 데이터 - 피연산자 operand와 계산을 위한 프로시저 - 연산자 operator를 조합해서 조합식 combination을 만들 수 있다. 조합식에 대한 계산 결과는 프로시저에 해당하는 연산자에 계산할 인자값 argument를 적용해서 계산한다. 52 | 53 | 원문에서는 Lisp 기반으로 설명하기 때문에 앞쪽 표기 prefix notation 방식으로 되어 있지만, 스위프트에서 연산은 수학 표기 방식과 비슷한 중간 표기 infix notation 방식이다. 54 | 55 | Lisp처럼 앞쪽 표기를 사용하면 인자가 많아져도 그대로 적용할 수 있다는 장점이 있다. 스위프트에서는 중간 표기 방식을 사용하기 때문에 할 수 없다. 56 | 57 | ```lisp 58 | (+ 21 35 12 7) 59 | > 75 60 | 61 | (+25 4 12) 62 | > 200 63 | ``` 64 | 65 | 스위프트에서는 표현식이 다음과 같이 달라진다. `+` 연산자 표기를 더 많이 반복해서 표현해야만 한다. 66 | 67 | ```swift 68 | 21 + 35 + 12 + 7 69 | $R6: Int = 75 70 | ``` 71 | 72 | 이렇게 연산자가 많아지는 경우에는 여러 겹으로 엮어서 처리해야 하는 경우가 생길 수 있다. 73 | 74 | ```swift 75 | (3 * 5) + (-10 + 6) 76 | $R7: Int = 11 77 | (3 * ((2 * 4) + (3 + 5))) + ((10 - 7) + 6) 78 | $R8: Int = 57 79 | ``` 80 | 81 | 표현식이 깊어져서 겹쳐써도 실행기는 전혀 실수없이 동작한다. 괄호를 정확하게 계산하는 단위를 묶어서 표시하면 충분하다. 반면에 사람은 조금만 식이 복잡해지면 실수로 계산을 틀릴 가능성이 있다. 실제로 이 예시를 작성하면서도 2번이나 잘못 입력했다. 82 | 83 | ```swift 84 | (3 * 85 | ( (2 * 4) + 86 | (3 + 5) 87 | ) 88 | ) + 89 | ( (10 - 7) + 90 | 6 91 | ) 92 | ``` 93 | 94 | 사람이 표현식을 읽을 때 도움을 주기 위해서 인자를 중심으로 줄을 맞추고 알맞게 들여쓰는 방식을 예쁜 출력 pretty-printing 이라고 한다. 실행기는 표현식이 어떻게 쓰여있더라도 상관없이 표현식을 읽고, 계산하고, 출력하는 일을 되풀이 read-eval-print loop 한다. 터미널에서 코드를 입력하고 바로바로 실행결과를 알려주는 환경을 `REPL`라고 부른다. 95 | 96 | ### 1.1.2 이름과 환경 97 | 98 | 프로그래밍 언어로 표현하는 것 중에 가장 중요한 일이 계산 객체 computational object에 다른 이름을 붙이는 것이다. 이 때 이름은 `상수 constant`라고 하고, 계산 객체는 변수의 `값 value`이라고 한다. 참고로 `변수 variable`에 대해서는 뒤에 다시 설명한다. 99 | 100 | ```swift 101 | let size = 2 102 | size: Int = 2 103 | ``` 104 | 105 | 이렇게 size라는 이름으로 2라는 값을 (다른 이름으로) 나타낼 수 있다. 106 | 아래처럼 여러 가지 값에 다른 이름을 붙일 수도 있다. 107 | 108 | ```swift 109 | let pi = 3.141592 110 | pi: Double = 3.1415920000000002 111 | 112 | let radius = 10.0 113 | radius: Double = 10 114 | 115 | pi * (radius * radius) 116 | $R0: Double = 314.1592 117 | 118 | let circumference = 2 * pi * radius 119 | circumference: Double = 62.83184 120 | ``` 121 | 122 | 이렇게 `let`을 사용하면 `circumference`라는 이름으로 둘레를 계산하는 복합 연산compound operation을 다르게 표현할 수 있다. 컴퓨터 하드웨어나 소프트웨어 프로그램은 값이나 표현식을 다 풀어서 복잡하게 쓰기 보다는, 조금 덜 복잡한 내용을 합쳐서 더 복잡한 것으로 만든다. 123 | 124 | 이렇게 어떤 값에 기호symbol로 이름을 붙였다가 다시 그 이름으로 이전에 지정한 값을 찾아서 사용할 수 있다는 것은 REPL 실행기에 이름과 값을 짝지어서 저장하는 기억장치memory가 있다는 의미다. 이런 기억장치를 `실행 환경environment`라고 하고, 여기서 실행 환경은 가장 바깥쪽에 있는 전체 환경global environment을 의미한다. 책 뒷부분에서 계산 과정에서 사용하는 환경이 여러 개 더 있다는 것을 알게 된다. 125 | 126 | ### 1.1.3 조합combinations 표현 실행하기 127 | 128 | 절차대로 일하는 순서를 정하는 데 고려해야 할 것들을 정리하고 있다. 129 | 130 | 조합 표현을 실행하는 순서는 다음과 같다. 131 | 1. 조합 표현에서 부분 표현을 먼저 실행한다 132 | 2. (스위프트 기준) 조합 표현에서 가운데 있는 표현이 연산을 위한 프로시저이고, 나머지 표현이 인자에 해당하는 값이 된다. 인자 값에 대해 프로시저를 적용해서 조합 표현의 값을 계산한다. 133 | 134 | 앞에서 설명했던 것처럼 부분 표현에 대한 값을 계산할 때도 같은 절차를 따른다. 반복해서 조합 표현을 계산하다보면 자연스럽게 귀납법recusive이 된다. 귀납법은 특정한 단계에서 동일한 반복해서 규칙을 다시 따르게 된다. 135 | 136 | 이렇게 깊이 겹쳐서 중첩된 표현식을 반복해서 계산하는 절차가 복잡한 절차를 단순하게 만드는 데 도움을 준다. 137 | 138 | ```swift 139 | (2 + (4 * 6)) * ((3 + 5) + 7) 140 | ``` 141 | 142 | 실제로 스위프트 실행기에 위의 코드를 입력하면 컴파일러 에러가 나오거나 실행이 10초 넘게 걸린다. 이유는 컴파일러가 타입 추론하는 데 실패하기 때문이다. 다음과 같이 Int() 타입이라는 것을 명시적으로 표시해야 제대로 동작한다. 143 | 144 | ```swift 145 | Int(2 + (4 * 6)) * Int((3 + 5) + 7) 146 | ``` 147 | 148 | 이 식을 그림1.1처럼 나무 모양 Tree로 표현하면, 부분 표현식을 계산하는 프로세스가 잘 드러난다. 149 | 150 | ![1.1 조합 표현에 대한 값을 모두 펼쳐 보여주는 그림](https://github.com/godrm/SICP-Swift/blob/master/images/1.1.png?raw=true) 151 | 152 | 나무 모양에서 계산해야 하는 값은 가지branch로, 연산자는 마디node로, 가장 끝에 나오는 마지막 마디 terminal node에는 숫자와 연산자를 표시한다. 앞에서 설명한 조합 표현 계산 프로세스로 설명해보면 나무 마지막 마디부터 차례차례 윗 단계로 올라가며 계산한다. 그러면서 같은 절차를 여러 번 반복하는 귀납적인 재귀recursion로 계산한다. 153 | 154 | 나무 그림 마지막 마디에 위치한 숫자나 내장 연산자built-in operator 표현 식을 계산할 때 규칙을 살펴보자. 155 | 156 | - 숫자 표현식 값은 여러 숫자가 모인 값 157 | - 내장 연산자 값은 해당 연산자가 의미하는 계산 동작을 미리 조합해 놓은 명령들 158 | - 나머지 다른 이름 값은 `환경`에 지정해 놓은 객체 159 | 160 | 여기서 핵심은 환경에 따라서 표현 식에서 사용하는 이름에 대한 의미가 달라질 수 있다는 것이다. 스위프트 실행기에서 `x`라는 이름이나 `+`라는 연산 이름을 정의하지 않았다면 아무런 의미가 없다. 3장에서 프로그램 실행을 위한 문맥 context를 결정하는 환경에 다시 설명한다. 161 | 162 | 지금까지 설명한 계산 규칙으로는 표현식에 이름을 붙이지는 못한다. `let pi = 3.141592` 표현에서 `let`이라는 프로시저가 있어서 pi 와 3.141592 인자 값으로 계산한다는 것이 아니다. let은 pi라는 이름으로 값을 대신할 뿐이다. 163 | 164 | 계산을 위한 규칙만으로 값을 계산하지 못하기 때문에 계산 규칙을 설명해 놓는 문법이 필요한다. 이런 문법을 특별한 형태 special form이라고 부른다. let만 설명을 했지만 앞으로 여러 종류의 표현식을 보게 된다. 이런 표현식들을 모아서 프로그래밍 언어의 문법syntax을 만든다. 165 | 166 | ### 1.1.4 복합 프로시저 compound procedure 167 | 168 | 지금까지 훌륭한 프로그래밍 언어가 갖고 있는 요소들을 정리했다. 복잡한 절차를 포함하는 복합적인 연산을 요약해서 다른 이름을 붙이는 `프로시저 만들기`에 대해 알아볼 차례다. 169 | 170 | `제곱`을 어떻게 표현하는지로 설명한다. 제곱하는 계산 방식을 말로 설명하면 '어떤 값을 제곱하는 것은 같은 값을 두 번 곱한다.'는 말이다. 이 말을 코드로 표현해보자. 171 | 172 | ```swift 173 | func square(_ x : Int) -> Int { x * x } 174 | ``` 175 | 176 | 위의 코드를 풀어서 읽어보면 다음과 같이 해석할 수 있다. 한국어는 영어와 어순이 반대라서 읽기 어렵다. `어떤 값의 제곱을 계산하려면, 그 값을 두 번 곱해서 구한다` 정도로 읽을 수 있다. 177 | 178 | ![1.2 프로시저 문법](https://github.com/godrm/SICP-Swift/blob/master/images/1.2.png?raw=true) 179 | 180 | 181 | 이렇게 프로시저를 선언하면 이미 있던 다른 프로시저를 요약하고 합쳐서 `square`라는 이름으로 복합 프로시저가 생긴다. 이 프로시저는 정의한 데로 같은 값을 두 번 곱해서 제곱을 계산한다. 위에 정의한 코드를 읽어보면, 곱할 값을 x라는 지역 이름local name으로 부른다. 이런 표현은 평소에 사용하는 자연 언어에서 대명사와 비슷한 표현이다. 182 | 183 | 프로시저를 선언하는 과정은 두 단계로 인지해야만 한다. 이름이 없는 상태로 새로운 복합 프로시저를 만드는 것과 이렇게 만든 프로시저에 `square`라는 이름을 붙이는 두 단계를 구분해야 한다. 나중에는 이름이 없는 프로시저만 만들어서 사용하기도 한다. 184 | 185 | **<노트>** 186 | 187 | ``` 188 | 스위프트에서는 이런 계산 절차를 선언한 프로시저를 `함수`라고 부른다. 오래된 프로그래밍 언어 중에서는 명확하게 프로시저와 함수를 구분했다. 특히 2000년대 이후에 사용하는 현대 프로그래밍 언어에서는 프로시저를 그냥 함수라고 부르는 경향이 두드러진다. 파스칼Pascal 같은 언어에서는 리턴값이 없는 절차는 프로시저라고 부르고, 리턴값이 있는 절차는 함수라고 부르기도 한다. 189 | 190 | 이 글에서는 원문에서 설명하는 계산 절차를 의미하는 프로시저를 더 강조하기 위해서 프로시저로 표현한다. 191 | ``` 192 | 193 | 프로시저를 선언하는 문법은 다음 형식이다. 194 | 195 | ```swift 196 | func () { 197 | 198 | } 199 | ``` 200 | 201 | 은 환경에서 프로시저를 구분하는 이름Symbol이다. 202 | 는 프로시저가 넘겨받는 인자를 구분하는 지역 이름들이다. 203 | 는 프로시저를 부를 때마다 계산하는 규칙을 표현한다. 프로시저에 인자를 넘기면 지역에만 있는 인자 이름을 해당 값으로 바꾼 다음에 계산해서 값을 구한다. 204 | 는 () 소괄호로 구분하고, 는 {} 중괄호로 구분한다. 205 | 프로시저를 부를 때는 는 생략하고 () 괄호에 인자 값만 전달한다. 206 | 207 | ```swift 208 | square(21) 209 | $R0: Int = 441 210 | 211 | square(2 + 5) 212 | $R1: Int = 49 213 | 214 | square(square(3)) 215 | $R2: Int = 81 216 | ``` 217 | 218 | square를 만들면서 *라는 기본 연산 프로시저를 사용한 것처럼 square도 다른 프로시저를 정의할 때 사용할 수도 있다. 219 | 220 | ```swift 221 | func sumOfSquares(x: Int, y: Int) -> Int { 222 | square(x) + square(y) 223 | } 224 | 225 | sumOfSquares(x: 3, y:4) 226 | $R3: Int = 25 227 | ``` 228 | 229 | 더 확장해보면 sumOfSquares도 다른 프로시저를 정의할 때 사용할 수 있다. 230 | 231 | ```swift 232 | func foo(a: Int) -> Int { 233 | sumOfSquares(x: a+1, y: a*2) 234 | } 235 | 236 | foo(a: 5) 237 | $R4: Int = 136 238 | ``` 239 | 240 | ### 1.1.5 교체 방식substitution model으로 프로시저 실행하기 241 | 242 | 직접 선언한 프로시저를 실행하는 과정은 기본 연산 프로시저를 실행하는 것과 동일하다. 모든 부분 표현 값을 구한 다음에, 연산자에 인자로 넣고 계산하면 된다. 243 | 244 | 복합 프로시저를 적용하는 규칙은 다음과 같다. 245 | 246 | > 복합 프로시저를 인자에 맞추는 것은, 프로시저 내부에 있는 모든 매개변수를 인자 값으로 바꾼 다음에 실행하는 것이다. 247 | 248 | `foo(a: 5)` 표현식을 실행하는 과정을 살펴보자. 249 | 250 | 앞에서 foo() 프로시저 내부는 `sumOfSquares(x: a+1, y: a*2)`로 선언했다. 251 | 여기에 인자값 5는 `sumOfSquares(x: 5+1, y: 5*2)`로 `a` 자리에 값으로 교체된다. 252 | 이렇게 표현식이 sumOfSquares() 라는 연산자와 피연산자 두 개를 합쳐놓은 조합combination이 된다. 마지막 노드에 해당하는 피연산자 표현식부터 값을 계산해서 인자 값으로 결정한다. `5 + 1`은 6이 되고, `5 * 2`는 10이 된다. 이제 sumOfSquares 프로시저에서 인자x는 6, 인자y는 10을 적용한다. sumOfSquares() 함수는 귀납적으로 `square(6) + square(10)` 표현식으로 바뀐다. square() 함수를 다시 적용하면 `(6 * 6) + (10 * 10)`으로 바뀌고, 괄호부터 곱셈을 하고 나면 `36 + 100`으로 바뀐다. 마지막에는 136이 된다. 253 | 254 | 프로시저를 적용하면서 교체하는 방식에 대해 설명했다. 하지만 두 가지 꼭지를 짚고 넘어가야 한다. 255 | 256 | - 여기서 설명한 교체 방식은 프로시저를 적용 과정을 설명을 위한 것일 뿐이고, 실행기가 정말로 이렇게 동작하는 것은 아니다. 프로시저를 실행하면서 매개 변수를 인자 값으로 문자 그대로 바꾸지는 않는다. 대신 프로시저 지역 환경에 인자 이름을 넣고 계산하는 방식으로 동작한다. 3장과 4장에서 실행기를 만드는 방법을 살펴볼 때 더 깊이 설명한다. 257 | 258 | - 책에서는 실행기가 어떻게 동작하는 지 차례대로 보여주다가 5장에 가서 완벽한 실행기와 번역기를 보여준다. 교체 방식은 계산 모형model of computation 중에 하나로, 표현식을 계산하는 과정 evaluation process을 설명하기 위한 출발선일 뿐이다. 이후에는 더 복잡한 계산 방법을 사용하게 된다. 259 | 260 | ##### 인자 먼저 계산법 대 순서대로 계산법 261 | 262 | 바로 앞서 설명한 인자 값부터 먼저 구하는 계산 방식을 '인자 먼저 계산법 applicative-order evaluation' 이라고 한다. 표현식에 대한 값을 구하는 방법이 연산자를 적용하기 전에 모든 인자값을 적용하는 것만 있는 게 아니다. 값이 필요한 상황까지 피연산자를 계산하지 않고 식 자체를 매개변수 대신 바꾸다가 (더 펼치지 못하는) 마지막에 기본 연산으로만 구성한 표현식으로 값을 계산하는 방법도 있다. 순서대로 계산하는 방식으로 계산해보자. 263 | 264 | `f(5)`를 프로시저 정의에 따라서 펼쳐보면 다음과 같다. 265 | 266 | ```swift 267 | sumOfSquares(x:5 + 1, y: 5 * 2) 268 | square(5 + 1) + square(5 * 2) 269 | (5 + 1)*(5 + 1) + (5 * 2)*(5 * 2) 270 | ``` 271 | 272 | 그리고 귀납법을 적용하면 다음과 같다. 273 | ```swift 274 | (6 * 6) + (10 * 10) 275 | 36 + 100 276 | 136 277 | ``` 278 | 279 | 지금 설명한 계산 절차와 다르지만 결과값은 동일하다. 이처럼 계산식을 끝까지 펼친 다음에 줄이는 계산 방법을 순서대로 계산법 normal-order evaluation 이라고 한다. Lisp 실행기는 인자 먼저 계산법을 적용한다. 280 | 281 | 교체 방식으로 계산해서 결과 값을 구할 수 있는 경우에는, 다른 계산 방식을 사용해도 동일한 결과값이 나온다. 그렇지 않은 경우는 연습 문제 1.5에서 확인할 수 있다. 282 | 283 | Lisp은 인자 먼저 계산하는 방식으로 동작한다. 그래서 (5+1)이나 (5*2)처럼 같은 식을 여러 번 반복해서 계산하지 않는다. 그 이유는 더 빠르게 동작하고, 교체 방식으로 동작하지 않는 프로시저가 있을 순서대로 계산하는 방식이 더 복잡하기 때문이다. 하지만 순서대로 계산하는 방식도 가치가 있기 때문에 3장과 4장에서 설명한다. 284 | 285 | ### 1.1.6 조건 표현식 286 | 287 | 조건에 따라서 계산 표현하는 방법을 정리한다. 예를 들어 음수인지, 0인지, 양수인지 조건에 따라 다르게 계산하고 싶을 때 표현하는 방법이 필요하다. 288 | 289 | 이처럼 경우의 수가 나눠지는 case analysis 경우에 스위프트에서는 switch-case 문법을 사용한다. 290 | 291 | ```swift 292 | func abs(x: Int) -> Int { 293 | switch x { 294 | case x where x > 0: 295 | return x 296 | case x where x < 0: 297 | return -x 298 | default: 299 | return 0 300 | } 301 | } 302 | ``` 303 | 304 | 조건 표현식 중에 switch-case 구문 문법은 다음과 같다. 305 | 306 | ```swift 307 | switch control expression { 308 | case pattern 1: 309 | statements 310 | case pattern 2 where condition: 311 | statements 312 | case pattern 3 where condition, 313 | pattern 4 where condition: 314 | statements 315 | default: 316 | statements 317 | } 318 | ``` 319 | 320 | switch 예약어 다음에는 제어를 위한 표현식 control expression이 있고, 그 아래에는 여러 조건을 case로 표현하는 절cluase이 나온다. 조건 식을 계산하는 방법은 간단하다. 먼저 pattern에 나오는 값을 구하고, control expression과 비교해서 참인지 비교한다. 뒤에 where절이 있는 경우는 condition 조건까지 비교해서 참인 경우에 해당 조건의 statements 표현식을 계산한다. 거짓이면 아래 다음 case 조건으로 내려간다. 마지막까지 조건이 모두 거짓인 경우는 default 구문 아래 statments를 실행한다. 논리식으로 모든 조건을 표현하지 않은 경우는 컴파일러가 에러를 표시한다. 321 | 322 | 절대값을 계산하는 프로시저를 다른 방식으로 만들어보자. 323 | 324 | ```swift 325 | func abs(x: Int) -> Int { 326 | if x < 0 { 327 | return -x 328 | } 329 | else { 330 | return x 331 | } 332 | } 333 | ``` 334 | 335 | switch-case 구문과 비슷하지만 조건식을 표현하기 위해 if 구문을 활용할 수 있다. switch-case가 조건이 여러 개인 경우에 사용한다면, if 구문은 조건이 두 개 뿐일 때 사용하기 좋은 문법이다. 물론 아래처럼 if 구문 문법을 보면 switch-case 구문처럼 여러 조건을 비교할 수 있지만, 권장할 만한 표현은 아니다. 336 | 337 | ```swift 338 | if condition 1 { 339 | statements //to execute if condition 1 is true 340 | } else if condition 2 { 341 | statements //to execute if condition 2 is true 342 | } else { 343 | statements //to execute if both conditions are false 344 | } 345 | ``` 346 | 347 | 실행기가 if 구문 계산할 때 condition 1 조건을 먼저 계산한다. 그 답이 참이라면 {} 괄호 내부 statements를 실행하고, 거짓이면 아래 조건을 계산한다. abs()를 계산하기 위해 사용한 기본 비교 표현과 복잡한 논리나 판단을 할 수도 있다. 논리 연산에 사용하는 기본 연산은 다음과 같이 세 가지가 있다. 348 | 349 | - (`` **&&** `` **&&** ... ``) 350 | 351 | 논리식 를 왼쪽에서 오른쪽으로 차례대로 계산한다. 그 중에 거짓으로 판단하는 가 나오면 and 식은 거짓이 되고, 나머지 값은 계산하지 않는다. 352 | 353 | - (`` **||** `` **||** ... ``) 354 | 355 | 논리식 를 왼쪽에서 오른쪽으로 계산한다. 그 중에 참이라 판단하는 가 나오면 or 식은 참이 되고, 나머지 값은 계산하지 않는다. 356 | 357 | - (**!** ``) 358 | 359 | 가 참이면 not 식은 거짓을, 거짓이면 참을 뒤집는다. 360 | 361 | 이 중에서 and 와 or는 다른 프로시저와 다르게 인자가 되는 모든 논리식 값을 구하지 않기 때문에 특별한 형태로 선언해야 한다. 362 | 363 | 만약 x가 5보다 크고, 10보다 작은 것을 판단하려면 `(x > 5) and (x < 10)` 처럼 작성한다. 364 | 365 | 어떤 수가 다른 수보다 크거나 같은지 판단하는 조건을 프로시저로 선언하면 다음과 같다. 366 | 367 | ```swift 368 | func >= (x: Int, y: Int) -> Bool { 369 | return (x > y) || (x == y) 370 | } 371 | ``` 372 | 373 | 다른 논리식으로 고쳐보면 이렇게 선언할 수도 있다. 374 | 375 | ```swift 376 | func >= (x: Int, y: Int) -> Bool { 377 | return !(x < y) 378 | } 379 | ``` 380 | 381 | ###### 연습문제 1.1 382 | 383 | 스위프트 실행기에서 아래 표현식들을 차례대로 계산하면 어떤 값이 나오는지 확인한다. 384 | 385 | ```swift 386 | 1> 10 387 | $R0: Int = 10 388 | 2> (5 + 3 + 4) 389 | $R1: (Int) = { 390 | _value = 12 391 | } 392 | 3> (9 - 1) 393 | $R2: (Int) = { 394 | _value = 8 395 | } 396 | 4> (6 / 2) 397 | $R3: (Int) = { 398 | _value = 3 399 | } 400 | 5> ((2 * 4) + (4 - 6)) 401 | $R4: (Int) = { 402 | _value = 6 403 | } 404 | 405 | 6> let a = 3 406 | a: Int = 3 407 | 7> let b = a+1 408 | b: Int = 4 409 | 410 | 8> a + b + (a * b) 411 | $R5: Int = 19 412 | 9> (a == b) 413 | $R6: (Bool) = { 414 | _value = 0 415 | } 416 | 417 | 10> if (b > a) && (b < (a*b)) { b } else { a } 418 | 11> if (b > a) && (b < (a*b)) { print(b) } else { print(a) } 419 | 4 420 | 421 | 12> switch (a, b) { 422 | 13. case (4, _): print(6) 423 | 14. case (_, 4): print(6+7+a) 424 | 15. default: print(25) 425 | 16. } 426 | 16 427 | ``` 428 | 429 | 위에 보이는 것처럼 9번까지는 swift 문법으로도 무난하게 표현 가능하다. 430 | 10번처럼 if 구문에 statement 자리에 값만 넣으면 아무것도 출력되지 않는다. {} 내부에 값만 있으면 해당 값을 대체할 뿐, 새로운 값으로 계산하지 않는다. 그래서 11번처럼 실행기에서 값을 확인하고 싶으면 print()라는 프로시저에 인자값으로 전달해야 한다. 431 | 432 | 12번 switch-case 구문에서도 마찬가지다. 조건식 뒤에 변수나 상수에 해당하는 값만 넣으면 아무런 동작을 하지 않는다. 특정한 조건식에 따른 결과를 출력하려면 print() 프로시저를 호출해야 한다. 433 | 434 | 스위프트에서는 if 구문 결과를 다른 연산자에 인자로 넘기는 표현은 불가능하다. 이렇게 값을 넘겨야 하는 경우는 if 구문과 비슷한 삼항 연산자 Ternary Conditional Operator를 사용해야 한다. `condition ? expression used if true : expression used if false` 435 | 436 | ```swift 437 | 17> (2 + if (b > a) { b } else { a }) 438 | error: repl.swift:17:6: error: expected expression after operator 439 | (2 + if (b > a) { b } else { a }) 440 | ^ 441 | 442 | 17> (2 + ((b > a) ? b : a)) 443 | $R7: (Int) = { 444 | _value = 6 445 | } 446 | ``` 447 | 448 | 다음과 같이 switch-case 구문도 if 구문과 마찬가지로 다른 연산자에 인자값으로 표현식을 전달할 수는 없다. 지금 a값이 3이고, b값이 4인 상태에서 19번처럼 a가 switch-case 구문을 대체해서 `a * (a + 1)`로 처리하지 않는다. 다음과 같이 컴파일 에러가 난다. 449 | 450 | ```swift 451 | 18> switch (a,b) { 452 | 19. case (a,b) where a>b: a 453 | 20. case (a,b) where a func select(_ a: Int,_ b: Int) -> Int { 470 | 19. switch (a,b) { 471 | 20. case (a,b) where a>b: return a 472 | 21. case (a,b) where a Int { 488 | switch (a,b,c) { 489 | case (a,b,c) where a > c && b > c: 490 | return a*a + b*b 491 | case (a,b,c) where a > b && c > b: 492 | return a*a + c*c 493 | case (a,b,c) where b > a && c > a: 494 | return b*b + c*c 495 | default: 496 | return 0 497 | } 498 | } 499 | 500 | exam1_3(a: 10, b: 5, c: 3) 501 | exam1_3(a: 10, b: 7, c: 9) 502 | exam1_3(a: 1, b: 7, c: 9) 503 | ``` 504 | 505 | ###### 연습문제 1.4 506 | 507 | 스위프트에서는 문제처럼 연산자를 대체하는 표현도 불가능하다. 다음과 같이 `값 (if 구문과 연산자) 값` 형태 표현식은 동작하지 않는다. 508 | 509 | ```swift 510 | func a_plus_abs_b(a: Int, b: Int) -> Int { 511 | return a (if (b > 0) { + } 512 | else { - }) b 513 | } 514 | ``` 515 | 516 | 다음과 같이 if 구문 아래 값과 연산자를 풀어서 작성하거나 연산자를 다른 방식으로 추상화해야 한다. 다른 방식은 더 복잡한 문법 표현 때문에 여기서 설명하지 않는다. 517 | 518 | ```swift 519 | func plus(_ a: Int, _ b: Int) -> Int { 520 | return a + b 521 | } 522 | func minus(_ a: Int, _ b: Int) -> Int { 523 | return a - b 524 | } 525 | func a_plus_abs_b(a: Int, b: Int) -> Int { 526 | if (b > 0) { 527 | return plus(a, b) 528 | } 529 | else { 530 | return minus(a, b) 531 | } 532 | } 533 | ``` 534 | 535 | ###### 연습문제 1.5 536 | 537 | ```swift 538 | func p() -> Int { 539 | return p() 540 | } 541 | 542 | func test(_ x: Int, _ y: Int) -> Int { 543 | if (x == 0) { 544 | return 0 545 | } 546 | else { 547 | return y 548 | } 549 | } 550 | 551 | test(0, p()) 552 | ``` 553 | 554 | > 인자 먼저 계산법 555 | 556 | `test(0, p())` 에서 x에는 0을 y에는 p()를 대체하기 전에 0는 값이지만 p()는 프로시저라서 p()를 먼저 계산해야만 한다. p()를 대체하기 전에 p()를 적용해보자. p() 내부에서 다시 p()를 호출하기 때문에 재귀로 반복된다. 모든 인자를 대체하기 전에 p()를 빠져나올 수 없다. 557 | 558 | > 순서대로 계산법 559 | 560 | `test(0, p())`에서 순서대로 x부터 0 대체하고 y대신에 p()로 대체한다고 가정하자. 561 | if 구문을 적용해보니 x값이 0이라서 `return 0`가 실행되고 y값에 포함한 p()는 필요하지 않다. 562 | 563 | #### 1.1.7 연습 뉴튼 계산법으로 제곱근 찾기 564 | 565 | 위에서 설명했던 프로시저들은 매개변수에 전달하는 인자값에 따라 결과가 달라지기 때문에, 수학의 함수와 매우 비슷하다. 그렇지만 수학의 순수 함수와 컴퓨터 프로시저 사이에는 `효율성`을 고려해야 한다는 중요한 차이점이 있다. 566 | 567 | 제곱근을 구하는 수학 문제를 살펴보자. 568 | 569 | `𝑦 ≥ 0 이고 𝑦²=𝑥 일때 √𝑥=𝑦 다.` 570 | 571 | 이 정의는 수학에서 올바른 함수 정의다. 이 함수로 한 수가 다른 수의 제곱근인지 확인하거나, 제곱근에 대한 일반적인 여러 사실을 추론할 수도 있다. 하지만 이 정의에는 계산을 위한 절차 - 프로시저가 없다. 수학의 함수 정의만 보고 한 수의 제곱근을 어떻게 계산하는 지 알수 없다. 다음과 같이 프로그램 언어로 흉내 내어 함수를 선언해도 마찬가지다. 572 | 573 | ```swift 574 | func sqrt(x: Int) -> Int { 575 | return y where (y >= 0 && square(y) == x) 576 | } 577 | ``` 578 | 579 | 역시나 같은 질문이 반복되고 해결되지 않는다. 580 | 581 | 함수와 프로시저의 차이는 물체가 어떤 성질을 지니는지 알아내는 것과 그것을 어떻게 만들지 알아내는 정도 차이다. 흔히 문제가 무엇인지 설명하는 지식declarative knowledge과 문제를 해결하기 위한 방법에 대한 지식imperative knowledge의 차이점이라고 한다. 수학은 무엇인지 선언하는 일에 관심을 가지지만, 컴퓨터 과학에서는 어떻게 만드는지 선언하는 데 관심을 둔다. 582 | 583 | 그러면 사람은 제곱근을 어떻게 계산할까? 가장 유명한 방식인 뉴튼 계산법으로 가장 가까운 값을 근사값을 반복해서 계산해보자. 계산 방법은 다음과 같다. x 제곱근에 가까운 y 값이 있을 때 y와 x/y의 평균을 구해서 진짜 제곱근에 더 가까운 값을 계산한다. 예를 들어 2의 제곱근은 다음과 같이 계산한다. 처음에는 1을 가까운 값이라고 가정하자. 584 | 585 | ![제곱근 계산](https://github.com/godrm/SICP-Swift/blob/master/images/1.1.7.png?raw=true) 586 | 587 | 이런 프로세스를 반복하다보면 점차 2의 제곱근에 가까운 값을 구할 수 있다. 588 | 589 | 이 과정을 프로시저로 표현해보자. 제곱근을 구하려는 수radicand x와 제급근에 가까운 근사값 guess를 가지고 계산을 시작한다. 계산하다가 얼추 맞는 값이라고 판단하면 멈추거나 혹은 더 가까운 값을 찾기 위해서 반복한다. 이런 흐름으로 프로시저를 선언하면 다음과 같다. 590 | 591 | ```swift 592 | func sqrt_iter(_ guess: Double, _ x: Double) -> Double { 593 | if good_enough(guess, x) { 594 | return guess 595 | } 596 | else { 597 | return sqrt_iter(improve(guess, x), x) 598 | } 599 | } 600 | ``` 601 | 602 | 근사값과 x를 근사값으로 나눈 값 (x / guess) 평균을 계산해서 참 제곱근에 더 가까운 값을 어림잡아 계산할 수 있다. 603 | 604 | ```swift 605 | func improve(_ guess: Double, _ x: Double) -> Double { 606 | return average(guess, x / guess) 607 | } 608 | 609 | func average(_ x: Double, _ y: Double) -> Double { 610 | return (x + y) / 2 611 | } 612 | ``` 613 | 614 | 이제 얼마나 더 가까워야 충분히 계산했는지(good-enough) 판단할 지 정해보자. 이렇게 선언하면 동작하지만 더 좋은 방법도 있다. 이 프로시저 활용하면 근사값을 제곱한 값에서 x를 뺀 차이가 미리 정한 기준(여기서 0.001)을 넘지 않을 때까지 반복해서 계산한다. 615 | 616 | ```swift 617 | func square(_ x : Double) -> Double { x * x } 618 | 619 | func good_enough(_ guess: Double, _ x: Double) -> Bool { 620 | return abs(square(guess) - x) < 0.001 621 | } 622 | 623 | func sqrt(x: Double) -> Double { 624 | return sqrt_iter(1, x) 625 | } 626 | ``` 627 | 628 | 지금까지 정의한 모든 프로시저를 스위프트 실행기에 넣으면 sqrt 프로시저를 내장 프로시저처럼 사용할 수 있다. 629 | 630 | ```swift 631 | sqrt(x: 9) 632 | $R1: Double = 3.0000915541313802 633 | sqrt(x: (100 + 37)) 634 | $R2: Double = 11.704699917758145 635 | sqrt(x: sqrt(x: 2) + sqrt(x: 3)) 636 | $R3: Double = 1.7739279023207892 637 | square( sqrt(x: 1000) ) 638 | $R4: Double = 1000.0003699243661 639 | ``` 640 | 641 | 지금 설명한 sqrt 프로그램은 다른 프로그래밍 언어로 표현하는 계산 절차에서 사용하는 반복처리를 위한 문법이 하나도 없어서 놀라울 지도 모른다. sqrt_iter()처럼 다른 프로시저를 재귀로 반복하는 것으로도 충분하다. 642 | 643 | ###### 연습문제 1.6 644 | 645 | 스위프트를 공부하던 엘리는 if 구문만 특별하다는 게 어색했다. 그래서 다음과 같은 new_if 프로시저를 선언하고 3항 비교 연산자를 정의했다. 646 | 647 | ```swift 648 | func new_if(predicate: Bool, then_clause: Double, else_clause: Double) -> Double { 649 | return (predicate) ? then_clause : else_clause 650 | } 651 | ``` 652 | 이 프로시저가 if 표현처럼 동작한다는 것을 보여주려고 아래처럼 작성했다. 653 | 654 | ```swift 655 | new_if(predicate:(2==3), then_clause:0, else_clause:5) 656 | $R0: Int = 5 657 | 658 | new_if(predicate:(1==1), then_clause:0, else_clause:5) 659 | $R1: Int = 0 660 | ``` 661 | 662 | 그래서 제곱근 프로그램 내부를 다음과 같이 new_if를 써서 고쳤다. 663 | 664 | ```swift 665 | func sqrt_iter(_ guess: Double, _ x: Double) -> Double { 666 | return new_if(predicate: good_enough(guess, x), 667 | then_clause: guess, 668 | else_clause: sqrt_iter(improve(guess, x), x)) 669 | } 670 | ``` 671 | 672 | 새롭게 선언한 new_if를 실행하기 전에 인자값을 계산하기 위해서 guess와 sqrt_iter()를 먼저 계산해야 한다. 그러면서 sqrt_iter() 재귀로 반복하면서 또 반복한다. 결국 끝나지 않고 무한 반복되다가 프로그램이 멈춘다. 673 | 674 | ###### 연습문제 1.7 675 | 676 | 앞에서 구현한 good_enough() 구현부분 `abs(square(guess) - x) < 0.001` 계산 방식으로는 아주 작은 수나 큰 수의 제곱근을 구할 때 올바르게 동작하지 않는다. 677 | 다른 방법으로는 guess 근사값을 구하면서 기존 계산값과 비교하면 새로운 계산값 차이가 아주 작아질 때까지 반복하는 것이다. 이 방식으로 제곱근 프로시저를 개선해보고, 아주 작은 수나 큰 수에 대한 제곱근을 구할 때 개선됐는지 확인한다. 678 | 679 | > 0.001보다 작은 수가 나오면 오차가 발생할 수 있다. 680 | > 너무 큰 수가 나오면 계산하다가 근사값과 차이가 커서 끝나지 않는다. 681 | 682 | 해결방법은 다음과 같이 근사값 차이를 이전 값으로 나눠서 값 자체가 커지거나 작아져도 비율로 계산할 수 있도록 하는게 좋다. 683 | 684 | ```swift 685 | func good_enough2(_ guess: Double, _ x: Double) -> Bool { 686 | return (abs(square(guess) - x) / x) < 0.0001 687 | } 688 | ``` 689 | 690 | ###### 연습문제 1.8 691 | 692 | 세제곱근 cube root를 구하는 뉴튼 계산법은 x의 세제곱근 근사값을 y라고 할 때, 더 가까운 다음 y값을 계산하는 것이다. 693 | 694 | ![세제곱근 방정식](https://github.com/godrm/SICP-Swift/blob/master/images/example1.8.png?raw=true) 695 | 696 | > 아직 동작 안함 697 | 698 | ```swift 699 | func cube(_ x: Double) -> Double { 700 | return x * x * x 701 | } 702 | 703 | func good_enough_cube(_ guess: Double, _ x: Double) -> Bool { 704 | return (abs(cube(guess) - x) / x ) < 0.0001 705 | } 706 | 707 | func improve_cube(_ guess: Double, _ x: Double) -> Double { 708 | return (x / (guess * guess) + 2 * guess) / 3 709 | } 710 | 711 | func cuberoot_iter(_ guess: Double, _ x: Double) -> Double { 712 | if good_enough_cube(guess, x) { 713 | return guess 714 | } 715 | else { 716 | return cuberoot_iter(improve_cube(guess, x), x) 717 | } 718 | } 719 | 720 | func cuberoot(x: Double) -> Double { 721 | return cuberoot_iter(1, x) 722 | } 723 | ``` 724 | 725 | #### 1.1.8 블랙박스처럼 간추린 프로시저 726 | 727 | 앞서 설명한 sqrt() 예제가 전체 프로세스에서 다른 프로시저를 호출하는 형태다. sqrt_iter()는 재귀recursive로 동작하는 프로시저라는 것을 눈여겨봐야 한다. 728 | 729 | ![그림1.2 sqrt 프로그램을 여러 단계로 조각낸 모습](https://github.com/godrm/SICP-Swift/blob/master/images/1.1.8.png?raw=true) 730 | 731 | 프로시저 선언 내부에서 자신의 일부를 불러서 사용한다. 프로시저 선언 부분에서 자기를 다시 부르도록 선언하는 게 어색할 수 있다. 프로시저 절차를 계속해서 빙빙 돌아가도록 정의하는 게 말이 안된다고 생각할 수 있다. 732 | 733 | 그림1.2를 보면 제곱근 전체를 구하는 큰 문제가, 근사값이 충분한가 판단하는 문제와 더 정확한 근사값을 구하는 문제 등으로 나눠진다. 이렇게 문제를 나눠서 작업하는 (문제를 푸는) 프로시저가 따로 있다. 그리고 여러 프로시저를 하나로 묶어서 sqrt() 프로그램을 구성한다. 734 | 735 | 큰 문제를 해결하는 프로시저를 아무 생각 없이 자르기만 하는 것은 쉽다. 더 중요한 것은 프로시저 하나를 조립식 부품module처럼 만들어서 다른 프로시저를 선언할 때도 사용할 수 있도록 잘라내는 것이다. square()를 사용해서 good_enough() 프로시저를 선언할 때 square() 프로시저가 구체적으로 어떻게 계산하는지 몰라도 제곱한 값을 내놓는 것을 알 수 있다. square()를 사용하는 곳에서 square()가 제곱을 어떤 방식으로 계산하는지 세세하게 알아야 하는 것은 아니다. good-enough() 프로시저 입장에서 보면 square() 계산 절차라기 보다는 프로시저를 묶어놓은 이름일 뿐이다. 이 과정을 프로시저로 요약하기 procedural abstraction라고 부른다. 프로시저를 부르는(호출하는) 쪽에서는 어떤 프로시저를 쓰더라도 제곱한 값을 계산해서 얻기만 하면 된다. 736 | 737 | 만약 simd 모듈을 사용해서 exp2() 나 log2() 함수를 호출해서 구현해도 square()를 호룰하는 입장에서는 둘 다 인자로 받은 값을 제곱해서 전달해 주는 것은 마찬가지다. 738 | 739 | ```swift 740 | import simd 741 | func square(_ x : Double) -> Double { 742 | exp2(double(log2(x))) 743 | } 744 | 745 | func double(_ x: Double) -> Double { return x + x } 746 | ``` 747 | 748 | 이처럼 프로그램을 여러 프로시저로 나누어 작성할 때는, 하위 프로시저가 어떻게 계산하는지 드러나지 않도록 감춰서 작성하는 게 중요하다. 그래야 프로시저를 사용하는 사람들이 필요한 모든 프로시저를 모두 만들어 사용하지 않고, 블랙박스처럼 불러서 사용할 수 있다. 프로시저를 사용하는 사람은 그 프로시저가 무엇을 하는지만 알면 되고, 굳이 어떻게 만들었는지는 몰라도 된다. 749 | 750 | ##### 지역 이름 local name 751 | 752 | 프로시저를 만드는 내부 입장에서는, 그것을 호출해서 사용하는 곳에 영향을 주면 안된다. 스위프트에서는 매개변수 이름이 다르면 프로시저도 다른 프로시저로 인식한다. 그래서 아래 두 square() 프로시저는 `square(x:)` 와 `square(y:)`로 구분한다. 753 | 754 | ```swift 755 | func square(x: Double) -> Double { return x * x } 756 | 757 | func square(y: Double) -> Double { return y * y } 758 | ``` 759 | 760 | 그렇지만 매개변수 이름은 프로시저 내부 지역에 갇힌 형태로 구현 내부에서만 사용하는 이름이다. 761 | 762 | ##### 매개변수 이름 Parameter name과 로컬 이름 local name 763 | 764 | 스위프트에서는 다른 언어와 다르게 호출할 때 사용하는 매개변수 이름과 내부에서 사용하는 로컬 이름을 구분할 수 있다. 765 | 766 | 아래 코드를 보면 첫 번째 선언한 프로시저는 호출할 때 `square(x:)`로 인식한다. 두 번째 프로시저는 `square(with:)`로 인식하고 `square(with: 5)` 이렇게 호출해야 한다. 5라는 값이 할당되는 로컬 이름은 x다. 그리고 이 두 프로시저는 서로 다른 프로시저로 인식한다. 세 번째처럼 매개변수 이름을 생략할 수도 있는데, `square(x:)` 프로시저로 인식하지만 매개변수 이름을 생략한 것이다. 그래서 호출할 때도 `square(5)` 형태로 호출한다. 이렇게 세 가지 형식은 모두 다른 프로시저로 인식한다. 767 | 768 | ```swift 769 | func square(x: Double) -> Double { return x * x } 770 | 771 | func square(with x: Double) -> Double { return x * x } 772 | 773 | func square(_ x: Double) -> Double { return x * x } 774 | ``` 775 | 776 | 앞서 설명했던 코드에서 `good_enough()` 프로시저를 매개변수 이름을 생략하고 다음과 같이 선언했었다. 777 | 778 | ```swift 779 | func good_enough(_ guess: Double, _ x: Double) -> Bool { 780 | return abs(square(guess) - x) < 0.0001 781 | } 782 | ``` 783 | 784 | 여기서도 내부 이름으로 x를 사용하는 데 square()에서 사용하는 x와는 전혀 다른 것이다. 각자 지역에서만 사용하는 이름일 뿐이다. 만약 다른 프로시저 바깥에서 내부에 인자 이름을 사용하게 된다면 good_enough()에서 x인지 square()에서 x인지 뒤죽박죽 섞이게 될 것이다. square()는 블랙박스여야 한다. 785 | 786 | 이렇게 프로시저에 매개변수 인자 이름이 얽매인다bind는 뜻에서 매인 변수bound variable라고 한다. 프로시저 선언 내부에서 인자 이름을 접근할 수 있는 표현식들을 유효 범위scope라고 한다. 위에서 guess와 x는 good_enough에 매인 변수지만, abs(), square()는 프로시저 선언에 얽매이지 않은 자유 범위를 가진다. 만약 guess를 abs로 바꾸면 매인 변수 abs가 자유 범위 abs 대신 덮어서 사용하게 된다. 하지만 good_enough()는 이미 바깥쪽에서 정의한 이름 abs를 쓰고 있기 때문에 자유 변수에서 벗어나는 것은 아니다. 787 | 788 | ###### 안쪽 정의와 블록 구조 789 | 790 | 앞서 설명한 것은 인자 이름이 프로시저에 얽매인 것을 설명하면서 이름 가둬놓기name isolation을 설명했다. 791 | 792 | sqrt() 내부를 여러 프로시저로 나눠서 선언했지만 사용할 때는 sqrt()만 있으면 된다. 다른 프로시저들은 헷갈리게 만들고, 다른 프로세저에서 같은 이름을 사용하려면 이미 sqrt()에서 선언했기 때문에 안된다. 여러 사람들이 큰 시스템을 함께 만들 때는 이렇게 프로시저 이름이 충돌하는 문제가 생길 수 있다. 이럴 경우에 sqrt()와 함께 관련이 있는 프로시저 good_enough나 improve 같은 이름을 한 프로시저 내부에 가둘 수도 있다. 이렇게 구현해서 다시 작성하면 다음과 같다. 793 | 794 | ```swift 795 | func sqrt(x: Double) -> Double { 796 | func improve(_ guess: Double, _ x: Double) -> Double { 797 | return average(guess, x / guess) 798 | } 799 | 800 | func good_enough(_ guess: Double, _ x: Double) -> Bool { 801 | return abs(square(guess) - x) < 0.0001 802 | } 803 | 804 | func sqrt_iter(_ guess: Double, _ x: Double) -> Double { 805 | if good_enough(guess, x) { 806 | return guess 807 | } 808 | else { 809 | return sqrt_iter(improve(guess, x), x) 810 | } 811 | } 812 | return sqrt_iter(1, x) 813 | } 814 | ``` 815 | 816 | 이렇게 프로시저 정의를 겹쳐 쓰는 방식을 불록 구조block structure라 한다. 이렇게 프로시저를 안쪽에 감추기 때문에 프로시저 정의를 짧게 줄일 수 있다. 우선 x가 sqrt에 매개 변수로 넘어온 얽매인 변수라서 다른 프로시저 선언마다 x를 넣지 않고 자유 변수로 만들어도 된다. 817 | 818 | ```swift 819 | func sqrt(x: Double) -> Double { 820 | func improve(_ guess: Double) -> Double { 821 | return average(guess, x / guess) 822 | } 823 | 824 | func good_enough(_ guess: Double) -> Bool { 825 | return abs(square(guess) - x) < 0.0001 826 | } 827 | 828 | func sqrt_iter(_ guess: Double) -> Double { 829 | if good_enough(guess) { 830 | return guess 831 | } 832 | else { 833 | return sqrt_iter(improve(guess)) 834 | } 835 | } 836 | return sqrt_iter(1.0) 837 | } 838 | ``` 839 | 840 | 이제부터는 큰 프로그램을 여러 조각으로 나누고 쉽게 선언하기 위해서 블록 구조를 자주 활용한다. 841 | 842 | ---- 843 | 844 | [다음 1.2 Procedures and the Processes They Generate](https://github.com/godrm/SICP-Swift/blob/master/1.2.md) 845 | -------------------------------------------------------------------------------- /1.2.md: -------------------------------------------------------------------------------- 1 | 2 | ### 이전 목차 3 | 4 | [1장 프로시저로 요약하는 방식](https://github.com/godrm/SICP-Swift/blob/master/Chapter1.md) 5 | 6 | [이전 1.1 The Elements of Programming](https://github.com/godrm/SICP-Swift/blob/master/1.1.md) 7 | 8 | ## 1.2 프로시저와 프로세스 Procedures and the Processes They Generate 9 | 10 | 지금까지 체스 게임으로 치면 이제 말을 움직이는 법만 배웠다. 처음 수를 어떻게 두는지나 어떤 전략과 전술로 말을 움직이는 지는 모른다. 체스 초보처럼 어떤 말을 어떻게 움직이는 게 더 중요한지 - 어떤 프로시저를 어떻게 정의해야 하는지 지식이 부족하다. 말을 움직이면 어떤 결과가 나올 지 - 프로시저를 실행 결과를 미리 예측하는 경험도 부족하다. 11 | 12 | 새로운 프로시저를 선언해서 프로그램을 만들어 내는 일을 할 때는 어떤 선언이 어떤 결과가 나올지 머릿 속으로 상상해서 그려낼 수 있어야 한다. 전문 사진 작가가 담고 싶은 장면을 상상 속에서 그려내서 미리 카메라를 조작할 수 있는 것처럼, 프로그램을 작성하는 개발자도 마찬가지다. 어떤 계산 결과를 얻기 위해서는 어떤 프로세스를 순서대로 진행해야 하는지 미리 정해서 설계하고 구현할 수 있어야 한다. 전문가라면 여러 가지 절차가 만들어 내는 프로세스를 미리 그려낼 수 있도록 학습해야 한다. 13 | 14 | 프로시저 (또는 프로그래밍 함수)는 한 컴퓨터 프로세스가 어떻게 진행해 나아가는지 local evolution of a computer를 의미한다. 지역 범위에서 절차대로 실행하는 하나의 프로시저가 아니라 여러 프로세스가 움직이는 모습을 전체 범위에서 간추려 봐야할 때가 있다. 15 | 16 | 여기서는 프로세스가 계산할 때 사용하는 시간과 공간 자원이 어떤 비율로 늘어나는지도 설명한다. 17 | 18 | ### 1.2.1 되돌리거나 recursion 반복하는 iteration 프로세스 19 | 20 | 사다리곱 factorial 함수가 있다고 하자. 21 | 22 | n! = n・(n-1)・(n-2)・・・3・2・1 23 | 24 | 사다리곱을 계산하는 방식을 잘 관찰해보면, 0보다 큰 수 n이 있을 때 n!값은 n과 (n-1)! 곱과 같다는 것이다. 25 | 26 | n! = n·[(n − 1)·(n − 2)···3·2·1] = n·(n − 1)! 27 | 28 | 따라서 (n-1)!을 계산한 값에 n을 곱하면 n! 값을 계산할 수 있다. 1!은 1이라는 것을 포함해서 계산 방법을 그대로 표현해보자. 29 | 30 | ```swift 31 | func factorial(n: Int) -> Int { 32 | if (n ==1) { 33 | return 1 34 | } 35 | else { 36 | return n * factorial(n - 1) 37 | } 38 | } 39 | ``` 40 | 41 | 이 계산 절차대로 6! 값을 계산해보면 1.1.5절에 나온 계산법에 따라 그림1.3과 같은 프로세스를 설명할 수 있다. 42 | 43 | 44 | 45 | > 그림1.3 6! 값을 계산하는 선형 재귀 프로세스 46 | 47 | 또 다른 계산 방법을 고민해보자. 계속해서 곱하는 값을 product라고 하고, 1부터 n까지 단계로 세는 변수를 counter라고 하자. 한 단계씩 진행할 때마다 counter와 product는 아래 규칙에 따라 바뀐다. 48 | 49 | product ← counter · product 50 | counter ← counter + 1 51 | 52 | ```swift 53 | func factorial(n: Int) -> Int { 54 | func fact_iter(_ product: Int, _ counter: Int, _ max: Int) -> Int { 55 | if (counter > max) { 56 | return product 57 | } 58 | else { 59 | return fact_iter((counter * product), (counter + 1), max) 60 | } 61 | } 62 | return fact_iter(1, 1, n) 63 | } 64 | ``` 65 | 66 | 67 | 68 | > 그림1.4 6! 값을 계산하는 선형 반복 프로세스 69 | 70 | 그림 1.3처럼 재귀로 표현한 계산 절차나 그림1.4처럼 반복하는 계산 절차 모두 n! 값을 계산하기 위해서 단계 수는 n에 비례해서 증가한다. 그래서 선형 재귀와 선형 반복이라고 부른다. 71 | 72 | 반복하는 프로세스는 프로그램에 있는 변수가 프로세스 계산 단계마다 어떤 상태인지 나타내기 때문에, 계산 중간에 멈추더라도 세 변수 값을 알고 있으면 다음 단계를 이어갈 수 있다. 반면에 재귀 되돌이 프로세스는 상태 변수가 없어서 어디까지 계산했는지 정보가 더 필요하다. 73 | 74 | 프로세스와 프로시저를 재귀로 표현하는 게 비슷한 것 같지만, 재귀 프로세스와 재귀 프로시저를 구분해서 이해해야 한다. 재귀 프로시저(또는 함수)는 내부에 선언한 표현식에서 자기 자신을 호출한다는 뜻이다. 반면에 재귀 프로세스는 프로시저 선언이 되돌아 간다는 의미가 아니라, 계산 방식 자체가 재귀 형태라는 의미다. fact_iter() 자체는 재귀 프로시저지만, 프로세스는 계산을 (재귀가 아니라) 반복할 뿐이다. 75 | 76 | C나 파스칼, 자바 같은 일반적인 프로그래밍 언어는 재귀 호출을 하면 호출하는 횟수에 비례해서 기억 공간을 사용한다. Lisp이나 Scheme 같은 언어들은 꼬리 재귀 tail-recursive 실행 방식을 구현하고 있어서, 지정된 기억 공간만을 사용해서 재귀가 돌아가도록 처리한다. **아쉽게도 스위프트도 컴파일 이후에는 C 언어 함수와 비슷하게 동작하며 꼬리 재귀를 지원하지 않는다. ** 77 | 78 | ##### 연습문제 1.9 79 | 80 | ```swift 81 | //A형태 82 | func plus(a: Int, b: Int) -> Int { 83 | return (a == 0) ? b : inc( plus( dec(a), b ) ) 84 | } 85 | 86 | /* 87 | plus(4, 5) 88 | 4 == 0 ? 5 : inc(plus(dec(4), 5)) 89 | inc(plus(dec(4), 5)) 90 | ... 91 | inc(plus(3,5)) 92 | ... 93 | inc(inc(plus(2,5))) 94 | ... 95 | inc(inc(inc(plus(1,5)))) 96 | ... 97 | inc(inc(inc(inc(plus(0, 5))))) 98 | ... 99 | inc(inc(inc(inc( 0 == 0 ? 5 : inc(plus(dec(0), 5)))))) 100 | inc(inc(inc(inc( 5 )))) 101 | inc(inc(inc(6))) 102 | inc(inc(7)) 103 | inc(8) 104 | 9 105 | */ 106 | ``` 107 | 108 | ```swift 109 | //B형태 110 | func plus(a: Int, b: Int) -> Int { 111 | return (a == 0) ? b : plus( dec(a), inc(b) ) 112 | } 113 | 114 | /* 115 | plus(4, 5) 116 | 4 == 0 ? 5 : plus(dec(4), inc(5)) 117 | plus(dec(4), inc(5)) 118 | ... 119 | plus(3, 6) 120 | ... 121 | plus(2, 7) 122 | ... 123 | plus(1, 8) 124 | ... 125 | plus(0, 9) 126 | 0 == 0 ? 9 : plus(dec(0, inc(9))) 127 | 9 128 | */ 129 | ``` 130 | 131 | ##### 연습문제 1.10 132 | 133 | > 애커만 함수 Ackemann function 134 | 135 | ```swift 136 | func A(x: Int, y:Int) -> Int { 137 | switch (x, y) { 138 | case (_, 0): return 0 139 | case (0, _): return 2 * y 140 | case (_, 1): return 2 141 | default: 142 | return A(x: (x-1), y: A(x: x, y:(y-1))) 143 | } 144 | } 145 | 146 | A(x:1, y:10) 147 | $R0: Int = 1024 148 | 149 | A(x:2, y:4) 150 | $R1: Int = 65536 151 | 152 | A(x:3, y:3) 153 | $R2: Int = 65536 154 | ``` 155 | 156 | 157 | ```swift 158 | func f(_ n: Int) -> Int { 159 | return A(x:0, y:n) 160 | } 161 | 162 | func g(_ n: Int) -> Int { 163 | return A(x:1, y:n) 164 | } 165 | 166 | func h(_ n: Int) -> Int { 167 | return A(x:2, y:n) 168 | } 169 | 170 | func k(_ n: Int) -> Int { 171 | return 5 * n * n 172 | } 173 | ``` 174 | 175 | 176 | #### 1.2.2 여러 갈래로 되도는 tree recursion 프로세스 177 | 178 | 0, 1, 1, 2, 3, 5, 8, 13, 21 ... 179 | 180 | 181 | 182 | 위 수학 정의에 따라 프로시저를 만들면 아래처럼 되도는 프로시저가 된다. 183 | 184 | ```swift 185 | func fibo(n: Int) -> Int { 186 | switch n { 187 | case 0: return 0 188 | case 1: return 1 189 | default: 190 | return (fibo(n-1) + fibo(n-2)) 191 | } 192 | } 193 | ``` 194 | 195 | 196 | 197 | 이 예제는 여러 갈래로 되도는 프로세스 예시일 뿐, 같은 계산을 반복하기 때문에 효율적이지 않다. 이 방식대로 하면 특정한 n에 대해서 fibo(n)는 지수에 비례해서 exponentially 증가한다. 이렇게 여러 갈래로 되도는 프로세스에서는 계산을 위해 반복하는 단계는 나무 마디 수에 비례하고, 기억 고간의 크기는 가장 깊은 나무 키에 비례한다. 198 | 199 | 동일한 피보나치 수열을 반복 프로세스로 계산해서 구할 수 있다. 200 | 201 | ```swift 202 | func fibo(n: Int) -> Int { 203 | fibo_iter(1, 0, n) 204 | } 205 | 206 | func fibo_iter(_ a: Int, _ b: Int, _ count: Int) -> Int { 207 | if (count == 0) { 208 | return b 209 | } 210 | else { 211 | return fibo_iter( (a+b), a, (count-1)) 212 | } 213 | } 214 | ``` 215 | 216 | 이렇게 선형 반복으로 계산해도 결과는 동일하다. 계산 단계가 n에 선형적으로 비례하는 방식과 fibo(b)에 비례하는 방식은 과정에서 계산 단계 수가 (숫자가 커질수록) 차이가 난다. 217 | 218 | 피보나치 수열에서는 여러 갈래로 되도는 프로세스가 쓸모 없는 것 같아도, 계층 구조가 반복되는 데이터 구조를 다루는 경우에는 쓸모가 있따. 특히 수학적인 정의와 가장 비슷한 선언적인 표현식이 가능하다. 219 | 220 | ###### 연습 : 돈 바꾸는 방법 221 | 222 | 1달러를 50센트, 25센트, 10센트, 5센트, 1센트 동전으로 바꾸는 방법을 프로시저로 표현해보자. 223 | 224 | 바꿀 수 있는 동전이 정해져 있다면 되도는 프로세스로 정의하면 다음과 같다. 225 | a만큼 돈이 있을 때 n가지 동전으로 바꾸는 경우의 수는 다음 경우의 수를 합한 값이다. 226 | 227 | - 맨 처음 나오는 한 가지 동전을 빼고, 남은 동전으로 바꾸는 경우의 수 228 | - 처음 나오는 동전이 d짜리일 때 a-d 계산한 돈을 n가지 동전으로 바꾸는 경우의 수 229 | 230 | 크게 보면 동전을 하나 골라서 처음 나오는 동전을 사용하는 방법과 그렇지 않고 사용하지 않는 방법이 있다는 것이다. 이 구분을 그대로 적용하면, 계속 반복해서 선택한 동전 경우의 수를 줄이면서 그 돈을 남은 동전으로 바꾸는 문제가 된다. 다만 이 알고리듬이 동작하려면 아래처럼 규칙이 끝나는 경우를 모두 찾아서 정리해야 한다. 231 | 232 | - a가 0이면, 동전으로 바꾸는 방법은 1 가지 뿐이다. 233 | - a가 0보다 적으면, 동전으로 바꾸는 방법은 없다. (경우의 수는 0개) 234 | - n이 0이면, 동전으로 바꾸는 방법은 없다. (경우의 수는 0개) 235 | 236 | 이 규칙을 코드로 작성해보자. 237 | 238 | ```swift 239 | func count_change(amount: Int) -> Int { 240 | cc(amount, 5) 241 | } 242 | 243 | func cc(_ amount: Int, _ kinds: Int) -> Int { 244 | if amount == 0 { 245 | return 1 246 | } 247 | else if (amount < 0) || (kinds == 0) { 248 | return 0 249 | } 250 | else { 251 | return cc(amount, kinds-1) 252 | + cc(amount - first_denomination(kinds), kinds) 253 | } 254 | } 255 | 256 | func first_denomination(_ kinds: Int) -> Int { 257 | switch kinds { 258 | case 1: return 1 259 | case 2: return 5 260 | case 3: return 10 261 | case 4: return 25 262 | case 5: return 50 263 | default: 264 | return 0 265 | } 266 | } 267 | ``` 268 | 269 | ##### 연습문제 1.11 270 | 271 | n < 3 일 때 f(n)=n 이고, n ≥ 일 때 f(n)=(n-1) + 2f(n-2) + 3f(n-3)으로 정의한 함수 f. 272 | 273 | ###### 되도는 프로세스 274 | 275 | ```swift 276 | func f_recursive(n: Int) -> Int { 277 | if (n < 3) { 278 | return n 279 | } 280 | else { 281 | return f_recursive(n-1) + 282 | 2 * f_recursive(n-2) + 283 | 3 * f_recursive(n-3) 284 | } 285 | } 286 | ``` 287 | 288 | ###### 반복 프로세스 289 | 290 | ```swift 291 | func f_iterate(n: Int) -> Int { 292 | if (n < 3) { 293 | return n 294 | } 295 | else { 296 | return f_iterate_calculate(a:2, b:1, c:0, count:n-2) 297 | } 298 | } 299 | func f_iterate_calculate(a: Int, b: Int, c: Int, count: Int) -> Int { 300 | if (count == 0) { 301 | return a 302 | } 303 | else { 304 | return f_iterate_calculate(a:a + 2*b + 3*c, b:a, c:b, count:count-1) 305 | } 306 | } 307 | ``` 308 | 309 | ##### 연습문제 1.12 310 | 311 | 파스칼 삼각형 Pascal's triangle. 312 | 313 | ``` 314 | 1 315 | 1 1 316 | 1 2 1 317 | 1 3 3 1 318 | 1 4 6 4 1 319 | ... 320 | ``` 321 | 322 | ###### 되도는 프로세스 323 | 324 | ```swift 325 | func pascal_triangle(row: Int, index: Int) -> Int { 326 | if (index > row) { 327 | return 0 328 | } 329 | else if (index == 1) || (index == row) { 330 | return 1 331 | } 332 | else { 333 | return pascal_triangle(row: row-1, index: index-1) + 334 | pascal_triangle(row: row-1, index: index) 335 | } 336 | } 337 | ``` 338 | 339 | ##### 연습문제 1.13 340 | 341 | (패스) 342 | 343 | #### 1.2.3 프로세스가 자라나는 정도 344 | 345 | 문제에 따라서 어떤 프로세스를 선언하느냐에 따라서 계산할 때 사용하는 자원 크기가 달라진다. 프로세스마다 자원 차이를 비교할 때 자라나는 정도 order of growth 라는 개념을 사용한다. 입력의 크기에 따라서 프로세스가 사용하는 자원 양이 자라나는 정도를 말한다. 346 | 347 | 문제의 크기를 매개변수 n이라고 할 때, n만큼 큰 문제를 푸는 데 사용하는 자원 양을 R(n)이라고 가정하자. 함수 인자로 n이 있어서가 아니라 아예 없는 경우도 있다. 제곱근 값을 계산하는 과정에서 정확한 숫자의 개수를 n으로 본다. 행렬을 곱하는 문제에서는 가로줄 개수를 n으로 볼 수 있다. 어떤 경우에는 연산 과정에서 레지스터를 몇 개 사용하는지 혹은 CPU 연산을 몇 번 반복하는지가 R(n)이 되기도 한다. 컴퓨터는 정해진 시간동안 처리할 수 있는 연산 개수가 일정하기 때문에, 계산하는 데 드는 시간은 그 계산 연산 개수에 비례한다. 348 | 349 | R(n) = 𝜣(f(n))에 대한 고찰 350 | 351 | 1.2.1절에서 factorial 계산할 때 되도는 프로세스는 계산 단계가 n에 비례에서 선형적으로 늘어나서 처리해야 할 단계는 𝜣(n)만큼, 계산에 필요한 기억 공간도 352 | 𝜣(n)만큼 필요하다. 반면에 반복 프로세스 경우에는 단계가 𝜣(n)만큼 늘어나지만, 기억 공간은 𝜣(1)만큼 늘지도 줄지도 않는다. 353 | 여러 갈래로 되도는 프로세스 fibo()는 계산 단계가 𝜣(𝜙n)이고, 기억 공간은 𝜣(n)이 필요하다. 354 | 355 | 자라나는 정도는 프로세스가 쓰는 자원 양을 얼추 어림잡아 나타내는 것이다. 예를 들어 n2 단계거나 1000m2이나 3n2+10n+17 단계를 거치는 것 모두를 𝜣(n2)으로 표현한다. 356 | 357 | 이제부터는 문제 크기가 두 배로 늘어도 정해진 만큼만 자원을 사용하는 로그logarithmic로 자라나는 알고리듬을 살펴본다. 358 | 359 | ##### 연습문제 1.14 360 | 361 | (패스) 362 | 363 | ##### 연습문제 1.15 364 | 365 | ```swift 366 | func cube(x: Double) -> Double { 367 | return x*x*x 368 | } 369 | 370 | func p(x: Double) -> Double { 371 | return (3*x) - (4 * cube(x: x)) 372 | } 373 | 374 | func sine(angle: Double) -> Double { 375 | if (abs(angle) <= 0.1) { 376 | return angle 377 | } 378 | else { 379 | return p(x: sine(angle: angle / 3.0)) 380 | } 381 | } 382 | ``` 383 | 384 | #### 1.2.4 거듭제곱 385 | 386 | 같은 수를 여러 번 곱하는 문제에 대한 프로시저를 고려해보자. 밑수b, 0보다 큰 정수 n을 인자로 받아서 bn을 구하는 프로시저를 만들면 된다. 이 프로시저를 구현하기 위해서 다음과 같은 재귀 규칙을 가정해본다. 387 | 388 | bn = b・bn-1 389 | b0 = 1 390 | 391 | ```swift 392 | func exponent(b: Double, n: Double) -> Double { 393 | if (n == 0) { 394 | return 1 395 | } 396 | else { 397 | return b * (exponent(b: b, n: n-1)) 398 | } 399 | } 400 | ``` 401 | 402 | 이 프로시저는 𝜣(n) 계산 단계를 거치고, 𝜣(n)만큼 기억 공간을 사용하는 선형 재귀로 동작한다. 사다리곱 문제와 마찬가지로 선형 반복 프로세스로 해결해보자. 403 | 404 | ```swift 405 | func exponent(b: Double, n: Double) -> Double { 406 | var product = 1.0 407 | for _ in stride(from: n, through: 0, by: -1) { 408 | product *= b 409 | } 410 | return product 411 | } 412 | ``` 413 | 414 | 이 프로세스는 𝜣(n) 계산 단계를 반복하지만, 선형 재귀 방식과 다르게 기억 공간은 𝜣(1) 만큼만 사용한다. 415 | 416 | 계산 단계를 줄이는 방법을 고민해보자. 거듭 제곱의 경우는 반복하거나 재귀하지 않고 직접 제곱을 계산하는 방법으로 풀어서 쓸 수도 있다. 예를 들어 b8 값을 계산하려면 `b * (b * (b * (b * (b * (b * (b * b))))))` 이렇게 풀어서 계산할 수 있다. 이제 계산 단계를 줄여보자. 417 | 418 | b2 = b * b 419 | b4 = b2 * b2 420 | b8 = b4 * b4 421 | 422 | 이렇게 세 단계로 표현할 수도 있다. 하지만 이렇게 줄이려면 짝수의 경우만 가능하다. 홀수, 짝수 모든 거듭 제곱을 계산하려면 다음과 같이 나눠야 한다. 423 | 424 | bn = (bn/2)2 `n이 짝수` 425 | bn = b * bn-1 `n이 홀수` 426 | 427 | ```swift 428 | func fast_exponent(b: Double, n:Double) -> Double { 429 | if n == 0 { 430 | return 1 431 | } 432 | else if n.remainder(dividingBy: 2) == 0 { 433 | return square(x: fast_exponent(b: b, n: n/2)) 434 | } 435 | else { 436 | return b * fast_exponent(b: b, n: n-1) 437 | } 438 | } 439 | ``` 440 | 441 | 이렇게 구현하면 fast_exponent() 프로시저가 처리하는 계산 단계와 필요한 기억 공간은 n의 로그로 자라난다. 곱셉을 할 때 마다 계산하는 지수의 크기는 얼추 두 배가 증가한다. 지수가 n일 때 곱셈을 계산한 횟수는 밑수를 2로 하는 n의 로그가 된다. 이런 자람 차수는 𝜣(log n)이다. 442 | n 값이 작을 경우는 𝜣(n)과 𝜣(log n) 차이가 별로 나지 않지만, 값이 커질수록 그 차이는 점점 벌어진다. 예를 들어 n이 1,000이면 fast_exponent()는 14번만 곱셈을 계산한다. 이런 문제들은 반복 프로세스로 구현할 수 있지만, 재귀 프로세스보다 구현하기가 더 어렵다. 443 | 444 | ###### 연습문제 1.16 445 | 446 | 힌트. (bn/2)2 = (b2)n/2 447 | 448 | > 1. 반복문을 이용하는 방식 449 | 450 | ```swift 451 | func fast_exponent_iterate(b: Double, n:Double) -> Double { 452 | var product = 1.0 453 | var base = b 454 | var counter = n 455 | while(counter != 0) { 456 | if counter.remainder(dividingBy: 2) == 0 { 457 | base = base * base 458 | counter = counter / 2 459 | } 460 | else { 461 | product *= base 462 | counter -= 1 463 | } 464 | } 465 | return product 466 | } 467 | ``` 468 | 469 | > 2. 재귀로 반복 프로세스를 처리하는 방식 470 | 471 | ```swift 472 | func fast_expt_iter(_ a: Double, _ b: Double, _ n: Double) -> Double{ 473 | if n == 0 { 474 | return a 475 | } 476 | else if n.remainder(dividingBy: 2) == 0 { 477 | return fast_expt_iter(a, b*b, n/2) 478 | } 479 | else { 480 | return fast_expt_iter(a*b, b, n-1) 481 | } 482 | } 483 | 484 | 485 | func fast_expt(b: Double, n: Double) -> Double { 486 | return fast_expt_iter(1, b, n) 487 | } 488 | ``` 489 | 490 | ###### 연습문제 1.17 491 | 492 | > 덧셈으로만 곱셈을 구현한 함수 493 | 494 | ```swift 495 | func times(a: Double, b: Double) -> Double { 496 | if b == 0 { 497 | return 0 498 | } 499 | else { 500 | return a + (a * (b-1)) 501 | } 502 | } 503 | ``` 504 | 505 | > `log n`으로 변경한 프로시저 506 | 507 | ```swift 508 | func double(x: Double) -> Double { 509 | return x + x 510 | } 511 | 512 | func halve(x: Double) -> Double { 513 | return x / 2 514 | } 515 | 516 | func fast_times(a: Double, b: Double) -> Double { 517 | if b == 1 { 518 | return a 519 | } 520 | else if a == 0 || b == 0 { 521 | return 0 522 | } 523 | else if b.remainder(dividingBy: 2) == 0 { 524 | return double(x: fast_times(a: a, b: halve(x: b))) 525 | } 526 | else { 527 | return a + fast_times(a: a, b: b-1) 528 | } 529 | } 530 | ``` 531 | 532 | ###### 연습문제 1.18 533 | 534 | > 반복 프로세스로 동작하는 `log n` 구현 535 | 536 | ```swift 537 | func fast_times_iter(_ a: Double, _ b:Double, _ n: Double) -> Double { 538 | if b == 1 { 539 | return n + a 540 | } 541 | else if a == 0 || b == 0 { 542 | return 0 543 | } 544 | else if b.remainder(dividingBy: 2) == 0 { 545 | return fast_times_iter(double(x: a), halve(x: b), n) 546 | } 547 | else { 548 | return fast_times_iter(a, b-1, n+a) 549 | } 550 | } 551 | 552 | func fast_times_i(a: Double, b:Double) -> Double { 553 | return fast_times_iter(a, b, 0) 554 | } 555 | ``` 556 | 557 | ###### 연습문제 1.19 558 | 559 | > 피보나치 수열에 대한 `log n` 방식 반복 프로세스 구현 560 | 561 | ```swift 562 | func fibo_i(n: Int) -> Int { 563 | fibo_iterate(1, 0, 0, 1, n) 564 | } 565 | 566 | func fibo_iterate(_ a: Int, _ b: Int, _ p: Int, _ q: Int, _ count: Int) -> Int { 567 | if count == 0 { 568 | return b 569 | } 570 | else if count % 2 == 0 { 571 | return fibo_iterate(a, b, p*p + q*q, 2*p*q + q*q, count/2) 572 | } 573 | else { 574 | return fibo_iterate(b*q + a*q + a*p, b*p + a*q, p, q, count-1) 575 | } 576 | } 577 | ``` 578 | 579 | #### 1.2.5 최대 공약수 580 | 581 | 정수 a와 b에 대해, 두 수를 나머지 없이 나눌 수 있는 가장 큰 정수를 최대 공약수(GCD)라고 한다. 예를 들어 16과 28의 GCD는 4. 2장에서 유리수 계산을 구현할 때도 약분하기 위해서 GCD를 사용한다. GCD는 인수 분해보다 더 효율적이라고 알려진 알고리듬으로 구현할 수 있다. 582 | 583 | `GCD(a, b) = GCD(b, r)` 584 | 585 | 이 계산법에 따르면 a와 b의 GCD 계산하는 문제는 그보다 적은 두 수 b와 r의 GCD를 구하는 작은 문제로 줄여서 해결할 수 있다는 의미다. 206과 40에 대해 GCD를 계산해보자. 586 | 587 | ``` 588 | GCD(206, 40) = GCD(40, 6) 589 | = GCD(6, 4) 590 | = GCD(4, 2) 591 | = GCD(2, 0) 592 | = 2 593 | ``` 594 | 595 | GCD(206, 40) 문제가 GCD(2, 0) 문제로 줄어들어 결과값은 2가 된다. 0보다 큰 정수를 계산하기 위해서 이런 과정을 반복해서 두 번째 수가 0이 될 때까지 계산하면 첫 번째 수가 GCD가 된다. 이 방법을 **유클리드 알고리듬** 이라고 한다. 596 | 597 | ```swift 598 | func gcd(a: Int, b: Int) -> Int { 599 | if b == 0 { 600 | return a 601 | } 602 | else { 603 | return gcd(a: b, b: (a % b)) 604 | } 605 | } 606 | ``` 607 | 608 | 이 프로시저는 반복하는 프로세스로 펼치면, 계산할 단계 수가 로그 비례로 커진다. 609 | 610 | ###### 연습문제 1.20 611 | 612 | (패스) 613 | 614 | #### 1.2.6 연습: 소수 찾기 615 | 616 | 정수 n이 소수인지 알아보는 방법을 알아보자. 617 | 618 | ##### 약수 찾기 619 | 620 | 소수인지 알아보는 방법 중에 하나는 약수를 찾아내는 것이다. 특정한 수 n의 약수 중에서 1보다 큰 가장 작은 약수를 찾는 방식이다. 621 | 622 | ```swift 623 | func square(x: Int) -> Int { return x * x } 624 | 625 | func smallest_divisor(n: Int) -> Int { 626 | return find_divisor(n, 2) 627 | } 628 | 629 | func find_divisor(_ n: Int, _ test: Int) -> Int { 630 | if (square(x: test) > n) { 631 | return n 632 | } 633 | else if (divides(test, n)) { 634 | return test 635 | } 636 | else { 637 | return find_divisor(n, (test + 1)) 638 | } 639 | } 640 | 641 | func divides(_ a: Int, _ b: Int) -> Bool { 642 | return b % a == 0 643 | } 644 | 645 | func isPrime(n: Int) -> Bool { 646 | return smallest_divisor(n: n) == n 647 | } 648 | ``` 649 | 650 | find_divisor()를 끝내는 조건은 √n이 소수가 아니라면 √n보다 작거나 같은 약수가 꼭 있다는 것을 바탕으로 동작한다. 그래서 n이 소수인지 알아보는 데 √n까지만 계산하면 되기 때문에 계산 단계는 𝜣(√n) 다. 651 | 652 | ##### 페르마 검사 653 | 654 | 페르마의 작은 정리라는 이론을 기반으로 소수를 찾을 수도 있다. 655 | 656 | `페르마의 작은 정리:` n이 소수고, a가 n보다 작고 0보다 큰 정수라면, an은 a modulo n으로 맞아떨어진다. 657 | 658 | 여기서 두 수를 n으로 나눈 나머지가 같을 때, 두 숫자는 modulo n으로 맞아떨어진다. n이 소수가 아닐 때 n보다 작은 수 a는 대부분 위에 나온 조건에 맞지 않는다. 숫자 n에 대해 a < n인 숫자 a를 골라서, an modulo n의 나머지를 계싼한다. 그 값이 a가 아니면 n은 소수가 아니고, a가 소수면 n도 소수일 가능성이 높다. 그러면 다시 다른 수 a를 골라서 같은 방법으로 계산하면 n이 소수일 가능성이 더 높아진다. 이렇게 계속 여러 a값을 고르고 계산하다보면 n이 소수일 확율을 높이는 것을 `페르마 검사` 라고 한다. 659 | 660 | 이 방식으로 계산하려면 modulo 프로시저가 필요하다. expmod() 프로시저를 정의하면 다음과 같다. expmod()를 계산해서 페르마 검사를 하는 fermat_test() 프로시저를 선언했다. 그리고 times 만큼 페르마 검사를 해서 소수인지 확인하는 프로시저를 선언한다. 661 | 662 | ```swift 663 | func expmod(base: Int, exp: Int, m: Int) -> Int { 664 | if exp == 0 { 665 | return 1 666 | } 667 | else if exp % 2 == 0 { 668 | return square(x: expmod(base: base, exp: exp / 2, m: m)) % m 669 | } 670 | else { 671 | return (base * expmod(base: base, exp: exp - 1, m: m)) % m 672 | } 673 | } 674 | 675 | func fermat_test(n: Int) -> Bool { 676 | func try_it(a: Int) -> Bool { 677 | return expmod(base: a, exp: n, m: n) == a 678 | } 679 | return try_it(a: Int.random(in: 1...n-1)) 680 | } 681 | 682 | func fast_prime(n: Int, times: Int) -> Bool { 683 | if times == 0 { 684 | return true 685 | } 686 | else if fermat_test(n: n) { 687 | return fast_prime(n: n, times: times - 1) 688 | } 689 | else { 690 | return false 691 | } 692 | } 693 | ``` 694 | 695 | ##### 확률을 바탕으로 하는 알고리듬 696 | 697 | 알고리듬으로 만들어서 계산하면 항상 틀림없는 답을 계산해야 한다. 앞서 살펴본 페르마 검사 프로시저는 그렇지가 않다. times 반복 횟수에 따라 어림잡아 계산한 답만 나올 뿐이다. 어떤 수 n이 페르마 검사를 모두 통과했다고 해도 n이 소수인지 명확하지 않다. 단지 확률이 높을 뿐이다. n에 대해 충분히 페르마 검사를 통과하면 n을 소수로 잘못 계산할 확률이 낮아질까? 698 | 699 | 페르마 검사와 반대로 n이 소수가 아닐 때 a < n인 정수 가운데 조건을 만족하는 경우가 거의 없다는 사실을 증명할 수 있다. 따라서 페르마 검사를 반복해서 잘못 계산할 확률을 낮출 수 있다. 700 | 701 | ###### 연습문제 1.21 702 | 703 | ```swift 704 | print(smallest_divisor(n: 199)) 705 | print(smallest_divisor(n: 1999)) 706 | print(smallest_divisor(n: 19999)) 707 | ``` 708 | 709 | ###### 연습문제 1.22 710 | 711 | Lisp 시스템에 있는 runtime 기본 프로시저 대신에 `CFAbsoluteTimeGetCurrent()` 함수를 활용해서 시간을 측정한다. 712 | 713 | ```swift 714 | func timed_prime_test(n: Int) { 715 | start_prime_test(n, CFAbsoluteTimeGetCurrent()) 716 | } 717 | 718 | func start_prime_test(_ n: Int, _ start_time: Double) { 719 | if isPrime(n: n) { 720 | print("\(n) ", String(format: "%.6f", CFAbsoluteTimeGetCurrent() - start_time)) 721 | } 722 | } 723 | 724 | func search_for_primes(base: Int) { 725 | for number in base.. 실행결과 737 | 738 | ``` 739 | 1009 0.000041 740 | 1013 0.000002 741 | 1019 0.000002 742 | 1021 0.000002 743 | 1031 0.000002 744 | 1033 0.000001 745 | 1039 0.000002 746 | 1049 0.000002 747 | 1051 0.000001 748 | 1061 0.000001 749 | 1063 0.000002 750 | 1069 0.000001 751 | 1087 0.000003 752 | 1091 0.000003 753 | 1093 0.000003 754 | 1097 0.000002 755 | 10007 0.000004 756 | 10009 0.000006 757 | 10037 0.000003 758 | 10039 0.000007 759 | 10061 0.000004 760 | 10067 0.000005 761 | 10069 0.000003 762 | 10079 0.000004 763 | 10091 0.000006 764 | 10093 0.000004 765 | 10099 0.000008 766 | 100003 0.000018 767 | 100019 0.000016 768 | 100043 0.000017 769 | 100049 0.000014 770 | 100057 0.000013 771 | 100069 0.000016 772 | 1000003 0.000069 773 | 1000033 0.000067 774 | 1000037 0.000054 775 | 1000039 0.000052 776 | 1000081 0.000042 777 | 1000099 0.000053 778 | ``` 779 | 780 | ###### 연습문제 1.23 781 | 782 | 다음 두 함수를 수정하고 다시 결과를 측정한다. 783 | 784 | ```swift 785 | func find_divisor(_ n: Int, _ test: Int) -> Int { 786 | if (square(x: test) > n) { 787 | return n 788 | } 789 | else if (divides(test, n)) { 790 | return test 791 | } 792 | else { 793 | return find_divisor(n, next(test)) 794 | } 795 | } 796 | 797 | func next(_ test: Int) -> Int { 798 | if test == 2 { 799 | return 3 800 | } 801 | else { 802 | return test + 2 803 | } 804 | } 805 | ``` 806 | 807 | > 실행결과 808 | 809 | ``` 810 | 1009 0.000040 811 | 1013 0.000003 812 | 1019 0.000002 813 | 1021 0.000002 814 | 1031 0.000002 815 | 1033 0.000001 816 | 1039 0.000002 817 | 1049 0.000001 818 | 1051 0.000002 819 | 1061 0.000002 820 | 1063 0.000002 821 | 1069 0.000002 822 | 1087 0.000002 823 | 1091 0.000002 824 | 1093 0.000002 825 | 1097 0.000002 826 | 10007 0.000004 827 | 10009 0.000004 828 | 10037 0.000003 829 | 10039 0.000003 830 | 10061 0.000003 831 | 10067 0.000004 832 | 10069 0.000004 833 | 10079 0.000004 834 | 10091 0.000004 835 | 10093 0.000004 836 | 10099 0.000003 837 | 100003 0.000011 838 | 100019 0.000010 839 | 100043 0.000008 840 | 100049 0.000009 841 | 100057 0.000011 842 | 100069 0.000010 843 | 1000003 0.000027 844 | 1000033 0.000029 845 | 1000037 0.000030 846 | 1000039 0.000027 847 | 1000081 0.000029 848 | 1000099 0.000030 849 | ``` 850 | 851 | 이전보다 계산량이 절반으로 줄어들었지만 2배로 빨라지지는 않았다. 알고리듬을 비교하면 빠르기 비율은 어림잡아 얼마인가? 852 | 853 | ###### 연습문제 1.24 854 | 855 | fast_prime() 프로시저를 사용해서 100번씩 검사하도록 수정하고 다시 결과를 측정한다. 856 | 857 | > 실행 결과 858 | ``` 859 | 1009 0.000262 860 | 1013 0.000203 861 | 1019 0.000200 862 | 1021 0.000252 863 | 1031 0.000215 864 | 1033 0.000229 865 | 1039 0.000210 866 | 1049 0.000230 867 | 1051 0.000232 868 | 1061 0.000239 869 | 1063 0.000230 870 | 1069 0.000233 871 | 1087 0.000232 872 | 1091 0.000215 873 | 1093 0.000214 874 | 1097 0.000210 875 | 10007 0.000290 876 | 10009 0.000252 877 | 10037 0.000283 878 | 10039 0.000282 879 | 10061 0.000267 880 | 10067 0.000320 881 | 10069 0.000386 882 | 10079 0.000355 883 | 10091 0.000292 884 | 10093 0.000298 885 | 10099 0.000293 886 | 100003 0.000321 887 | 100019 0.000359 888 | 100043 0.000281 889 | 100049 0.000325 890 | 100057 0.000337 891 | 100069 0.000313 892 | 1000003 0.000287 893 | 1000033 0.000346 894 | 1000037 0.000390 895 | 1000039 0.000413 896 | 1000081 0.000322 897 | 1000099 0.000341 898 | ``` 899 | 900 | ###### 연습문제 1.25 901 | 902 | (패스) 903 | 904 | ###### 연습문제 1.26 905 | 906 | (패스) 907 | 908 | ###### 연습문제 1.27 909 | 910 | (패스) 911 | 912 | ###### 연습문제 1.28 913 | 914 | (패스) 915 | 916 | ---- 917 | 918 | [다음 1.3 Formulating Abstractions with Higher-Order Functions](https://github.com/godrm/SICP-Swift/blob/master/1.3.md) 919 | -------------------------------------------------------------------------------- /1.3.md: -------------------------------------------------------------------------------- 1 | 2 | ### 이전 목차 3 | 4 | [1장 프로시저로 요약하는 방식](https://github.com/godrm/SICP-Swift/blob/master/Chapter1.md) 5 | 6 | [이전 1.1 The Elements of Programming](https://github.com/godrm/SICP-Swift/blob/master/1.1.md) 7 | 8 | [이전 1.2 Procedures and the Processes They Generate](https://github.com/godrm/SICP-Swift/blob/master/1.2.md) 9 | 10 | ## 1.3 차수가 높은 고차 프로시저(함수)로 요약하는 방식 Formulating Abstractions with Higher-Order procedure 11 | 12 | ```swift 13 | func cube(x: Int) -> Int { 14 | return x * x * x 15 | } 16 | 17 | func cube(x: Double) -> Double { 18 | return x * x * x 19 | } 20 | ``` 21 | 22 | 이 프로시저는 정수와 실수 모두 세제곱을 계산하도록 구현한 것이다. 워낙 간단한 계산이기 때문에 이렇게 선언한 것을 사용하지 않고, `3 * 3 * 3` 또는 `x * x * x`처럼 기본 연산으로 풀어서 쓸 수도 있다. 같은 세제곱을 계산하더라도 언어로 세제곱이라는 계산(또는 연산)을 나타내는 단어를 선언하는 게 좋다. 프로그래밍 언어는 되풀이되는 계산을 간추려서 대표하는 의미있는 이름으로 붙이는 게 중요하다. 프로시저를 선언하는 것도 그 중에 한 가지 방법이다. 23 | 24 | 단순한 계산을 하는 경우만 봐도 프로시저가 매개변수로 숫자만 받을 수 있다면, 새로운 표현으로 추상화해서 정의하는 능력은 제한될 수 밖에 없다. 이런저런 프로그램에서 여러 프로시저를 사용하는 방식이 비슷한 경우를 종종 볼 수 있다. 그래서 반복하는 계산 방식을 간추리려면, 숫자는 물론 다른 프로시저를 인자로 받거나 결과로 되돌려 주는 프로시저를 만들 수 있어야 한다. 이처럼 프로시저를 데이터처럼 사용하는 프로시저를 고차 프로시저( 또는 고차 함수) high-order function 라고 한다. 25 | 26 | ### 1.3.1 프로시저를 인자로 받는 프로시저 27 | 28 | 다음 두 함수는 각각 a부터 b까지 정수의 합을 구하는 프로시저와 정해진 넓이 속 정수를 세제곱 하는 프로시저다. 29 | 30 | ```swift 31 | func sum_intergers(a: Int, b: Int) -> Int { 32 | if a > b { 33 | return 0 34 | } 35 | else { 36 | return a + sum_intergers(a + 1, b) 37 | } 38 | } 39 | 40 | func sum_cubes(a: Int, b: Int) -> Int { 41 | if a > b { 42 | return 0 43 | } 44 | else { 45 | return a + sum_cubes(a + 1, b) 46 | } 47 | } 48 | ``` 49 | 50 | 세 번째 프로시저는 다음 수열에서 모든 마디를 더하는 프로시저다. 51 | 52 | ```swift 53 | func pi_sum(a: Double, b: Double) -> Double { 54 | if a > b { 55 | return 0 56 | } 57 | else { 58 | return 1.0 / (a * (a + 2)) + pi_sum(a + 4, b) 59 | } 60 | } 61 | ``` 62 | 63 | 여기까지 프로시저를 읽고나면 세 프로시자가 같은 계산 방법을 사용하고 있다는 것을 눈치챌 수 있다. 프로시저의 이름, 마디 값 a를 받아서 계산하는 함수, a 다음 값을 계산하는 함수만 다를 뿐이다. 따라서 다음 프로시저 틀에서 name, term, next 빈 칸만 채우면 세 가지 모든 프로시저를 표현할 수 있다. 64 | 65 | ```swift 66 | func name(a: Double, b: Double) -> Double { 67 | if a > b { 68 | return 0 69 | } 70 | else { 71 | return term(a) + name(next(a), b) 72 | } 73 | } 74 | ``` 75 | 76 | 이렇게 여러 프로시저에 같은 계산 방법을 적용할 수 있다는 것은 이를 간추릴 때 사용할 수 있는 프로시저가 나올 수 있다는 의미다. 수학자들은 아주 오래 전부터 수열 마디를 더하는 일을 같은 계산 방법으로 표현할 수 있다는 것을 알았다. 그렇게 `시그마 표현식`을 만들었다. 77 | 78 | name, term, next 빈 자리를 인자 이름오 바꿔서 표현해보면 다음과 같다. 79 | 80 | ```swift 81 | func sum(_ term: (Double) -> Double, _ a: Double, _ next: (Double) -> Double, _ b: Double) -> Double { 82 | if a > b { 83 | return 0 84 | } 85 | else { 86 | return term(a) + sum(term, next(a), next, b) 87 | } 88 | } 89 | ``` 90 | 91 | 위에 선언한 sum 프로시저는 수의 범위를 나타내는 a, b 뿐만 아니라 프로시저를 인자로 받는 term과 next가 있다는 것을 주의해야 한다. sum을 사용하는 방식은 보통 프로시저와 동일하다. 92 | 93 | 인자에 1을 더하는 프로시저 inc()와 sum_cubes를 정의하면 다음과 같다. 94 | 95 | ```swift 96 | func inc(_ n: Double) -> Double { 97 | return n + 1 98 | } 99 | 100 | func sum_cubes(_ a: Double, _ b: Double) -> Double { 101 | return sum(cube, a, inc, b) 102 | } 103 | 104 | sum_cubes(1, 10) 105 | ``` 106 | 107 | 또 다른 예제를 살펴보자. 받은 대로 그대로 돌려주는 함수로 identity가 정의되어 있다면, 프로시저 sum으로 sum_integers를 만들어보자. 108 | 109 | ```swift 110 | func identity(_ x: Double) -> Double { 111 | return x 112 | } 113 | 114 | func sum_integers(_ a: Double, _ b: Double) -> Double { 115 | return sum(identity, a, inc, b) 116 | } 117 | 118 | sum_integers(1, 10) 119 | ``` 120 | 121 | 위에서 구현했던 pi_sum() 프로시저를 다시 한 번 선언해보자. 122 | 123 | ```swift 124 | func pi_sum(_ a: Double, _ b: Double) -> Double { 125 | func pi_term(_ x: Double) -> Double { 126 | return 1.0 / (x * (x + 2)) 127 | } 128 | func pi_next(_ x: Double) -> Double { 129 | return x + 4 130 | } 131 | return sum(pi_term, a, pi_next, b) 132 | } 133 | 134 | 8 * pi_sum(1, 1000) 135 | ``` 136 | 137 | 훨씬 복잡한 아주 작은 값 dx가 있을 때 함수 f를 a부터 사이에서 적분한 값은 다음과 같다. 138 | 139 | ```swift 140 | func integral(_ f : (Double) -> Double, _ a: Double, _ b: Double, _ dx: Double) -> Double { 141 | func add_dx(_ x: Double) -> Double { 142 | return x + dx 143 | } 144 | return sum(f, a + dx / 2, add_dx, b) * dx 145 | } 146 | 147 | integral(cube, 0, 1, 0.01) 148 | integral(cube, 0, 1, 0.005) 149 | ``` 150 | 151 | ##### 연습문제 1.29 152 | 153 | ```swift 154 | func simpsons_rule_integral(f: (Double) -> Double, a: Double, b: Double, n: Double) -> Double { 155 | func helper(h: Double) -> Double { 156 | func y(_ k: Double) -> Double { 157 | return f((k*h) + a) 158 | } 159 | func term(k: Double) -> Double { 160 | if k == 0 || k == n { 161 | return y(k) 162 | } 163 | else if k.remainder(dividingBy: 2) == 0 { 164 | return 2 * y(k) 165 | } 166 | else { 167 | return 4 * y(k) 168 | } 169 | } 170 | return sum(term, 0, inc, n) 171 | } 172 | return helper(h: (b-a) / n ) 173 | } 174 | 175 | simpsons_rule_integral(f: cube, a: 0, b: 1, n: 100) 176 | simpsons_rule_integral(f: cube, a: 0, b: 1, n: 1000) 177 | ``` 178 | 179 | ##### 연습문제 1.30 180 | 181 | ```swift 182 | func sum(term: (Double) -> Double, a: Double, next: (Double) -> Double, b: Double) -> Double { 183 | func iter(a: Double, result: Double) -> Double { 184 | if a > b { 185 | return result 186 | } 187 | else { 188 | return iter(a: next(a), result: result+term(a)) 189 | } 190 | } 191 | return iter(a: a, result: 0) 192 | } 193 | ``` 194 | 195 | ##### 연습문제 1.31 196 | 197 | ```swift 198 | func product_r(term: (Double) -> Double, a: Double, next: (Double) -> Double, b: Double) -> Double { 199 | if a > b { 200 | return 1 201 | } 202 | else { 203 | return term(a) * product_r(term: term, a: next(a), next: next, b: b) 204 | } 205 | } 206 | 207 | func product_i(term: (Double) -> Double, a: Double, next: (Double) -> Double, b: Double) -> Double { 208 | func iter(_ a: Double, _ result : Double) -> Double { 209 | if a > b { 210 | return result 211 | } 212 | else { 213 | return iter(next(a), term(a) * result) 214 | } 215 | } 216 | return iter(a, 1) 217 | } 218 | 219 | product_r(term: identity, a:1, next:inc, b:5) 220 | product_i(term: identity, a:1, next:inc, b:5) 221 | ``` 222 | 223 | ##### 연습문제 1.32 224 | 225 | > 재귀 방식 226 | 227 | ```swift 228 | func accumulate_r(combiner: (Double, Double) -> Double, null_value: Double, term: (Double) -> Double, a: Double, next: (Double) -> Double, b: Double) -> Double 229 | { 230 | if a > b { 231 | return null_value 232 | } 233 | else { 234 | return combiner(term(a), accumulate_r(combiner: combiner, null_value: null_value, 235 | term: term, a: next(a), next: next, b: b)) 236 | } 237 | } 238 | ``` 239 | 240 | > 반복 방식 241 | 242 | ```swift 243 | func accumulate_i(combiner: (Double, Double) -> Double, null_value: Double, term: (Double) -> Double, a: Double, next: (Double) -> Double, b: Double) -> Double 244 | { 245 | func iter(_ a: Double, _ result: Double) -> Double { 246 | if a > b { 247 | return result 248 | } 249 | else { 250 | return iter(next(a), combiner(term(a), result)) 251 | } 252 | } 253 | return iter(a, null_value) 254 | } 255 | 256 | func sum_i(term: (Double) -> Double, a: Double, next: (Double) -> Double, b: Double) -> Double 257 | { 258 | func plus(_ x: Double, _ y: Double) -> Double { 259 | return x + y 260 | } 261 | return accumulate_i(combiner: plus, null_value: 0, term: term, a: a, next: next, b: b) 262 | } 263 | 264 | func product_r(term: (Double) -> Double, a: Double, next: (Double) -> Double, b: Double) -> Double { 265 | func times(_ x: Double, _ y: Double) -> Double { 266 | return x * y 267 | } 268 | return accumulate_i(combiner: times, null_value: 1, term: term, a: a, next: next, b: b) 269 | } 270 | ``` 271 | 272 | ##### 연습문제 1.33 273 | 274 | ```swift 275 | func filtered_accumulate(combiner: (Double, Double) -> Double, null_value: Double, term: (Double) -> Double, a: Double, next: (Double) -> Double, b: Double, filter: (Double) -> Bool) -> Double { 276 | if a > b { 277 | return null_value 278 | } 279 | else if filter(a){ 280 | return combiner(term(a), filtered_accumulate(combiner: combiner, null_value: null_value, term: term, a: next(a), next: next, b: b, filter: filter)) 281 | } 282 | else { 283 | return filtered_accumulate(combiner: combiner, null_value: null_value, term: term, a: next(a), next: next, b: b, filter: filter) 284 | } 285 | } 286 | ``` 287 | 288 | ## 1.3.2 클로저closure로 나타내는 프로시저 289 | 290 | 1.3.1절에서 sum()을 만들 때처럼 pi_term() 이나 pi_next() 같은 프로시저를 인자로 전달하기 위해서 매번 선언하는 것은 귀찮은 일이다. 그래서 항상 프로시저를 선언하는 대신 '인자에 4를 더하고 돌려주는 프로시저' 또는 '인자와 인자에 2를 더한 값을 곱해서 역수를 계산하는 프로시저'를 바로 선언하는 게 편리하다. 클로저라는 형식이 이런 문제를 푸는 데 도움이 된다. 291 | 292 | 스위프트에서 클로저는 다음과 같이 선언한다. 그렇지만 클로저를 어딘가에 담지 않는다면 이 상태 그대로 사용하지는 못한다. 293 | 294 | ```swift 295 | { x in x + 4 } 296 | { x in 1 / (x * (x + 2)) } 297 | ``` 298 | 299 | 클로저를 다음과 같이 상수와 함께 선언할 수 있다. 300 | ```swift 301 | let k = { x in x + 4 } 302 | let z = { x in 1 / (x * (x + 2)) } 303 | ``` 304 | 305 | 또는 integral() 같은 다른 함수를 선언할 때 보조 함수를 선언하지 않고 직접 선언할 수 있다. 306 | ```swift 307 | func pi_sum(a: Double, b: Double) -> Double { 308 | return sum({x in 1 / (x * (x + 2))}, 309 | a, 310 | {x in x + 4}, 311 | b 312 | ) 313 | } 314 | 315 | func integral(_ f : (Double) -> Double, _ a: Double, _ b: Double, _ dx: Double) -> Double { 316 | return sum(f, a + dx / 2, { x in x + dx }, b) * dx 317 | } 318 | ``` 319 | 320 | 이처럼 함수를 선언하는 것과 큰 차이는 없지만, 이름을 붙이지 않고 매개 변수 자리에 바로 넣을 수 있다. 321 | 322 | `func plus4(x: Double) -> Double { return x + 4 }`로 선언한 함수가 있다고 가정하자. 323 | 이 프로시저는 `let plus4 = { x in x + 4 }`처럼 클로저를 상수에 선언한 것과 같다. 324 | 325 | 클로저 문법 내용을 풀어서 설명하면 다음과 같다. 326 | 327 | ![lambda]() 328 | 329 | 함수 대신 값을 처리하는 연산자가 있는 표현식처럼, 연산자 대신 클로저를 선언할 수도 있다. 330 | 331 | `let k = {(x, y, z) in x + y + square(z)}(1,2,3)` 332 | 333 | #### let으로 갇힌 변수 만들기 334 | 335 | ```swift 336 | func f(x: Double, y: Double) -> Double { 337 | func f_helper(_ a: Double, _ b: Double) -> Double { 338 | return x * square(a) + y*b + a*b 339 | } 340 | return f_helper(1 + x*y, 1-y) 341 | } 342 | ``` 343 | 344 | ```swift 345 | func f_closure(x: Double, y: Double) -> Double { 346 | return { a, b in x * square(a) + y*b + a*b }(1 + x*y, 1-y) 347 | } 348 | ``` 349 | 350 | ```swift 351 | func f_local(x: Double, y: Double) -> Double { 352 | let a = 1 + x*y 353 | let b = 1 - y 354 | return x * square(a) + y*b + a*b 355 | } 356 | ``` 357 | 358 | ##### 연습문제 1.34 359 | 360 | ```swift 361 | func f(g: (Double) -> Double) -> Double { 362 | return g(2) 363 | } 364 | 365 | f(g: square) 366 | > 4 367 | f(g: { z in z * (z + 1)}) 368 | > 6 369 | ``` 370 | 371 | 위에 코드를 실행이 되지만, `f(g: f)` 형식은 동작하지 않는다. 스위프트는 f() 타입이 g() 타입과 다르기 때문에 컴파일 에러가 발생한다. 372 | 373 | 374 | ## 1.3.3 일반 방법을 표현하는 프로시저 375 | 376 | 1.1.4절에서는 계산하는 방법에만 집중해서 프로시저로 요약하는 방법을 다루었다. 1.3.1절에서는 만든 integral 적분 계산식처럼 차수 높은 프로시저를 만들어서 쓰임새를 늘릴 수 있었다. 377 | 378 | #### 이분법으로 방정식의 근 찾기 379 | 이분법 half-interval method는 어떤 너비를 반으로 나누는 방법으로, 연속 함수 f에서 f(x)=0을 만족하는 근을 찾는 방법이다. 계산을 반복할 때마다 값을 찾아야 할 넓이가 절반으로 줄어든다. L이 처음 넓이, T가 허용오차 error tolerance일 때 계산 단계는 𝜣(log(L / T)) 정도가 된다. 380 | 381 | ```swift 382 | func search(_ f: (Double)-> Double, _ neg_point: Double, _ pos_point: Double) -> Double { 383 | let midpoint = average(neg_point, pos_point) 384 | if close_enough(neg_point, pos_point) { 385 | return midpoint 386 | } 387 | let test_value = f(midpoint) 388 | if positive(test_value) { 389 | return search(f, neg_point, midpoint) 390 | } 391 | else if negative(test_value) { 392 | return search(f, midpoint, pos_point) 393 | } 394 | return midpoint 395 | } 396 | 397 | func close_enough(_ x: Double, _ y: Double) -> Bool { 398 | return abs(x - y) < 0.001 399 | } 400 | 401 | func average(_ x: Double, _ y: Double) -> Double { 402 | return (x + y) / 2.0 403 | } 404 | 405 | func positive(_ x: Double) -> Bool { 406 | return x > 0 407 | } 408 | 409 | func negative(_ x: Double) -> Bool { 410 | return x < 0 411 | } 412 | 413 | func half_interval_method(_ f:(Double)->Double, _ a: Double, _ b: Double) -> Double? { 414 | let a_value = f(a) 415 | let b_value = f(b) 416 | if negative(a_value) && positive(b_value) { 417 | return search(f, a, b) 418 | } 419 | else if negative(b_value) && positive(a_value) { 420 | return search(f, b, a) 421 | } 422 | print("Values are not of opposite sign \(a), \(b)") 423 | return nil 424 | } 425 | 426 | print(half_interval_method(sin, 2.0, 4.0)) 427 | print(half_interval_method({ (x:Double) in (x*x*x) - (2*x) - 3 }, 1.0, 2.0)) 428 | ``` 429 | 430 | 431 | #### 고정점 찾기 함수 432 | 433 | 특정한 수 x에 대해 f(x) = x가 참이면 x를 f의 고정점 fixed point라고 한다. 434 | 함수 f에 임시값을 주고 반복해서 f를 계산하다가 그 값이 크게 바뀌지 않으면 f의 고정점을 찾을 수 있다. 435 | 436 | ```swift 437 | let tolerance = 0.0001 438 | func fixed_point(_ f:(Double)->Double, _ first_guess:Double) -> Double { 439 | func close_enough(_ x: Double, _ y: Double) -> Bool { 440 | return abs(x - y) < tolerance 441 | } 442 | func try_with(guess: Double) -> Double { 443 | let next = f(guess) 444 | if close_enough(guess, next) { 445 | return next 446 | } 447 | else { 448 | return try_with(guess: next) 449 | } 450 | } 451 | return try_with(guess: first_guess) 452 | } 453 | 454 | fixed_point(cos, 1.0) 455 | fixed_point( { y in sin(y) + cos(y)} , 1.0) 456 | ``` 457 | 458 | 1.1.7절에서 제곱근을 찾던 방법을 고정점 찾는 방식으로 표현하면 `y = x / y` 가 되는 고정점을 찾으면 된다. 459 | 460 | ```swift 461 | func sqrt_y(x: Double) -> Double { 462 | return fixed_point({ y in x / y}, 1.0) 463 | } 464 | ``` 465 | 466 | 하지만 이렇게 구현하면 제급근에 가까워지지 않고, 고정점 근처를 왔다갔다 반복할 뿐이다. 467 | y 다음 값이 x/y 대신 `(y + x/y)/2` 가 되도록 y와 x/y 평균값을 구하면 된다. 468 | 469 | ```swift 470 | func sqrt(x: Double) -> Double { 471 | return fixed_point({ y in average(y, (x / y))}, 1.0) 472 | } 473 | ``` 474 | 475 | 476 | ##### 연습문제 1.35 477 | 478 | `fixed_point( { x in 1 + (1 / x)}, 1.0 )` 479 | 480 | ##### 연습문제 1.36 481 | 482 | > 출력하는 버전과 평균값으로 찾아가는 버전 483 | 484 | ```swift 485 | let tolerance = 0.0001 486 | func fixed_point(_ f:(Double)->Double, _ first_guess:Double) -> Double { 487 | func close_enough(_ x: Double, _ y: Double) -> Bool { 488 | return abs(x - y) < tolerance 489 | } 490 | func try_with(guess: Double) -> Double { 491 | print("guess = \(guess)") 492 | let next = f(guess) 493 | if close_enough(guess, next) { 494 | return next 495 | } 496 | else { 497 | return try_with(guess: next) 498 | } 499 | } 500 | return try_with(guess: first_guess) 501 | } 502 | 503 | func fixed_point_average(_ f:(Double)->Double, _ first_guess:Double) -> Double { 504 | func close_enough(_ x: Double, _ y: Double) -> Bool { 505 | return abs(x - y) < tolerance 506 | } 507 | func try_with(guess: Double) -> Double { 508 | print("guess = \(guess)") 509 | let next = (guess + f(guess)) / 2 510 | if close_enough(guess, next) { 511 | return next 512 | } 513 | else { 514 | return try_with(guess: next) 515 | } 516 | } 517 | return try_with(guess: first_guess) 518 | } 519 | ``` 520 | 521 | ##### 연습문제 1.37 522 | 523 | > 반복 프로시저 524 | 525 | ```swift 526 | func continue_frac_r(n: (Double)->Double, d: (Double)->Double, k: Double) -> Double { 527 | func fraction(i: Double) -> Double { 528 | if i > k { 529 | return 0 530 | } 531 | return n(i) / (d(i) + fraction(i: i+1)) 532 | } 533 | return fraction(i: 1) 534 | } 535 | 536 | continue_frac_r(n: { i in 1.0 }, d: { i in 1.0 }, k: 20) 537 | ``` 538 | 539 | > 절차 프로시저 540 | 541 | ```swift 542 | func continue_frac_i(n: (Double)->Double, d: (Double)->Double, k: Double) -> Double { 543 | func fraction(_ i: Double, _ current: Double) -> Double { 544 | if i == 0 { 545 | return current 546 | } 547 | return fraction(i - 1, n(i) / (d(i) + current)) 548 | } 549 | return fraction(k, 0) 550 | } 551 | 552 | continue_frac_i(n: { i in 1.0 }, d: { i in 1.0 }, k: 20) 553 | ``` 554 | 555 | ##### 연습문제 1.38 556 | 557 | ```swift 558 | print(2 + continue_frac_r(n: { i in 1.0 }, 559 | d: { i in ((i + 1).remainder(dividingBy: 3) < 1) ? 2 * (i+1) / 3 : 1.0}, 560 | k: 20)) 561 | ``` 562 | 563 | ##### 연습문제 1.39 564 | 565 | ```swift 566 | func tan_cf(x: Double, k: Double) -> Double { 567 | return continue_frac_i(n: { i in i == 1 ? x : -x * x }, 568 | d: { i in 2 * i - 1}, k: k) 569 | } 570 | 571 | print(tan_cf(x: 3.14 / 2.0, k: 20)) 572 | ``` 573 | 574 | ## 1.3.4 프로시저를 만드는 프로시저 Procedures as Returned Values 575 | 576 | 프로시저를 매개 변수로 받아 사용하면 언어 표현력이 좋아지는 것처럼, 프로시저를 결과 값으로 돌려줄 수 있으면 표현력을 한층 더 끌어 올릴 수 있다. 577 | 578 | 앞서 1.3.3절에서 고정값을 찾는 문제를 되짚어보자. 이 때 어림값을 평균내서 잦아드는 방법을 사용했었다. 평균 잦아들기 방식을 표현하면 다음 예제 코드와 같다. 579 | 580 | ```swift 581 | func average_damp(f: @escaping (Double)->Double) -> (Double) -> Double { 582 | return { x in average(x, f(x)) } 583 | } 584 | 585 | average_damp(f: square)(10) 586 | ``` 587 | 588 | average_damp() 프로시저는 f라는 함수 f를 인자로 전달받아서, x와 f(x)에 평균값을 구하는 클로저로 만든 프로시저를 내놓는다. 이 프로시저를 활용해서 제곱근 프로시저를 다시 작성하면 다음과 같다. 589 | 590 | ```swift 591 | func sqrt(x: Double) -> Double { 592 | fixed_point(average_damp(f: { y in x / y}), 1.0) 593 | } 594 | 595 | sqrt(x: 5) 596 | ``` 597 | 598 | 1.1.7절에서 만든 제곱근 프로시저와 비교해보면, 같은 계산 과정을 나타내지만 지금처럼 더 간추릴수록 계산 방법이 더 명확하게 드러난다는 것이다. 계산 방법 자체는 하나라도 이를 프로시저로 나타내는 방법은 여러 가지가 있고, 경험이 많은 좋은 프로그래머라면 그 중에서도 계산 방법을 명확하게 표현할 수 있는 프로시저를 작성하려고 한다. 제곱근을 응용해보기 위해서, x의 세제곱근은 함수 y → x/y2 고정점이라는 사실을 바탕으로 세제곱근 프로시저를 작성하면 다음과 같다. 599 | 600 | ```swift 601 | func cube_root(x: Double) -> Double { 602 | fixed_point(average_damp(f: { y in x / square(y)}), 1.0) 603 | } 604 | 605 | cube_root(x: 27) 606 | ``` 607 | 608 | #### 뉴튼 방법 609 | 610 | x → g(x)가 미분되는 함수라면, g(x)=0 방정신 근은 다음과 같은 f(x)의 정점과 같다. 611 | f(x) = x - g(x) / Dg(x) 612 | 613 | `미분`은 평균 잦아들기와 마찬가지로 어떤 함수를 다른 함수로 바꾸는 것이다. x → x3 에 대한 미분은 함수 x → 3x2 이다. 614 | 615 | g가 함수이고 dx가 아주 작은 값이면, x에서 g를 미분한 Dg는 다음과 같다. 616 | 617 | [미분 수식] 618 | 619 | ```swift 620 | let dx = 0.00001 621 | func deriv(g: @escaping (Double) -> Double) -> (Double) -> Double { 622 | return { x in (g(x + dx) - g(x)) / dx } 623 | } 624 | 625 | deriv(g: cube)(5) 626 | ``` 627 | 628 | deriv() 미분 프로시저를 선언하고 x → x3을 5에서 미분한 값을 계산할 수 있다. 629 | deriv를 기반으로 뉴튼 방법을 고정점 찾는 방법으로 나타내보자. 630 | 631 | ```swift 632 | func newton_transform(g: @escaping (Double)->Double) -> (Double) -> Double { 633 | return { x in x - g(x) / deriv(g: g)(x)} 634 | } 635 | 636 | func newtons_method(g: @escaping (Double)->Double, guess: Double) -> Double { 637 | return fixed_point(newton_transform(g: g), guess) 638 | } 639 | 640 | func sqrt_newton(x: Double) -> Double { 641 | return newtons_method(g: { y in square(y) - x}, guess: 1.0) 642 | } 643 | 644 | sqrt_newton(x: 9) 645 | ``` 646 | 647 | #### 요약Abstractions과 일급 프로시저 first-class procedures 648 | 649 | 고정점 찾기나 뉴튼 방법처럼 일반적인 수학 계산 방법을 `제곱근 찾기` 문제 해결을 위해서 어떻게 활용하는 지 살펴봤다. 뉴튼 방법 내부도 고정점 찾기로 해결할 수 있으니까 결국 고정점 찾기를 응용해서 제곱근을 찾을 수 있었다. 이 과정을 요약해보자. 650 | 651 | ```swift 652 | func fixed_point_of_transform(g: @escaping (Double)->Double, transform: (@escaping (Double)->Double) -> (Double)->Double, guess: Double) -> Double { 653 | fixed_point(transform(g), guess) 654 | } 655 | 656 | func sqrt_transform(x: Double) -> Double { 657 | return fixed_point_of_transform(g: { y in x / y }, transform: average_damp, guess: 1.0) 658 | } 659 | 660 | sqrt_transform(x: 25) 661 | ``` 662 | 663 | 이렇게 복잡한 계산을 요약하기 위해서 묶음 프로시저 compound procedure가 얼마나 중요한 지 알 수 있다. 이렇게 차수 높은 프로시저가 프로시저를 받거나 되돌려주는 방식으로 일반적인 계산 방법을 구현할 수 있다. 664 | 665 | 프로그래머는 프로그래밍 과정에서 작성한 절차를 놓고, 그 중에서 간추릴 게 무엇인지 찾아내고 더 높은 표현 수단으로 만들기 위해 노력해야 한다. 더 이상 요약하지 못할 때까지 모든 프로그램을 요약하라는 게 아니다. 어느 정도까지 요약해야 다른 문제를 풀 때 다시 쓸 수 있도록 일반 표현 수단이 되는지도 신경써야 한다. 차수 높은 프로시저가 프로시저를 값(데이터)로 사용하는 것이 그래서 중요하다. 666 | 667 | 프로그래밍 언어에서는 계산 과정에서 어떤 기능을 우선적으로 어떻게 다루어야 하는지 제약을 걸어둔다. 가장 제약이 적은 것을 일등급 first-class에 속한다고 부른다. 668 | 669 | 일등급이 누리는 특권은 다음과 같다. 670 | - 변수 이름을 붙여서 변수의 값이 될 수 있다. (referred to name) 671 | - 프로시저 인자로 사용할 수 있다. (passed as arguments) 672 | - 프로시저의 결과로 만들어질 수 있다. (returned as the result) 673 | - 데이터 구조 속에 집어 넣을 수 있다. (included in data structures) 674 | 675 | 스위프트도 함수(프로시저)를 일등급으로 다룰 수 있다. 676 | 677 | ##### 연습 문제 1.40 678 | 679 | ```swift 680 | func cubic(a: Double, b: Double, c: Double) -> (Double) -> Double { 681 | return { x in cube(x: x) + a * square(x) + b * x + c } 682 | } 683 | ``` 684 | 685 | ##### 연습 문제 1.41 686 | 687 | ```swift 688 | func double(f: @escaping (Double) -> Double) -> (Double) -> Double { 689 | return { x in f(f(x)) } 690 | } 691 | ``` 692 | 693 | ##### 연습 문제 1.42 694 | 695 | ```swift 696 | func compose(f: @escaping (Double) -> Double, g: @escaping (Double) -> Double) -> (Double) -> Double { 697 | return { x in f(g(x))} 698 | } 699 | ``` 700 | 701 | ##### 연습 문제 1.43 702 | 703 | ```swift 704 | func compose(f: @escaping (Double) -> Double, g: @escaping (Double) -> Double) -> (Double) -> Double { 705 | return { x in f(g(x))} 706 | } 707 | 708 | func repeated(f: @escaping (Double) -> Double, n: Double) -> (Double) -> Double { 709 | if n == 0 { 710 | return { x in x } 711 | } 712 | return compose(f: f, g: repeated(f: f, n: n-1)) 713 | } 714 | ``` 715 | 716 | ##### 연습 문제 1.44 717 | 718 | ```swift 719 | func compose(f: @escaping (@escaping (Double) -> Double) -> (Double) -> Double, g: @escaping (@escaping (Double) -> Double) -> (Double) -> Double) -> (@escaping (Double) -> Double) -> (Double) -> Double { 720 | return { x in f(g(x))} 721 | } 722 | 723 | func repeated(f: @escaping (@escaping (Double) -> Double) -> (Double) -> Double, n: Double) -> (@escaping (Double) -> Double) -> (Double) -> Double { 724 | if n == 0 { 725 | return { x in x } 726 | } 727 | return compose(f: f, g: repeated(f: f, n: n-1)) 728 | } 729 | 730 | 731 | let dx = 0.00001 732 | func smooth(f: @escaping (Double) -> Double) -> (Double) -> Double { 733 | return { x in (f(x - dx) + f(x) + f(x + dx)) / 3 } 734 | } 735 | 736 | func n_fold_smooth(f: @escaping (Double) -> Double, n: Double) -> (Double) -> Double { 737 | return repeated(f: smooth, n: n)(f) 738 | } 739 | ``` 740 | 741 | ##### 연습 문제 1.45 742 | 743 | ```swift 744 | func fast_expt_iter(_ a: Double, _ b: Double, _ n: Double) -> Double{ 745 | if n == 0 { 746 | return a 747 | } 748 | else if n.remainder(dividingBy: 2) == 0 { 749 | return fast_expt_iter(a, b*b, n/2) 750 | } 751 | else { 752 | return fast_expt_iter(a*b, b, n-1) 753 | } 754 | } 755 | 756 | func fast_expt(b: Double, n: Double) -> Double { 757 | return fast_expt_iter(1, b, n) 758 | } 759 | 760 | func nth_root(n: Double, x: Double) -> Double { 761 | return fixed_point(repeated(f: average_damp, 762 | n: floor(log(n)))({ y in x / fast_expt(b: y, n: n-1)}), 763 | 1.0) 764 | } 765 | 766 | nth_root(n: 3, x: 2) 767 | ``` 768 | 769 | ##### 연습 문제 1.46 770 | 771 | ```swift 772 | func good_enough(_ guess: Double, _ x: Double) -> Bool { 773 | return abs(square(guess) - x) < 0.0001 774 | } 775 | 776 | func improve(_ guess: Double, _ x: Double) -> Double { 777 | return average(guess, x / guess) 778 | } 779 | 780 | func iterative_improve(good_enough: @escaping (Double) -> Bool, 781 | improve: @escaping (Double) -> Double) -> (Double) -> Double { 782 | func iterate(guess: Double) -> Double { 783 | if good_enough(guess) { 784 | return guess 785 | } 786 | return iterate(guess: improve(guess)) 787 | } 788 | return iterate 789 | } 790 | 791 | func sqrt_improve(x: Double) -> (Double) -> Double { 792 | return iterative_improve(good_enough: { y in good_enough(y, x)}, improve: { y in improve(y, x)}) 793 | } 794 | 795 | sqrt_improve(x: 49)(1.0) 796 | ``` 797 | 798 | 799 | -------------------------------------------------------------------------------- /2.1.md: -------------------------------------------------------------------------------- 1 | ### 이전 목차 2 | 3 | [2장 데이터로 요약하는 방식](https://github.com/godrm/SICP-Swift/blob/master/Chapter2.md) 4 | 5 | ## 2.1 데이터 내용 감추고 요약하기 Introduction to Data Abstraction 6 | 7 | 1.1.8절에서 프로시저가 상위 프로시저 일부라면, 상위 프로시저가 일하는 절차를 요약해서 하위 프로시저 속에 감추는 것으로 볼 수 있다는 것을 알았다. 다른 의미로 보면, 프로시저 내용을 모르더라도 같은 동작을 하는 다른 프로시저가 있다면 부품처럼 맞바꿔 사용할 수 있다는 의미다. 요약한 프로시저를 사용하기 때문에, 프로시저에 절차를 표현해서 정의하는 하위 부분과 사용하는 상위 부분을 나누어 생각할 수 있다. 그 경계를 `요약의 경계`로 구분할 수 있다. 데이터에서 이와 같은 개념이 `데이터 요약`이다. 데이터도 마찬가지로 묶음 데이터를 만드는 부분과 사용하는 부분으로 구분할 수 있다. 8 | 9 | 데이터 요약 관점에서 프로그램을 작성할 때 복잡한 데이터는 `요약한 데이터`로 연산할 수 있어야 한다. 데이터에 조건을 달지 않아야 하고, 데이터를 사용하는 프로그램에도 드러내지 않아야 한다. 데이터를 만드는 부분과 사용하는 부분을 이어주는 인터페이스를 선택자selector와 구성자constructor 라고 부른다. 유리수 프로시저를 설계하면서 설명해보자. 10 | 11 | ### 2.1.1 연습 : 유리수를 위한 산술 연산 12 | 13 | 분자와 분모로 유리수를 짜맞추는 연산 constructor와 유리수에서 분모와 분자를 골라내는 연산 selector를 다음 프로시저로 정의하자. 14 | 15 | `make_rational(n, d)` 분자가 `n`이고 분모가 `d`인 유리수를 내놓는다. 16 | `number(x)` 유리수 `x`의 분자를 내놓는다. 17 | `denominator(x)` 유리수 `x`의 분모를 내놓는다. 18 | 19 | 여기 세 프로시저는 어떻게 정의했는지 몰라도 이 프로시저만 있으면, 아래와 같은 관계로 유리수 연산들을 만들 수 있다. 20 | 21 | ```swift 22 | func add_rational(x: Rational, y: Rational) -> Rational { 23 | return make_rational(n: number(r: x) * denominator(r: y) + number(r: y) * denominator(r: x), 24 | d: denominator(r: x) * denominator(r: y)) 25 | } 26 | 27 | func sub_rational(x: Rational, y: Rational) -> Rational { 28 | return make_rational(n: number(r: x) * denominator(r: y) - number(r: y) * denominator(r: x), 29 | d: denominator(r: x) * denominator(r: y)) 30 | } 31 | 32 | func mul_rational(x: Rational, y: Rational) -> Rational { 33 | return make_rational(n: number(r: x) * number(r: y) , 34 | d: denominator(r: x) * denominator(r: y)) 35 | } 36 | 37 | func div_rational(x: Rational, y: Rational) -> Rational { 38 | return make_rational(n: number(r: x) * denominator(r: y), 39 | d: denominator(r: x) * number(r: y)) 40 | } 41 | 42 | func equal_rational(x: Rational, y: Rational) -> Bool { 43 | return number(r: x) * denominator(r: y) == number(r: y) * denominator(r: x) 44 | } 45 | ``` 46 | 47 | #### 쌍Pair과 튜플Tuple 48 | 49 | 스킴Scheme에서는 데이터를 짝을 맞춰서 표현하기 위해서 쌍Pair이라는 데이터 구조를 사용한다. 스위프트에서 Pair 대신에 Tuple을 사용한다. 튜플에서 내부 값을 골라낼 때는 `.0`, `.1` 등을 사용한다. 50 | 51 | ```swift 52 | let x = (1, 2) 53 | 54 | x.0 55 | > 1 56 | 57 | x.1 58 | > 2 59 | ``` 60 | 61 | 튜플에는 이름을 붙일 수도 있고, 연산하기 위해 전달할 수도 있다. 다른 튜플을 새로운 튜플로 묶을 수도 있다. 62 | 63 | ```swift 64 | let x = (1, 2) 65 | let y = (3, 4) 66 | let z = (x, y) 67 | 68 | z.0.0 69 | > 3 70 | z.1.0 71 | > 1 72 | ``` 73 | 74 | 튜플은 엄밀히 말해서 쌍이 아니라 여러 값을 묶을 수도 있다. 75 | 76 | ```swift 77 | let t = (1, 2, 3) 78 | let q = (9, 10, 11, 12) 79 | ``` 80 | 81 | 복잡한 데이터 구조를 만들 때 튜플을 확장하면 리스트 구조를 갖춘 데이터를 만들 수 있다. 82 | 83 | ### 유리수 만들기 84 | 85 | 튜플을 활용해서 위에서 만든 유리수 시스템을 완성해보자. 86 | 87 | ```swift 88 | typealias Rational = (Int, Int) 89 | 90 | func make_rational(n: Int, d: Int) -> Rational { 91 | return Rational(number: n, denominator: d) 92 | } 93 | 94 | func number(r: Rational) -> Int { 95 | return r.0 96 | } 97 | 98 | func denominator(r: Rational) -> Int { 99 | return r.1 100 | } 101 | ``` 102 | 103 | 이렇게 Rational 이란 튜플을 새로운 타입 이름으로 별명typealias를 지정할 수 있다. 104 | 스위프트 튜플에서는 `r.0` 또는 `r.1` 순서 대신에 이름을 붙일 수도 있다. 105 | 106 | ```swift 107 | typealias Rational = (number:Int, denominator:Int) 108 | 109 | func make_rational(n: Int, d: Int) -> Rational { 110 | return Rational(number: n, denominator: d) 111 | } 112 | 113 | func number(r: Rational) -> Int { 114 | return r.number 115 | } 116 | 117 | func denominator(r: Rational) -> Int { 118 | return r.denominator 119 | } 120 | ``` 121 | 122 | 이제 계산 결과를 나타낼 때 `분자 / 분모` 차례로 유리수를 찍을 수도 있다. 123 | 124 | ```swift 125 | func print_rational(x: Rational) { 126 | print("\(number(r:x)) / \(denominator(r:x))") 127 | } 128 | ``` 129 | 130 | 모든 시스템 프로시저를 만들었으니까, 제대로 돌아가는지 살펴보자. 131 | 132 | ```swift 133 | let one_half = make_rational(n: 1, d: 2) 134 | print_rational(x: one_half) 135 | 136 | let one_third = make_rational(n: 1, d: 3) 137 | print_rational(x: add_rational(x: one_half, y: one_third)) 138 | 139 | print_rational(x: mul_rational(x: one_half, y: one_third)) 140 | 141 | print_rational(x: add_rational(x: one_third, y: one_third)) 142 | ``` 143 | 144 | 마지막 계산 결과는 `6/9`가 출력되고 기약분수가 아니다. 이 문제를 해결하려면 make_rational()을 고쳐야 한다. 1.2.5절에서 만든 gcd 프로시저를 써서, 분자와 분모를 최대공약수로 나누어서 기약분수로 만든다. 145 | 146 | ```swift 147 | func make_rational(n: Int, d: Int) -> Rational { 148 | let g = gcd(a: n, b: d) 149 | return Rational(number: n / g, denominator: d / g) 150 | } 151 | ``` 152 | 153 | 이제 `print_rational(x: add_rational(x: one_third, y: one_third))`를 실행하면 `2/3` 값이 출력된다. 다른 연산 프로시저는 고치지 않았고 단지 make_rational()만 고쳤다. 154 | 155 | #### 연습문제 2.1 156 | 157 | ```swift 158 | func sign(_ x: Int) -> Int { 159 | if x < 0 { 160 | return -1 161 | } 162 | else if x > 0 { 163 | return 1 164 | } 165 | return 0 166 | } 167 | 168 | func make_rational(n: Int, d: Int) -> Rational { 169 | let g = gcd(a: n, b: d) 170 | return Rational(number: sign(n) * sign(d) * abs(n / g), denominator: abs(d / g)) 171 | } 172 | ``` 173 | 174 | ## 2.1.2 요약의 경계 abstraction barrier 175 | 176 | 유리수 예제에서 부족한 부분을 살펴보자. make_rational()으로 데이터를 만들고, number()와 denominator()로 데이터를 골라내서 유리수 연산을 정의했다. 데이터를 요약하기 위해 바탕이 되는 생각은, 데이터 물체가 어떤 타입인지 결정할 연산이 무엇인가 고려해 보는 것이다. 이러한 기본 연산은 그 타입에 속하는 모든 물체를 표현할 수 있고, 딱 그 연산만으로 데이터를 계산할 수 있다. 177 | 178 | 유리수 시스템은 그림2.1처럼 생각할 수 있다. 그림에서 가로 선은 요약의 경계 abstraction barrier라고 한다. 이렇게 시스템을 구성하는 부품들은 저마다 다른 수준level에 위치한다. 각자 수준마다 데이터를 사용하는 상위 프로그램과 데이터를 구현하는 하위 프로그램이 생긴다. 경계에 있는 프로시저들은 요약의 경계를 연결해주는 인터페이스다. 179 | 180 | ![그림2.1](https://github.com/godrm/SICP-Swift/blob/master/images/2.1.png?raw=true) 181 | 182 | 데이터를 사용하는 프로그램에서는 데이터를 표현하는 방식이 바뀌면 프로그램도 함께 고쳐줘야 한다. 이렇게 경계를 나눠놓으면 프로그램을 관리하고 고치기가 쉬워진다. 데이터 표현에 영향을 받는 부분을 프로그램을 구성하는 부품의 일부로 제한하도록 설계해야 한다. 그렇지 않으면 대규모 프로그램을 작성할 때 크나큰 대가를 치른다. 183 | 184 | 앞에서 만든 유리수를 생성할 때부터 분자와 분모를 약분하지 않고, 만들어 놓은 유리수에서 분자와 분모를 꺼낼 때만 약분하도록 작성할 수도 있다. 185 | 186 | ```swift 187 | func make_rational(n: Int, d: Int) -> Rational { 188 | return Rational(number: n, denominator: d) 189 | } 190 | 191 | func number(r: Rational) -> Int { 192 | let g = gcd(a: r.0, b: r.1) 193 | return r.0 / g 194 | } 195 | 196 | func denominator(r: Rational) -> Int { 197 | let g = gcd(a: r.0, b: r.1) 198 | return r.1 / g 199 | } 200 | ``` 201 | 202 | 유리수 분자와 분모를 가져오는 경우가 많은 프로그램이라면 이전처럼 처음 유리수를 생성할 때 gcd를 계산하는 것이 좋다. 그렇지 않다면 지금처럼 값을 가져갈 때 gcd 계산을 해도 된다. 여기서 주목할 것은 이렇게 데이터를 어떤 방식으로 가져오더라도, add_rational() 이나 sub_rational()은 바꿀 필요가 없다. 203 | 204 | #### 연습문제 2.2 205 | 206 | ```swift 207 | typealias Point = (x:Double, y:Double) 208 | typealias Segment = (start:Point, end:Point) 209 | 210 | func x_point(p: Point) -> Double { 211 | return p.x 212 | } 213 | 214 | func y_point(p: Point) -> Double { 215 | return p.y 216 | } 217 | 218 | func make_point(x: Double, y: Double) -> Point { 219 | return Point(x: x, y: y) 220 | } 221 | 222 | func make_segment(start: Point, end: Point) -> Segment { 223 | return Segment(start: start, end: end) 224 | } 225 | 226 | func start_segment(s: Segment) -> Point { 227 | return s.start 228 | } 229 | 230 | func end_segment(s: Segment) -> Point { 231 | return s.end 232 | } 233 | 234 | func average(a: Double, b: Double) -> Double { 235 | return (a + b) / 2.0 236 | } 237 | 238 | func mid_point(x: Segment) -> Point { 239 | let a = start_segment(s: x) 240 | let b = end_segment(s: x) 241 | return make_point(x: average(a: x_point(p: a), b: x_point(p: b)), 242 | y: average(a: y_point(p: a), b: y_point(p: b))) 243 | } 244 | ``` 245 | 246 | #### 연습문제 2.3 247 | 248 | > 첫번째 구현 249 | 250 | ```swift 251 | typealias Rect = (bottomLeft: Point, topRight: Point) 252 | 253 | func make_rect(bottomLeft: Point, topRight: Point) -> Rect { 254 | return Rect(bottomLeft: bottomLeft, topRight: topRight) 255 | } 256 | 257 | func top_right(rect: Rect) -> Point { 258 | return rect.topRight 259 | } 260 | 261 | func bottom_right(rect: Rect) -> Point { 262 | return make_point(x: x_point(p: rect.topRight), 263 | y: y_point(p: rect.bottomLeft)) 264 | } 265 | 266 | func top_left(rect: Rect) -> Point { 267 | return make_point(x: x_point(p: rect.bottomLeft), 268 | y: y_point(p: rect.topRight)) 269 | } 270 | 271 | func bottom_left(rect: Rect) -> Point { 272 | return rect.bottomLeft 273 | } 274 | 275 | func width_rect(_ rect: Rect) -> Double { 276 | return abs(x_point(p: bottom_left(rect: rect)) - x_point(p: bottom_right(rect: rect))) 277 | } 278 | 279 | func height_rect(_ rect: Rect) -> Double { 280 | return abs(y_point(p: bottom_left(rect: rect)) - y_point(p: top_left(rect: rect))) 281 | } 282 | 283 | func area_rect(_ rect: Rect) -> Double { 284 | return width_rect(rect) * height_rect(rect) 285 | } 286 | 287 | func preimeter_rect(_ rect: Rect) -> Double { 288 | return 2 * (width_rect(rect) + height_rect(rect)) 289 | } 290 | ``` 291 | 292 | > 두번째 구현 293 | 294 | ```swift 295 | typealias Rect = (bottomLeft: Point, size: (width: Double, height: Double)) 296 | 297 | func make_rect(bottomLeft: Point, width: Double, height: Double) -> Rect { 298 | return Rect(bottomLeft: bottomLeft, size: (width, height) ) 299 | } 300 | 301 | func height_rect(_ rect: Rect) -> Double { 302 | return rect.size.height 303 | } 304 | 305 | func width_rect(_ rect: Rect) -> Double { 306 | return rect.size.width 307 | } 308 | 309 | func area_rect(_ rect: Rect) -> Double { 310 | return width_rect(rect) * height_rect(rect) 311 | } 312 | 313 | func preimeter_rect(_ rect: Rect) -> Double { 314 | return 2 * (width_rect(rect) + height_rect(rect)) 315 | } 316 | ``` 317 | 318 | ## 2.1.3 데이터란 무엇인가? What is meant by Data? 319 | 320 | 도대체 `데이터data`란 무엇을 의미하는가? 단지 연산으로 짜맞추거나 골라내는 것으로 설명하기에 부족하다. 321 | 유리수라는 데이터를 만드는 데 바탕이 되는 연산 make_rational, number, denomiator가 만족해야 할 조건은 `(number x) / (denominator x) = n / d` 뿐이다. 데이터는 이런 프로시저가 알맞은 데이터 표현을 만들어 내는지 따질 수 있는 조건까지 함께 정의해놓은 것이다. 322 | 323 | C.A.R. Hoare가 제안한 요약한 모형(abstract model)법은 이미 정의된 데이터 타입을 빌려서 새로운 타입을 정의한다. 또 다른 방법으로 대수 명세법(algebraic specification)이 있는데 프로시저를 요약한 대수 체계 원소로 보고, 그 성질을 공리로 밝힌다. Liskov and Zilles 1975 논문을 참고하자. 324 | 325 | 앞에서 설명한 유리수 시스템을 돌아보자. 두 물체를 쌍으로 묶어서 튜플로 만들수 있다면, 튜플에서 다시 골라낼 수 있다는 점이다. 이 조건을 만족하도록 데이터 표현을 쓰지 않고 단지 cons, car, cdr 프로시저만 써서 정의할 수도 있다. 326 | 327 | ```swift 328 | enum RationalError : Error { 329 | case InvalidParameter 330 | } 331 | 332 | func cons(x: Int, y: Int) -> (Int) throws -> Int { 333 | return { m in 334 | if m == 0 { return x } 335 | else if m == 1 { return y } 336 | throw RationalError.InvalidParameter 337 | } 338 | } 339 | 340 | func car(z: (Int) throws -> Int) throws -> Int { 341 | return try z(0) 342 | } 343 | 344 | func cdr(z: (Int) throws -> Int) throws -> Int { 345 | return try z(1) 346 | } 347 | ``` 348 | 349 | 이렇게 데이터를 프로시저로 나타내면 그대로 데이터라고 생각하기는 어렵다. 그럼에도 이 프로시저를 위에서 밝힌 데이터 조건을 만족하는지 증명하면 아무런 문제가 되지 않는다. 350 | cons()가 내놓는 값이 클로저라는 것을 눈여겨보자. 내부에서 선언한 클로저를 리턴하는 데, 인자 하나를 받아서 그 값이 0이면 x를 1이면 y를 내놓는다. 데이터를 사용하도록 구현하는 것과 cons, car, cdr 같은 인터페이스만 구현해도 구분하지 못한다. 351 | 하지만 이렇게 데이터 구조를 사용하지 않는 프로시저로 표현한 것은 실제로 사용한다는 것은 아니다. 이런 방식으로 구현해도 튜플 대신 쌍을 구현할 수 있다는 것이다. 프로시저를 물체처럼 다룰 수 있으면, 합친 데이터를 나타내는 표현도 가능해진다. 데이터를 프로시저로 표현하는 기법을 메시지 패싱message passing이라고 한다. 352 | 353 | ##### 연습문제 2.4 354 | 355 | pass 356 | 357 | ##### 연습문제 2.5 358 | 359 | pass 360 | 361 | ##### 연습문제 2.6 362 | 363 | 프로시저로 쌍 데이터 구조를 표현할 수 있을 뿐만 아니라, 양의 정수를 표현할 수도 있다. 364 | 0과 1과 같은 동작도 프로시저와 연산만으로 표현할 수 있다. 365 | 366 | 이런 표현을 람다 계산법을 만들어 낸 알론조 처치의 이름을 따서 처치의 수 `Church numeral`라 부른다. 367 | 368 | ```swift 369 | typealias fx = (Int) -> Int 370 | typealias fn = (@escaping fx) -> fx 371 | 372 | let zero = { (f : fx) in { (x : Int) in x }} 373 | let one = { (f : @escaping fx) in { x in f(x) }} 374 | let two = { (f : @escaping fx) in { x in f(f(x)) }} 375 | 376 | func add(_ n : @escaping fn) -> fn { 377 | return { (f : @escaping fx) in { x in n(f)(x) } } 378 | } 379 | 380 | func plus(_ n : @escaping fn, _ m : @escaping fn) -> fn { 381 | return { (f : @escaping fx) in { x in n(f)(m(f)(x)) } } 382 | } 383 | 384 | let three = plus(one, two) 385 | 386 | func church_to_number(_ c : @escaping fn) -> Int { 387 | return c({ n in n + 1})(0) 388 | } 389 | 390 | church_to_number(three) 391 | ``` 392 | 393 | ## 2.1.4 집중과제 : 구간 산술 연산 만들기 394 | 395 | ![](https://github.com/godrm/SICP-Swift/blob/master/images/equation2.1.4.png?raw=true) 396 | 397 | 저장 값은 레지스터를 만드는 제조사가 공개한 허용 오차를 따른다. `6.8Ω 허용 오차 10%` 레지스터를 구매했다면 실제 저항 값은 6.8 - 0.68 = 6.12에서 6.8 + 0.68 = 7.48 사이가 된다. 이런 방식으로 6.8Ω 10% 저항과 4.7Ω 5% 저항을 병렬로 연결하면 2.58Ω에서 2.97Ω 사이가 된다. 398 | 부정확한 양을 나타낼 수 있는 구간inverval 값을 계산할 수 있는 시스템을 설계해보자. 구간 값은 결과 값의 범위를 나타낸다. 구간 값을 더하고, 빼고, 곱하고, 나눈 결과는 다시 구간 값이 된다. 399 | 400 | 우선 `구간`을 표현하며 요약하는 물체가 있다고 가정하자. 구간 값은 하한과 상한 값을 두 끝점으로 나타낸다. 그리고 구간을 이루는 양 끝점을 받아서 구간 데이터를 만드는 make_interval가 이미 있다고 가정하자. 401 | 이런 가정하에 구간 값을 더하는 프로시저를 만들어보자. 이 프로시저는 두 구간 값에서 하한끼리 더하고, 상한끼리 더한 값을 새로운 하한과 상한으로 하는 구간을 만들면 된다. 402 | 403 | ```swift 404 | func add_interval(x: Interval, y: Interval) -> Interval { 405 | return make_interval(lower: x.lower + y.lower , upper: x.upper + y.upper) 406 | } 407 | ``` 408 | 409 | 구간 값을 곱하는 프로시저도 만들어 보자. 이 프로시저는 다음과 같이 두 구간의 상한과 하한을 조합해서 곱하는 모든 경우를 계산해서 가장 큰 값을 상한으로, 가장 작은 값을 하한으로 내놓아야 한다. 410 | 411 | ```swift 412 | func mul_interval(x: Interval, y: Interval) -> Interval { 413 | let p1 = x.lower * y.lower 414 | let p2 = x.lower * y.upper 415 | let p3 = x.upper * y.lower 416 | let p4 = x.upper * y.upper 417 | return make_interval(lower: min(p1, p2, p3, p4), upper: max(p1, p2, p3, p4)) 418 | } 419 | ``` 420 | 421 | 구간 값을 나누는 프로시저를 만들어보자. 두번 째 인자로 받은 구간에 대한 역수를 구해서 첫 번째 인자로 받은 구간과 곱해야 한다. 422 | 423 | ```swift 424 | func div_interval(x: Interval, y: Interval) -> Interval { 425 | return mul_interval(x: x, y: make_interval(lower: 1.0 / y.upper, upper: 1.0 / y.lower)) 426 | } 427 | ``` 428 | 429 | 430 | #### 연습문제 2.7 431 | 432 | `typealias Interval = (lower:Double, upper:Double)`로 정의한 Interval 튜플에서 upper와 lower를 구분하는 함수를 만들면 다음과 같다. 433 | 434 | ```swift 435 | func upper(_ x: Interval) -> Double { 436 | return x.upper 437 | } 438 | 439 | func lower(_ x: Interval) -> Double { 440 | return x.lower 441 | } 442 | ``` 443 | 444 | #### 연습문제 2.8 445 | 446 | ```swift 447 | func sub_interval(x: Interval, y: Interval) -> Interval { 448 | return make_interval(lower: lower(x) - lower(y) , upper: upper(x) - upper(y)) 449 | } 450 | ``` 451 | 452 | #### 연습문제 2.9 453 | 454 | 구간 값의 폭width는 구간의 상한에서 하한을 빼고 반으로 나눈 값을 말한다. 폭은 구간 값이 정확하지 않은 정도를 나타낸다. 구간 i에 대해 폭 W(i), 하한값 L(i), 상한값 U(i)이라고 가정하면 다음과 같다. 455 | 456 | ![example2.9](https://github.com/godrm/SICP-Swift/blob/master/images/example2.9.png?raw=true) 457 | 458 | #### 연습문제 2.10 459 | 460 | 구간에 0이 들어있는 경우 문제가 될 수 있으니까 개선해보자. 0이 포함된 경우는 Interval로 계산할 수가 없기 때문에 아무런 값이 없다는 의미로 `nil`을 리턴한다. 461 | 462 | ```swift 463 | func div_interval(x: Interval, y: Interval) -> Interval? { 464 | if lower(y) <= 0 && upper(y) >= 0 { 465 | return nil 466 | } 467 | return mul_interval(x: x, y: make_interval(lower: 1.0 / y.upper, upper: 1.0 / y.lower)) 468 | } 469 | ``` 470 | 471 | #### 연습문제 2.11 472 | 473 | 구간 양 끝점의 부호가 어떻게 되는지 검사하면, mul_interval 프로시저에서 계산하는 방법이 아홉 가지로 나눌 수 있다. 그 중에서 두 번 곱셈하는 경우는 한 번 뿐이 없다. 이렇게 mul_interval을 개선하면 다음과 같다. 474 | 475 | ```swift 476 | func positive(n: Double) -> Bool { 477 | return n >= 0 478 | } 479 | 480 | func trouble_interval(xl: Double, xu: Double, yl: Double, yu: Double) -> Interval { 481 | let p1 = xl * yl 482 | let p2 = xl * yu 483 | let p3 = xu * yl 484 | let p4 = xu * yu 485 | return make_interval(lower: min(p1, p2, p3, p4), upper: max(p1, p2, p3, p4)) 486 | } 487 | 488 | func mul_interval(x: Interval, y: Interval) -> Interval? { 489 | switch (positive(x.lower), positive(x.upper), positive(y.lower), positive(y.upper)) { 490 | case (true, true, true, true): 491 | return make_interval(lower: x.lower * y.lower, upper: x.upper * y.upper) 492 | case (true, true, false, true): 493 | return make_interval(lower: x.upper * y.lower, upper: x.upper * y.upper) 494 | case (true, true, false, false): 495 | return make_interval(lower: x.upper * y.lower, upper: x.lower * y.upper) 496 | case (false, true, true, true): 497 | return make_interval(lower: x.lower * y.upper, upper: x.upper * y.upper) 498 | case (false, true, false, false): 499 | return make_interval(lower: x.upper * y.lower, upper: x.lower * y.upper) 500 | case (false, false, true, true): 501 | return make_interval(lower: x.lower * y.upper, upper: x.upper * y.lower) 502 | case (false, false, false, true): 503 | return make_interval(lower: x.lower * y.upper, upper: x.lower * y.lower) 504 | case (false, false, false, false): 505 | return make_interval(lower: x.upper * y.upper, upper: x.lower * y.lower) 506 | case (false, true, false, true): 507 | return trouble_interval(xl: x.lower, xu: x.upper, yl: y.lower, yu: y.upper) 508 | default: 509 | return nil 510 | } 511 | } 512 | ``` 513 | 514 | ---- 515 | 516 | 이렇게 해서 프로시저를 고친 다음 이 프로그램이 필요한 사람에게 보여줬더니, 구간 가운데 값과 허용 오차로 나타낸 수를 다루는 프로그램이 필요하다고 한다. 구간을 [3.35, 3.65]로 나타내는 게 아니라 3.5 ± 0.15 형태로 표시해야 한다. 그래서 다음과 같이 center와 width를 다루는 새로운 프로시저를 추가했다. 517 | 518 | ```swift 519 | func make_interval(center: Double, width: Double) -> Interval { 520 | return make_interval(lower: center - width, upper: center + width) 521 | } 522 | 523 | func center(_ i: Interval) -> Double { 524 | return (i.lower + i.upper) / 2 525 | } 526 | 527 | func width(_ i: Interval) -> Double { 528 | return (i.upper - i.lower) / 2 529 | } 530 | ``` 531 | 532 | #### 연습문제 2.12 533 | 534 | 구간 가운데 값과 허용 오차를 인자로 받아서 구간 값을 만드는 `make(center: Double, percent: Double)` 프로시저와 percent 프로시저를 구현한다. 535 | 536 | ```swift 537 | func make(center: Double, percent: Double) -> Interval { 538 | let width = center * (percent / 100) 539 | return make_interval(center: center, width: width) 540 | } 541 | 542 | func percent(_ i: Interval) -> Double { 543 | return (width(i) / center(i)) * 100 544 | } 545 | ``` 546 | 547 | #### 연습문제 2.13 548 | 549 | 증명은 패스 550 | 551 | ---- 552 | 553 | 병렬 저항을 구할 때 대수적으로 같은 결과를 나타내는 공식이 두 개가 있어서 각각 프로그램으로 구현했다. 554 | 555 | ```swift 556 | func par1(r1: Interval, r2: Interval) -> Interval? { 557 | return div_interval(x: mul_interval(x: r1, y: r2), y: add_interval(x: r1, y: r2)) 558 | } 559 | 560 | func par2(r1: Interval, r2: Interval) -> Interval? { 561 | let one = make_interval(lower: 1, upper: 1) 562 | return div_interval(x: one, 563 | y: add_interval(x: div_interval(x: one, y: r1)!, 564 | y: div_interval(x: one, y: r2)!)) 565 | } 566 | ``` 567 | 568 | 그런데 이렇게 구현하고 보니, 계산 결과값이 같지 않았다. 569 | 570 | #### 연습문제 2.14 571 | 572 | 두 계산 방식의 결과값이 다른 문제를 해결하려면 어떻게 해야하나? 573 | 구간 값 A, B가 있고 A/A 와 A/B를 구할 때 동작하는 방식을 확인해봐야 한다. 574 | 특히 A/A를 계산한 구간 값은 길이는 0이고 정확하게 1이어야 한다. 575 | 그렇지 않으면 여러 번 계산이 반복될 수록 오차가 누적되서 최종 결과값이 달라진다. 576 | 577 | #### 연습문제 2.15 578 | 579 | 구간을 계산할 때 부정확한 수를 담은 변수를 되풀이해서 사용하지 않는 공식이 오차 범위를 더 줄인다고 한다. 위에서 병렬 저항을 계산하는 par2가 par1보다 나은 프로그램이라고 할 수 있나? 580 | 이런 경우는 `의존성 문제` 라고 부르는 데, par2가 매개 변수 R1과 R2를 더 적게 사용한다. 581 | 582 | #### 연습문제 2.16 583 | 584 | 구간 산술 연산에서 `의존성 문제`를 해결하기 위해서는 선형 다항식 형태로 테일러 급수 방식으로 계산할 수 있다. 하지만 쉽지 않은 문제다. 585 | 586 | ---- 587 | 588 | [다음 2.2 계층 구조 데이터와 닫힘 성질 Hierarchical Data and the Closure Property](https://github.com/godrm/SICP-Swift/blob/master/2.2.md) 589 | 590 | -------------------------------------------------------------------------------- /2.2.md: -------------------------------------------------------------------------------- 1 | ### 이전 목차 2 | 3 | [2장 데이터로 요약하는 방식](https://github.com/godrm/SICP-Swift/blob/master/Chapter2.md) 4 | 5 | [이전 2.1 데이터 내용 감추고 요약하기 Introduction to Data Abstraction](https://github.com/godrm/SICP-Swift/blob/master/2.1.md) 6 | 7 | ## 2.2 계층 구조 데이터와 닫힘 성질 Hierarchical Data and the Closure Property 8 | 9 | 앞에서는 튜플 구조로 복잡한 물체를 표현하는 방법을 설명했다. 그림 2.2는 튜플 구조에 대한 상자와 화살표 표현이다. 10 | 11 | ![그림2.2](https://github.com/godrm/SICP-Swift/blob/master/images/2.2.png?raw=true) 12 | 13 | 그리고 증명했던 것처럼 튜플 구조를 사용하면 모든 종류의 데이터 구조를 설계하는 데 사용할 수 있다. 그림 2.3은 1,2,3,4를 튜플로 표현하는 두 가지 방법이다. 14 | 15 | ![그림2.3](https://github.com/godrm/SICP-Swift/blob/master/images/2.3.png?raw=true) 16 | 17 | 튜플이 다시 튜플의 원소가 될 수도 있다. 이렇게 계층 형태로 만들 수 있도록 하는 성질은 대수학에서는 닫힘(closure)라고 부른다. 데이터 물체를 연산하고 난 결과 물체가 다시 그 연산의 대상이 될 수 있으면 닫힘 성질closure property를 갖는 것이다. 이 성질은 흔히 말하는 함수와 클로저에서 클로저와는 다른 표현이다. 18 | 19 | 스위프트 문법으로는 이런 성질을 갖도록 선언하려면 타입을 좀 더 추상적인 타입으로 지정해야 한다. 20 | 21 | ```swift 22 | typealias Pair = (a: Any, b: Any) 23 | 24 | func make_pair(a: Any, b: Any) -> Pair { 25 | Pair(a: a, b: b) 26 | } 27 | 28 | make_pair(a: make_pair(a: 1, b: 2), b: make_pair(a: 3, b: 4)) 29 | ``` 30 | 31 | 지금까지 작성한 프로그램 대부분 닫힘 성질을 갖고 있었지만, 이 절에서는 닫힘 성질에 바탕을 둔 합친 데이터 구조 - 차례열sequence와 나무tree를 만들어본다. 32 | 33 | ### 2.2.1 차례열의 표현 34 | 35 | 그림 2.4처럼 튜플을 연결해서 1,2,3,4 차례열을 표현할 수 있다. 36 | 37 | ![그림2.4](https://github.com/godrm/SICP-Swift/blob/master/images/2.4.png?raw=true) 38 | 39 | 하지만 실제로 다음과 같이 작성하면 동작하지 않는다. 책에서는 List 개념을 Pair를 재귀적으로 반복해서 사용하며, 마지막에 nil이 나오는 데이터 표현으로 설명하고 있다. 스위프트에서는 동일한 데이터 구조가 없다. 40 | 41 | ```swift 42 | Pair(a: 1, 43 | b: Pair(a: 2, 44 | b: Pair(a: 3, 45 | b: Pair(a: 4, 46 | b: nil)))) 47 | ``` 48 | 49 | 대신 이렇게 튜플을 연결한 것과 같은 개념으로 차례열을 배열Array로 제공한다. 이런 배열을 생성해서 만들 때도 Array() 라는 프로시저를 사용한다. 50 | 51 | ```swift 52 | Array(arrayLiteral: 1,2,3,4) 53 | ``` 54 | 55 | 스위프트에서 배열을 자주 쓰다보니, 원소를 괄호로 묶어서 `[]` 형태로 배열 값을 표현할 수도 있다. 56 | 57 | ```swift 58 | let one-throught-four = [1,2,3,4] 59 | ``` 60 | 61 | 배열에서 첫 번째 원소를 빼는 방법은 first라는 연산을 호출하는 것이다. 맨 마지막 원소를 구하는 방법은 last 연산을 호출하는 것이다. 62 | 63 | ```swift 64 | oneThroughFour.first 65 | //result = 1 66 | oneThroughFour.last 67 | //result = 4 68 | [10]+oneThroughFour 69 | //result = 10,1,2,3,4 70 | ``` 71 | 72 | #### 배열 연산 73 | 74 | 차례열 원소를 배열로 만든 데이터가 있다면, 반복해서 훑어 내려가는 프로그램 기법으로 배열 연산을 만들 수 있다. 배열과 수 n을 매개 변수로 받아서, 배열의 n번째 원소를 내놓는 프로시저를 만들어보자. 75 | 76 | ```swift 77 | func array_ref(items: Array, n: Int) -> Any { 78 | if n == 0 { 79 | return items[0] 80 | } 81 | return array_ref(items: Array(items[1...]), n: n-1) 82 | } 83 | 84 | array_ref(items: [1,4,9,16,25], n: 3) 85 | ``` 86 | 87 | 배열을 인자로 받아서 원소가 몇 개인지 알아보는 length() 함수는 전체를 탐색해서 훓어 내려가는 경우다. 88 | 89 | ```swift 90 | func length(items: Array) -> Int { 91 | var length = 0 92 | for item in items { 93 | length += 1 94 | } 95 | return length 96 | } 97 | ``` 98 | 99 | 100 | ---- 101 | 102 | [다음 2.3 Symbolic Data](https://github.com/godrm/SICP-Swift/blob/master/2.3.md) 103 | 104 | -------------------------------------------------------------------------------- /Chapter1.md: -------------------------------------------------------------------------------- 1 | ## 1장. 프로시저로 요약하는 방식 (Building Abstractions with Procedures) 2 | 3 | ``` 4 | The acts of the mind, wherein it exerts its power over simple ideas, are chiefly these three: 5 | 1. Combining several simple ideas into one compound one, and thus all complex ideas are made. 6 | 2. The second is bringing two ideas, whether simple or complex, together, and setting them by one another so as to take a view of them at once, without uniting them into one, by which it gets all its ideas of relations. 7 | 3. The third is separating them from all other ideas that accompany them in their real existence: this is called abstraction, and thus all its general ideas are made. 8 | 9 | —John Locke, An Essay Concerning Human Understanding (1690) 10 | ``` 11 | 12 | 생각(아이디어)에 대한 인용문으로 시작한다. 인간은 단순한 생각들을 합쳐서 복합적인 생각으로 만들기도 하며, 단순하거나 복잡한 생각을 펼쳐놓고 관계를 살펴보기도 한다. 그리고 복잡한 생각이나 실체를 분리하고, 간추려서 요약(추상화, Abstraction)하기도 한다. 이 글은 추상화(Abstraction)에 대한 인지를 위해서 인용한 것 같다. 13 | 14 | `추상화`라는 단어는 말 그대로 추상적이다. 복잡한 실체를 감추고 요약되어 있기 때문에 추상화 과정을 거친 표현은 구체적인 정보를 포함하지 않는다. 사람들이 뇌에서 떠올린 개념을 각자의 언어로 표현하는 순간에도 이미 말과 글로 요약되기 마련이다. 15 | 16 | 이 책은 `추상화`라는 용어를 인용하면서 시작한다. 특히 번역서는 특히 한글로 풀어쓰는 경향이 강하기 때문에 추상화라는 한자어보다는 요약, 간추린 표현이 더 자주 등장한다. 이제 본문을 좀 더 읽어보자. 17 | 18 | ### 앞부분 19 | 20 | #### 용어 정리 21 | 22 | ##### 계산 프로세스 computational process 와 프로그램 Program 23 | 24 | - 컴퓨터 내부에서 데이터를 조작하면서 하는 어떤 작업(일) 25 | - 개발자가 지정해놓은 규칙을 따라 동작한다. 26 | - 이 규칙의 묶음을 프로그램 Program 이라고 한다. 27 | 28 | 프로그램은 여러 개의 표현식 Symbolic expression을 포함하고, 각 표현식을 작성할 때 사용하는 언어를 프로그래밍 언어라고 한다. 29 | 30 | ##### 프로그램은 위험한가 31 | 32 | 프로그램을 작성하는 일 자체가 위험하지는 않지만, 프로그램을 사용하는 기계가 사람을 위험하게 만들 수도 있다. 33 | 그래서 안전과 연결되는 일을 하는 기계에 들어가는 프로그램은 지식과 경험이 많은 개발자가 주의깊게 작성하도록 한다. 34 | 책에 나오는 것처럼 산업 인프라 혹은 비행기, 기차에서 사용하는 프로그램의 기준은 훨씬 높다. 35 | 36 | #### 리스프 Lisp과 스킴 Scheme 37 | 38 | Lisp은 `LISt Processing` 리스트 처리라는 말의 약자로, 기호로 이루어진 데이터를 다루기 편리하도록 설계했다. 39 | 기계가 처리할 프로세스를 표현하기 위해 선택한 언어는 Lisp 계통의 스킴 Scheme 40 | 41 | 이 책에서 Lisp 계열 언어를 중심으로 설명하는 이유는 다음과 같은 표현이 나온다. 42 | 43 | ``` 44 | The importance of this is that there are powerful program-design techniques that rely on the ability to blur the traditional distinction between “passive” data and “active” processes. As we shall discover, Lisp’s flexibility in handling procedures as data makes it one of the most convenient languages in existence for exploring these techniques. The ability to represent procedures as data also makes Lisp an excellent language for writing programs that must manipulate other programs as data, such as the interpreters and compilers that support computer languages. 45 | ``` 46 | 47 | 바로 능동적인 `프로세스`와 수동적인 `데이터`를 구분하는 일반적인 절차지향 프로그래밍 방식보다 `프로시저(또는 함수)`를 `데이터`처럼 다룰 수 있는 유연한 언어적인 특성 때문이다. 48 | 이런 특징은 객체지향 패러다임과 함수형 패러다임을 모두 갖춘 현대적인 언어들에서도 공통적으로 찾아볼 수 있는 장점이다. 49 | 그래서 이 글에서는 마찬가지 기준으로 스위프트를 활용해서 설명하려고 한다. 50 | 51 | 덧붙여서 Swift로 프로그램을 작성하는 과정도 매우 즐거운 일이다. 52 | 53 | ### 목차 54 | 55 | [1.1 The Elements of Programming](https://github.com/godrm/SICP-Swift/blob/master/1.1.md) 56 | 57 | [1.2 Procedures and the Processes They Generate](https://github.com/godrm/SICP-Swift/blob/master/1.2.md) 58 | 59 | [1.3 Formulating Abstractions with Higher-Order procedure](https://github.com/godrm/SICP-Swift/blob/master/1.3.md) 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /Chapter2.md: -------------------------------------------------------------------------------- 1 | ## 2장. 데이터로 요약하는 방식 (Building Abstractions with Data) 2 | 3 | ``` 4 | We now come to the decisive step of mathematical abstraction: we forget about what the symbols stand for.... [The mathematician] need not be idle; there are many operations which he may carry out with these symbols, without ever having to look at the things they stand for. 5 | —Hermann Weyl, The Mathematical Way of Thinking 6 | ``` 7 | 8 | 1장에서는 프로그램을 설계하는 데 계산 프로세스와 프로시저가 어떤 역할을 하는지 살펴봤다. 9 | 프로그램을 작성한다는 것은 기본 데이터 값과 연산을 어떻게 사용하는지부터 시작한다. 프로시저는 매개 변수와 조건식을 가지고 복잡한 프로시저를 어떻게 정의하느냐가 중요하다. 프로시저는 프로세스가 자라나는 규칙을 정의하는 것이라서 계산 과정이 비슷하게 움직이는 알고리즘을 분석도 했다. 10 | 11 | 이 장에서는 복잡한 데이터를 어떻게 요약하는지 공부한다. 앞으로는 단순한 데이터만 가지고 풀기 어려운 문제를 살펴본다. 프로그램은 복잡한 현상들을 비슷하게 흉내내서 부품처럼 프로세스로 만들 수 있다. 이런 계산 물체를 만들기 위해서, 여러 데이터 물체를 묶어서 묶음 데이터로 요약하는 방법을 살펴본다. 12 | 13 | 데이터 묶음은 묶음 프로시처럼 더 높은 수준에서 프로그램 설계를 생각할 수 있고, 모듈을 조립해서 프로그램을 작성하는 데 꼭 필요하다. 복잡한 데이터를 만들려면 기본 데이터만으로 부족하다. 언어 표현력을 넓혀서 문제 풀이에 좀 더 적합한 방식으로 데이터를 다뤄야 한다. 14 | 15 | 유리수를 다루는 시스템을 기본 데이터로 다룬다고 생각해보자. 분모와 분자를 각각 정수로 나타낼 수 밖에 없다. 그리고 계산을 한다고 해도 분모와 분자를 각각 계산하는 프로시저를 두 개 만들어야만 한다. 이렇게 하면 어떤 분모와 어떤 분자가 쌍을 이루는지도 기억해야 하고 불편하다. 그래서 유리수 프로그램에서는 유리수 하나를 분모와 분자 한 쌍 데이터를 묶어서 만들어 사용해야 한다. 16 | 17 | 예를 들어 ax + by 형태 `일차 결합 linear combination` 식을 표현해보자. 이름이 a,b,x,y가 모두 숫자를 나타낼 때 다음과 같이 만들 수 있다. 18 | 19 | ```swift 20 | func liner_combination(a: Int, b: Int, x: Int, y: Int) -> Int { 21 | return a * x + b * y 22 | } 23 | ``` 24 | 25 | 이 상태에서 정수 뿐만 아니라 유리수나 복소수, 다항식 등 어떤 것이든지 덧셈과 곱셈을 하는 표현식으로 프로시저를 고쳐야 한다면 어떻게 해야할까. 어떤 데이터가 들어오더라도 덧셈add과 곱셈mul 계산할 수 있는 더 똑똑한 표현이 필요하다. 26 | 27 | ```swift 28 | func add(_ a: T, _ b: T) -> T where T : Numeric { 29 | return a + b 30 | } 31 | 32 | func mul(_ a: T, _ b: T) -> T where T : Numeric { 33 | return a * b 34 | } 35 | 36 | func linear_combination(a: T, b: T, x: T, y: T) -> T where T : Numeric { 37 | return add(mul(a, x), mul(b, y)) 38 | } 39 | ``` 40 | 41 | 스위프트는 명확하지 않은 모든 데이터 타입에 대해 미리 계산 방식을 결정할 수 없다. 적어도 어떤 역할을 해야 하는지 숫자 형태라는 것을 와 where 조건으로 명시해야 한다. 자세한 문법은 나중에 확인해보자. 42 | 43 | 데이터 요약을 통해 프로그램을 구성할 때, 여러 부품 사이에 알맞은 요약 경계 abstraction barrier를 구분해야 한다. 이런 경계를 구분해서 묶음 데이터를 만들 때 가장 중요한 것은 프로그래밍 언어로 묶는 표현이다. 여러 데이터를 묶는 방법도 여러 가지가 있고, 프로시저만으로도 복잡한 데이터를 표현할 수 있다는 것을 알았다. 묶음 데이터 가운데 가장 많이 사용하는 것은 차례열sequence과 나무tree 구조 표현도 살펴본다. 44 | 45 | 묶음 데이터를 설명할 때 중요한 것은 닫힘 성질closure property이다. 묶음 데이터를 묶으면 다시 묶음 데이터가 된다는 것이다. 묶음 데이터는 기본 데이터를 묶어서 만들어지고, 묶음 데이터를 다시 묶어도 묶음 데이터가 된다. 46 | 47 | 여러 프로그램 부품을 조립하다보면 서로 부품이 맞지 않는 경우가 생기기도 하는데, 묶음 데이터를 활용해서 여러 부품 사이에서 공통 인터페이스conventional interface 역할을 한다. 이를 설명하기 위해서 닫힘 성질을 만족하는 그래픽 언어를 만든다. 48 | 49 | 이 장부터는 숫자 말고도 데이터로 표현하는 글자 표현 symbolic expression을 배워서 언어의 표현력을 끌어올린다. 집합을 나타내는 방법들도 확인한다. 숫자를 다룰 때도 여러 계산 프로세스로 구현할 수 있었듯이, 복잡한 데이터도 여러 방법으로 있고 알고리즘에 따라 시간과 공간이 달라진다는 것을 알게 된다. 50 | 51 | 그 이후에는 프로그램에서 한 가지 데이터를 여러 방법으로 나타낼 수 있어야 할 때 프로그램 구조에 대해 공부한다. 이 문제는 서로 다른 타입 데이터를 다룰 수 있도록 일반화된 연산generic operation을 만든다. 앞서 설명한 것처럼 요약의 경계를 확실하게 구분하기 위해서 `데이터 중심으로 프로그램 작성하는 방법`을 알아본다. 여러 데이터를 처리하는 다항식 산술 연산 꾸러미를 만들어본다. 52 | 53 | 54 | ### 목차 55 | 56 | [2.1 Introduction to Data Abstraction](https://github.com/godrm/SICP-Swift/blob/master/2.1.md) 57 | 58 | [2.2 Hierarchical Data and the Closure Property](https://github.com/godrm/SICP-Swift/blob/master/2.2.md) 59 | 60 | [2.3 Symbolic Data](https://github.com/godrm/SICP-Swift/blob/master/2.3.md) 61 | 62 | [2.4 Multiple Representations for Abstract Data](https://github.com/godrm/SICP-Swift/blob/master/2.4.md) 63 | 64 | [2.5 Systems with Generic Operations](https://github.com/godrm/SICP-Swift/blob/master/2.5.md) 65 | 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SICP-Swift 2 | SICP with Swift 해설 프로젝트 3 | 4 | ## 시작하면서 5 | Structure and Interpretation of Computer Programs 6 | 컴퓨터 프로그램의 구조와 해석 7 | 8 | 이미 번역서가 `컴퓨터 프로그램의 구조와 해석 (인사이트)` 나와있어서 그대로 번역을 하기 보다는 9 | 스위프트 언어를 기반으로 하는 요약과 해석을 작성하기로 한다. 10 | 11 | 이 책은 5부로 나눠져있다. 12 | 1. [프로시저로 요약하는 방식](https://github.com/godrm/SICP-Swift/blob/master/Chapter1.md) (Building Abstractions with Procedures) 13 | 14 | 2. [데이터를 요약하는 방식](https://github.com/godrm/SICP-Swift/blob/master/Chapter2.md) (Building Abstractions with Data) 15 | 16 | 3. 모듈과 물체, 상태 (Modularity, Objects, and State) 17 | 18 | 4. 언어를 처리하는 방식 (Metalinguistic Abstraction) 19 | 20 | 5. 레지스터 기계로 계산하기 (Computing with Register Machines) 21 | 22 | > 영문판 목차를 보려면 [목차](https://mitpress.mit.edu/sites/default/files/sicp/full-text/book/book-Z-H-4.html#%_toc_start)에서 확인하면 된다. 23 | 24 | 25 | ### 프로젝트 시작 동기 26 | 27 | 원문에서는 Lisp 언어와 같은 계통 언어인 Scheme을 기준으로 예제 코드가 만들어졌다. 28 | 다른 책에서는 JS를 기반으로 작성한 버전이 있다. [JS Adaption](https://sicp.comp.nus.edu.sg) 29 | 30 | 이와 마찬가지로 이 글의 목표는 스위프(Swift) 언어를 기준으로 예제를 작성하고, 해설하는 것이다. 31 | 대부분 설명과 내용은 번역서를 통해서 읽어도 충분하지만, Lisp 이나 Scheme과 같은 함수 언어에 대한 경험이 없는 사람이 많다. 단지 Lisp이나 Scheme에서 표현하기 어려운 부분도 있기 때문에 JS버전도 공존하는 것 같다. 32 | 33 | 스위프트는 처음 배우는 사람들에게도 어렵지 않게 책의 내용을 설명하기에 적합한 언어라고 생각한다. 34 | 그래서 이 프로젝트를 진행하기로 한다. 35 | 36 | #### 라이센스 37 | 38 | 원문 책의 라이센스가 CC BY-SA 4.0 으로 작성되어 있다. 39 | 40 | ``` 41 | This is work is licensed under a Creative Commons 42 | Attribution-ShareAlike 4.0 International License. (CC BY-SA 4.0) 43 | ``` 44 | 45 | 이 글의 라이센스도 마찬가지로 `CC BY-SA 4.0`을 따르기로 한다. 46 | -------------------------------------------------------------------------------- /images/1.1.7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godrm/SICP-Swift/c6b7bda039d8056a49f2b4c72cb3c50f801894af/images/1.1.7.png -------------------------------------------------------------------------------- /images/1.1.8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godrm/SICP-Swift/c6b7bda039d8056a49f2b4c72cb3c50f801894af/images/1.1.8.png -------------------------------------------------------------------------------- /images/1.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godrm/SICP-Swift/c6b7bda039d8056a49f2b4c72cb3c50f801894af/images/1.1.png -------------------------------------------------------------------------------- /images/1.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godrm/SICP-Swift/c6b7bda039d8056a49f2b4c72cb3c50f801894af/images/1.2.png -------------------------------------------------------------------------------- /images/1.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godrm/SICP-Swift/c6b7bda039d8056a49f2b4c72cb3c50f801894af/images/1.3.png -------------------------------------------------------------------------------- /images/1.4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godrm/SICP-Swift/c6b7bda039d8056a49f2b4c72cb3c50f801894af/images/1.4.png -------------------------------------------------------------------------------- /images/1.5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godrm/SICP-Swift/c6b7bda039d8056a49f2b4c72cb3c50f801894af/images/1.5.png -------------------------------------------------------------------------------- /images/2.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godrm/SICP-Swift/c6b7bda039d8056a49f2b4c72cb3c50f801894af/images/2.1.png -------------------------------------------------------------------------------- /images/2.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godrm/SICP-Swift/c6b7bda039d8056a49f2b4c72cb3c50f801894af/images/2.2.png -------------------------------------------------------------------------------- /images/2.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godrm/SICP-Swift/c6b7bda039d8056a49f2b4c72cb3c50f801894af/images/2.3.png -------------------------------------------------------------------------------- /images/2.4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godrm/SICP-Swift/c6b7bda039d8056a49f2b4c72cb3c50f801894af/images/2.4.png -------------------------------------------------------------------------------- /images/equation1.4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godrm/SICP-Swift/c6b7bda039d8056a49f2b4c72cb3c50f801894af/images/equation1.4.png -------------------------------------------------------------------------------- /images/equation2.1.4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godrm/SICP-Swift/c6b7bda039d8056a49f2b4c72cb3c50f801894af/images/equation2.1.4.png -------------------------------------------------------------------------------- /images/example1.8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godrm/SICP-Swift/c6b7bda039d8056a49f2b4c72cb3c50f801894af/images/example1.8.png -------------------------------------------------------------------------------- /images/example2.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godrm/SICP-Swift/c6b7bda039d8056a49f2b4c72cb3c50f801894af/images/example2.9.png --------------------------------------------------------------------------------