You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
@@ -15,7 +15,7 @@ Functional language 중 하나인 Haskell은 코드의 안정성을 위해 제
15
15
16
16
## Basis
17
17
18
-
가장 많이 참고했던 수학 이론은 바로 집합이다. 현대 수학에서 집합을 파고 들어가면 한도 끝도 없이 파고들게 되고 얼마 안가서 대체 뭘 하려고했는지 잊어버리게 되는데, 프로그래밍 언어에서는 그 정도로 고도화된 이론을 바탕으로 하지 않는다. 집합에 대해 알아야 할 것들은 아래와 같다.
18
+
가장 많이 참고했던 수학 이론은 바로 집합이다. 현대 수학에서 집합을 파고 들어가면 한도 끝도 없이 파고들게 되고 얼마 안가서 대체 뭘 하려고했는지 잊어버리게 되는데, 프로그래밍 언어에서는 그 정도로 고도화된 이론을 바탕으로 하지 않는다. 하지만 그럼에도 수학에서 정의되는 `집합`과 `대응`이라는 개념을 이해할 필요가 있다. 집합에 대해 알아야 할 것들은 아래와 같다.
19
19
20
20
### Useful knowledge in the Set theory
21
21
@@ -25,23 +25,27 @@ Functional language 중 하나인 Haskell은 코드의 안정성을 위해 제
25
25
26
26
수학이라는 분야 자체에 얽매이면 이 `대상`을 숫자 이외에 더 넓은 개념으로 확장시키지 못하기도 하는데, 집합에서 대상은 반드시 숫자일 필요가 없다. 집합에서 다루는 `대상`은 숫자나 수식, 문자, 사물, 개념 등등 모든 것이다. 물론 이 방향으로 사고를 확장하다보면 집합이 `대상`인 집합을 생각하게되면서 `러셀의 역설`에 맞닥들일 수 있기 때문에 더 엄밀한 정의를 필요로 한다면 현대 수학의 기반이 되고있는 [ZFC 공리계](https://ko.wikipedia.org/wiki/%EC%B2%B4%EB%A5%B4%EB%A9%9C%EB%A1%9C-%ED%94%84%EB%A0%9D%EC%BC%88_%EC%A7%91%ED%95%A9%EB%A1%A0)을 살짝 찾아볼 수는 있다.
27
27
28
-
우리가 이해해야 할 점은 바로 두 집합간의 대응관계이다. 한 집합의 대상과 다른 집합의 대상끼리 연관을 지을 수 있는데 이 관계가 여러 대상에 적용된다면 두 집합은 대응관계라고 할 수 있다. 이 관계는 크게 네가지 종류가 있는데, 1:1 대응, 1:n 대응, n:1 대응, m:n 대응이다.
29
-
30
-
이제 바로 여기에서부터 `함수`가 나온다. `함수`는 대응 관계 중에서도 특수한 것으로, 두 집합의 1:1 대응, n:1 대응 만을 표현할 수 있다. 1:n이나 m:n으로 표현되는 것이 있다면 그것은 `함수`라고 부르지 않는다. 수학적 개념이니 수학적인 느낌으로 표현해보면 이렇게 볼수있다.
31
-
32
-
set A 와 set B 가 있으면 함수 f는 f: A -> B 이다. a <- A, b <- B 라면, f(a) = b 라고 표현한다.
28
+
Haskell에서 집합이 어떻게 표현되는지는 다음에 한번 더 정리를 해볼 예정이다.
33
29
34
30
### About function
35
31
36
32
현대에 주류 언어에서는 거의 함수라는 용어로 통용되어가는것 같다. 하지만 원래 비슷한 개념들이 프로그래밍 언어에는 아주 많았는데, Function, Procedure, Subroutine, 그리고 OOP에서 사용되는 Method이다. 모두 비슷하지만 다른 용어이다.
37
33
38
34
프로그램 코드 덩어리를 하나의 작업으로 보면 항상 같은 과정으로 작업을 수행하기 때문에 Routine 이라고 부른다. 이 Routine을 분할하면 각각을 Subroutine이라고 부른다. Procedure는 프로그램 코드를 특정한 목적을 달성하기위한 Process라고 할 때 그 Process 묶음을 Procedure라고 부른다. Method란 어떤 사람, 동물, 기계, 각종 사물, 개념적인 뭔가가 하는 행위 자체를 의미한다. 이 개념은 Procedure와 Routine을 포함하고 있다. 그래서 Method는 항상 Class 안에서만 정의되고 Class로 생성된 Object에서 호출한다.
39
35
40
-
이 개념들을 두고 보면 Subroutine, Procedure, Method는 모두 호출되는 시점의 환경과 조건에 영향을 받는다. Function(함수)는 그런 점이 다른데, 다른 언어에서는 혼용해도 상관없지만 Haskell 등의 일부 Pure functional language에서는 반드시 구분해야 한다.
36
+
이런 점에서 Function은 다른 개념과는 큰 차이가 있는데, Function은 본질적으로 두 집합간의 대응을 의미기 때문이다. 그래서 다른 개념은 실행 시점의 상태(state)에 따라서 같은 입력에도 다른 결과가 발생할 수 있다. 하지만 Function은
37
+
38
+
$$
39
+
f:A \to B, \forall a \in A, \exists! b \in B \text{ such that } f(a) = b
40
+
$$
41
+
42
+
로 정의할 수 있다. 이러한 함수의 정의가 보여주는 핵심은 ‘입력값이 같으면 항상 같은 결과를 반환하는가’의 여부이며, 이 조건이 충족되는 경우에만 진정한 의미의 함수(pure function)라고 부른다.
43
+
44
+
이 특징이 Haskell과 같은 프로그래밍 언어에서 어떤 의미를 갖는지 이해하는 것이 중요하다.
41
45
42
46
## Function in Haskell
43
47
44
-
위의 내용을 보고 Haskell의 함수를 보면 왜 이렇게 될 수밖에 없는지 이해할 수 있다. 간단하게 \(f(x) = x + 1\) 을 Haskell 문법으로 표현하면 이렇게 된다.
48
+
위의 내용을 바탕으로 Haskell의 함수를 바라보아야 Haskell 코드의 작동 방식을 제대로 이해할 수 있다. 간단한 예로 $f(x) = x + 1$ 을 Haskell 문법으로 표현하면 이렇게 된다.
45
49
46
50
```haskell
47
51
f::Num->Num
@@ -54,7 +58,7 @@ f x = x + 1
54
58
55
59
이 기능은 수학에서 흔히 쓰이는 표현과 대응된다. 간단한 함수를 예로 들어서 보면 이런걸 들 수 있다.
56
60
57
-
$$
61
+
$$
58
62
\begin{cases}
59
63
&y = x + 1 (x\ge0) \\
60
64
&y = 1 (x\lt0)
@@ -70,7 +74,7 @@ y x
70
74
| x <0=1
71
75
```
72
76
73
-
여기서 `|`를 가드(Guard)라고 부른다. Haskell 에서는 Procedure language 패러다임에서 보이는 분기 구문이 없기 때문에 이러한 방법으로 조건에 따라 다른 연산을 할 수 있게 수식을 분리할 수 있다. 위에서 이야기했던 함수의 특징에는 순차실행의 개념이 없기 때문이다.
77
+
여기서 `|`를 가드(Guard)라고 부른다. Haskell 에서는 Procedure language 패러다임에서 보이는 분기 구문이 없기 때문에 이러한 방법으로 조건에 따라 다른 연산을 할 수 있게 수식을 분리할 수 있다. 위에서 이야기했던 함수의 특징에는 순차실행의 개념이 없기 때문이다.
74
78
75
79
### Recursive function
76
80
@@ -79,7 +83,7 @@ y x
79
83
$$
80
84
\begin{cases}
81
85
&a_1 = 1 \\
82
-
&a_n = a_{n-1} + 2
86
+
&a_n = a_{n-1} + 2
83
87
\end{cases}
84
88
$$
85
89
@@ -94,12 +98,135 @@ a x
94
98
95
99
이 코드를 약간 해석한다면 함수는 정수 값을 입력 받아서 정수 값을 반환하는 함수인데, 이 점화식으로 표현되는 집합의 x 번째 수를 구하는 함수가 된다. 이러한 재귀적 함수를 작성할 때는 반드시 재귀가 종료되는 지점을 알려주어야 한다.
96
100
97
-
여기에 약간 공학적인 관점에서 이유를 붙이자면, 컴파일러는 함수를 호출하는 시점을 메모리 스택에 기록해두는데, 호출 한 함수가 끝났을 때 다시 호출시점으로 돌아오게 하기위함이다. 그런데 메모리에는 항상 한계가 있어서 무한 재귀함수는 공학적으로 불가능하기 때문에 시스템은 늘 재귀 한계를 정의해둔다. 따라서 이 한계를 넘어서는 순간 Stack overflow, 혹은 Segmentation fault로 프로그램을 강제로 종료시켜버린다. 이런 조치가 없으면 프로그램 카운터가 이 프로그램이 할당받은 메모리 양을 무단으로 넘어가게되고 확률적으로 여러 다른 프로그램에 말도 안되는 영향을 줄 수 있기 때문에 OS가 메모리를 관리하려는 차원에서도 강제로 종료시키는 것이다.
101
+
여기에 약간 공학적인 관점에서 이유를 붙이자면, 컴파일러는 함수를 호출하는 시점을 메모리 스택에 기록해두는데, 호출 한 함수가 끝났을 때 다시 호출시점으로 돌아오게 하기위함이다. 그런데 메모리에는 항상 한계가 있어서 무한 재귀함수는 공학적으로 불가능하기 때문에 시스템은 늘 재귀 한계를 정의해둔다. 따라서 이 한계를 넘어서는 순간 Stack overflow, 혹은 Segmentation fault로 프로그램을 강제로 종료시켜버린다. 이런 조치가 없으면 프로그램 카운터가 이 프로그램이 할당받은 메모리 양을 무단으로 넘어가게되고 확률적으로 여러 다른 프로그램에 말도 안되는 영향을 줄 수 있기 때문에 OS가 메모리를 관리하려는 차원에서도 강제로 종료시키는 것이다.
102
+
103
+
Haskell 에는 Lazy evaluation 등의 다양한 최적화 기법들을 이용해서 메모리가 무작정 낭비되는 문제를 피하기 떄문에 쉽게 오류를 볼 수는 없겠지만 무한히 긴 실행시간을 보게된다. 무슨말이냐면, 재귀 함수가 무한히 반복되지 않게 해야 한다는 것이다.
104
+
105
+
### Lambda expression
106
+
107
+
함수를 표현하는 또 하나의 문법으로 `익명 함수`를 의미하는 Lambda expression이 있다. 이 표현식은 "Lambda calculus"에서 사용되는 표기법을 옮긴 것으로, 간단한 구조의 함수들은 인라인에서 정의될 수 있음을 활용하는 것이라고 할 수 있다.
108
+
109
+
가령, 앞에서 예로 들었던 함수 $f(x) = x + 1$ 의 경우 이 표현식을 위해서 함수를 정의하기 보다는 `\x -> x + 1` 로 표현하는 식이다. 이런 굉장히 단순해보이는 표기법의 변경에서 자칫 Lambda expression이 문법을 단순화해주는 도구로 오해할 수 있지만, 이 표현식의 핵심은 모든 계산이 함수로 표현될 수 있다는 Lambda calculus의 핵심 개념과 닿아있다. 즉, 앞에서 보인 함수 표현식은 $\lambda x.x+1$과 직접적으로 대응되는 표현이다.
110
+
111
+
## Application of function
112
+
113
+
다시 말하자면 함수는 Subroutine, Procedure, Method와 다른 방식으로, 보다 수학적인 의미의 함수로 활용이 가능하다. 아래에는 그런 활용 방식을 나열해본다.
114
+
115
+
### 일급 함수로서의 Lambda expression
116
+
117
+
Haskell과 같은 Functional programming language는 함수를 객체화한다. 가장 간단한 예로 이런 함수를 정의해보겠다.
118
+
119
+
```haskell
120
+
f::(a->a) ->a->a
121
+
f t x = (t x) +1
122
+
```
123
+
124
+
이렇게 정의한 함수를 아래와같이 호출하는 것이다.
98
125
99
-
Haskell 에는 Lazy evaluation 등의 다양한 최적화 기법들을 이용해서 메모리가 무작정 낭비되는 문제를 피하기 떄문에 쉽게 오류를 볼 수는 없겠지만 무한히 긴 실행시간을 보게된다. 무슨말이냐면, 재귀 함수가 무한히 반복되지 않게 해야 한다는 것이다.
126
+
```haskell
127
+
v = f (\n ->2* n) 10
128
+
```
129
+
130
+
이 호출 결과 v는 21이 되고, 여기서 함수 f의 파라미터인 t를 일급 함수라고 할 수 있다.
131
+
132
+
### 고차 함수로서의 Lambda expression
133
+
134
+
고차함수, Higher order function은 함수를 인자로 받을 수 있고, 반환결과로 함수를 반환하는 함수를 가리킨다. 뒤에서 좀 더 자세히 보겠지만, currying으로 아주 자연스럽게 구현되어있는데, 항목 1의 예제 함수와 같은
135
+
136
+
```haskell
137
+
f::Numa=> (a->a) ->a->a
138
+
f t x = (t 2*x) /2
139
+
```
140
+
141
+
라고 할 때, 함수 f 자체가 고차 함수라고 할 수 있다. f가 고차함수로 활용되는 예를 이렇게 할 수 있다.
142
+
143
+
```haskell
144
+
g = f \x->(sqrt x)
145
+
v = g 10
146
+
```
147
+
148
+
이 때는 함수 f의 반환형도 바로 함수 g이다.
149
+
150
+
### Closure
151
+
152
+
클로저는 고차 함수에서 파생되는 개념으로, 고차함수를 정의할 때 반환되는 함수에서 하나 이상의 값으로 표현되는 실행 환경을 나중에 정의할 수 있도록 변수화 하는 기법이라고 표현할 수 있을 것 같다.
153
+
154
+
수식으로 표현해본다면
155
+
156
+
$$
157
+
f(x) = a * x + b
158
+
$$
159
+
160
+
이 수식에서 a = 1, b = 2라고 정의하면
161
+
162
+
$$
163
+
f(x) = 1 * x + 2
164
+
$$
165
+
166
+
이렇게 만들어진다.
167
+
168
+
그런데 이 a, b도 함수의 파라미터로 정의할 수 있는데, 바로 Haskell 문법에서 이렇게 적을 수 있게된다.
169
+
170
+
```haskell
171
+
g::Numa=>a->a->(a->a)
172
+
g a b =\x->a*x+b
173
+
174
+
f = g 12
175
+
```
176
+
177
+
코드가 이런 구조일 때 함수 g는 고차함수가 되고, a, b가 클로저이다. 즉, 함수 g를 호출하는 것으로 상수인 계수 a, b를 고정하는 것으로 함수 f가 정의되는 시점의 상태를 고정한 함수의 부분 적용과 같다고 볼 수 있다. 아래는 좀 더 실용적으로 표현해본 예제이다.
178
+
179
+
```haskell
180
+
g::Numa=>a->(a->a)
181
+
g a =\x->a*x
182
+
183
+
makeDouble = g 2
184
+
4== makeDouble 2
185
+
186
+
makeTriple = g 3
187
+
6== makeTriple 2
188
+
```
189
+
190
+
### Composition
191
+
192
+
집합에서 함수 $f:A \to B, g:B \to C$ 일 때, 합성 함수 $g \circ f:A \to C$ 라고 표현할 수 있는데, Haskell 언어에서도 이 표현식을 문법으로 지원하고 있다.
193
+
194
+
```haskell
195
+
f x = x +1
196
+
g x =2* x
197
+
198
+
5== f.g 2
199
+
```
200
+
201
+
길게 볼 필요 없이 이렇게 된다. 이때 `f.g`와 $f \circ g$는 동일한 표현이다.
202
+
203
+
### Currying
204
+
205
+
이 언어의 이름인 Haskell은 원래 지금 설명하는 `Currying`과 함께 수학자인 [Haskell Curry (wikipedia)](https://en.wikipedia.org/wiki/Haskell_Curry)의 이름에서 따온 이름이다.
206
+
207
+
`Currying`는 원래의 수학적 의미가 다변수 함수를 단변수 함수의 중첩으로 변환하는 작업을 의미한다. Haskell에도 이 의미가 그대로 적용되어 아래와 같은 변화가 문법 수준에서 지원된다.
208
+
209
+
```haskell
210
+
f::(a,a) ->a
211
+
f x y = x * y
212
+
```
213
+
214
+
이렇게 표현하면 (a, a)는 같은 타입으로 전달되는 두개의 파라미터를 가리키는데, 이 표현은 이렇게 바꿀 수 있다.
215
+
216
+
```haskell
217
+
f::a->a->a
218
+
f x y = x*y
219
+
```
220
+
221
+
그리고 이렇게 표현된 함수는 이런 식으로 활용이 가능하다.
222
+
223
+
```haskell
224
+
makeDouble = f 2
225
+
6== makeDouble 3
226
+
```
100
227
101
228
## Conclusion
102
229
103
-
아주 간단하고 가볍게 함수에 대해 정리하고 이게 Haskell에서 어떻게 표현되는지 정리해봤다. 너무 짧게 정리하려고 했더니 논리적인 비약도 있고 건너뛴 점도 많고 왜곡도 있을 수 있지만 대충 아는대로 정리한 내용이라서 나중에 더 많이 알게되면 좀 더 내용을 보완해서 새로운 글로 만들어야 할 것 같다. 특히 함수에 대해서는 Haskell을 포함해서 Functional programming 자체를 이해하기 위해 집합론을 대충 훑었던 지식을 짜집기한 결과이기 때문에 실제로는 아닌 내용이 있을 수 있다.
230
+
가능하면 간단하게 함수의 활용에 대해 정리하고 이게 Haskell에서 어떻게 표현되는지 정리해봤다. 너무 짧게 정리하려고 했더니 논리적인 비약도 있고 건너뛴 점도 많고 왜곡도 있을 수 있지만 대충 아는대로 정리한 내용이라서 나중에 더 많이 알게되면 좀 더 내용을 보완해서 새로운 글로 만들어야 할 것 같다. 특히 함수에 대해서는 Haskell을 포함해서 Functional programming 자체를 이해하기 위해 집합론을 대충 훑었던 지식을 짜집기한 결과이기 때문에 실제로는 아닌 내용이 있을 수 있다.
104
231
105
-
Haskell에서의 함수도 겨우 이 정도만 갖고 프로그램을 쓸 수는 없다. 하지만 기초적인 수준에서 프로그램을 작성하는데는 문제가 없을 것 같아 주관적인 기준에서 제일 중요하다 싶은 문법 구조를 두가지만 예제코드와 함께 요약했다. 나중에 다른 언어에서 Functional programming을 할 때 참고가 되길 바라면서..
232
+
물론 이것 만으로는 프로그램을 만들 수 없기 때문에 이 다음에는 타입 시스템에 대해서도 다뤄볼까 한다.
0 commit comments