3. 함수

일, 2006-07-09 20:39 — 귤

함수와 바꿔쓰기 규칙

함수형 언어는 '바꿔쓰기 규칙'에 바탕을 둔 프로그래밍 언어다. 바꿔쓰기 규칙이란 이름 그대로다. 다음과 같은 예를 보자.
a = 3 f x = x + 1
위의 코드에서 첫 줄은 "앞으로 a는 3으로 바꿔쓴다", "f x는 x + 1로 바꿔쓴다"라고 정의한 것이다. 여기서 a나 f처럼 맨 앞에 나온 이름을 함수라고 한다. f x의 x처럼 함수 뒤에 따라붙는 기호를 매개변수라고 한다. 매개변수는 실제로 x를 가리키는 것이 아니라 f 뒤에 따라붙는 값에 일시적으로 이름을 붙여주는 것이다. 그러니까 f 3이라고 쓰면 3 + 1, f 4라고 쓰면 4 + 1로 바꿔 쓴다. 만약 f a라는 표현이 있다면 a + 1로 바꿔쓰고 a + 1은 3 + 1로 바꿔쓴다. 마지막은 3 + 1은 4로 바꿔쓴다. 이런 식으로 더이상 바꿔쓸 수 없을 때까지 계속 바꿔쓴다. 다음 코드를 보면 함수형 언어와 명령형 언어의 결정적 차이를 알 수 있다.
x = x + 1
명령형 언어에서 위 코드의 뜻은 "x 변수에 저장된 값에 1을 더해서 다시 x 변수에 저장하라"는 뜻이다. 하지만 Haskell에서는 x는 x + 1로 바꿔쓴다는 뜻이다. x + 1에도 x가 있으므로 다시 x + 1로 바꿔쓴다. 이것이 아래와 같이 계속된다.
x ⇒ x + 1 ⇒ x + 1 + 1 ⇒ x + 1 + 1 + 1 ...

순환 (recursion)

바로 앞의 x처럼 함수가 "자기 자신을 포함한 표현으로 바꿔쓰는 경우"를 순환 (recursion)이라고 부른다. C와 유사한 언어에서는 '재귀호출'이라고 부른다. 순환은 같은 종류의 계산을 반복해야할 때 많이 사용한다.
간단한 계승(factorial:어떤 자연수 n에서 1까지 모든 자연수를 곱하는 것. 예컨데 5의 계승은 5*4*3*2*1=120이다.) 함수를 정의해보자. 일단 5의 계승은 4의 계승 (4*3*2*1)에 5를 곱한 것과 같다. 4의 계승은 3의 계승에 4를 곱한 것과 같다. 이런 식으로 생각해보면 어떤 수 n의 계승은 "n * (n-1)의 계승"이라고 생각할 수 있다. 다만 0의 계승은 1이라는 점만 유의하면 된다. 이것을 그대로 써주면 Haskell 코드가 된다.
factorial 0 = 1 factorial n = n * factorial (n - 1)
factorial 3을 했을 때 어떻게 되는지 살펴보자.
factorial 3 ⇒ 3 * factorial 2 ⇒ 3 * (2 * factorial 1) ⇒ 3 * (2 * (1 * factorial 0)) ⇒ 3 * (2 * (1 * 1)) ⇒ 3 * (2 * 1) ⇒ 3 * 2 ⇒ 6
방금 계승을 구현할 때 사용한 순환은 함수형 프로그래밍의 기본기이기 때문에 충분히 익혀두는 게 좋다.

함수에서 함수 만들기

앞에서 만들었던 f와 똑같은 함수 g를 한 번 만들어보자. g는 f와 똑같으니까 f로 바꿔쓰면 된다.
g = f
똑같은 함수를 두 번 만들 일은 없으니까 별로 쓸모가 없어보인다. 하지만 이 방법은 아주 편리한 경우가 있다.
add x y = x + y inc = add 1
add는 수 두 개를 더하는 함수다. 별로 어려울 것이 없다. 그러나 inc는 좀 특이하다. add 1이라고 했으니 1을 더하는 함수라는 것은 금방 이해가 된다. 그러나 C에 익숙한 사람이라면 어떻게 함수를 이렇게 만들 수 있는 지 이해가 가지 않을 것이다. 그럴 땐 바꿔쓰기를 해보면 된다.
inc 3 ⇒ add 1 3 ⇒ 1 + 3
Haskell은 이런 식으로 이미 있는 함수를 가지고 새로운 함수를 쉽게 만들 수 있다. 만약에 2를 더하는 함수를 만들고 싶다면 어떻게 할까? add 2라고 해도 되지만 다음과 같은 방법도 가능하다.
plusTwo = inc . inc
.은 수학의 ˆ와 동일한 뜻으로 두 함수를 합성하라는 함수다. .의 정의를 보면 왜 위의 코드가 2를 더하는 지 금방 이해할 수 있다.
g . f x = g (f x) plusTwo 3 ⇒ (inc . inc) 3 ⇒ inc (inc 3) ⇒ inc (1 + 3) ⇒ inc 4 ⇒ 1 + 4
.처럼 다른 함수를 가져다가 새로운 함수를 만들어주는 함수를 고차 함수 (high-order function)이라고 부른다. 여기까지 본 것처럼 Haskell은 함수를 자유자재로 손쉽게 다룰 수 있기 때문에 많은 코드를 작성하지 않아도 쉽게 프로그램을 작성할 수 있다.

람다 함수

지금까지 살펴본 함수들은 모두 a, f, factorial, add, inc처럼 이름이 있었다. 하지만 이름이 없는 함수도 가끔 필요할 때가 있다. 이런 함수를 람다 함수라고 하는 데 함수 이름 대신 \를 쓰고, = 대신 ->를 쓰면 된다.
map inc [1,2,3] ⇒ [2,3,4] map (\x-> 2*x + 1) [1,2,3] ⇒ [3,5,7]
map은 함수를 여러 개의 값에 적용시켜주는 함수다. 자세한 것은 나중에 나온다. 맨 위의 map inc는 1,2,3에 모두 1씩 더해줘서 2,3,4를 만든다. 그럼 2*x+1처럼 해주고 싶으면 어떨까. f x = 2*x + 1이라고 정의한 다음에 map f라고 해줘도 괜찮겠지만 두 번 다시 f를 사용할 일이 없다면 이름을 붙여줄 필요가 없다. 그래서 즉석에서 람다 함수를 만들어 넘겨주는 것이다.
Haskell에서 매개변수를 받는 모든 함수는 사실 람다 함수에 이름을 붙여준 것과 똑같다. 하지만 표기법이 번거롭기 때문에 지금처럼 간단하게 표기할 수 있도록 한 것이다. 람다 함수는 Haskell의 바꿔쓰기 규칙에 바탕을 이루는 기본 개념이지만 실제로는 별로 쓸 일이 없으므로 그런 것이 있다는 정도로 기억해두면 된다. 아래 코드는 위의 add 함수를 람다 함수에 이름을 붙이는 방식으로 정의한 예다.
add = (\ x y -> x + y)

Comments

댓글 보기 옵션그냥보기 - 접음그냥보기 - 펼침묶어보기 - 접음묶어보기 - 펼침 날짜 - 최근 것 우선날짜 - 예전 것 우선 10 comments per page30 comments per page50 comments per page70 comments per page90 comments per page150 comments per page200 comments per page250 comments per page300 comments per page 원하시는 댓글 전시 방법을 선택한 다음 "설정 저장"을 누르셔서 적용하십시오.
수, 2006-09-06 16:16 — philosup

오타인지 제가 잘 못알고 있는건지?

오늘 처음으로 접하게 된 언어라 여기저기 보고 있는데여기가 설명이 잘 되어 이해가 잘 되어 가던중..
plusTwo 3 ⇒ inc . inc 3 ⇒ inc (inc 3) ⇒ inc (1 + 4) => inc (1 + 3) ⇒ inc 5 => inc 4 ⇒ 1 + 5 => 1 + 4
뒤쪽에 제가 표현한데로 바뀌어야 되는게 아닌가 싶어서글을 남겨봅니다.