계정: 로그인
AA 📝
3. 함수

A Gentle Introduction to Haskell, Version 98
이전 다음 위로


Haskell은 함수형 언어기 때문에 함수가 중요한 역할을 하리라 생각할 것이며, 실제로 그렇습니다. 이번 장에서는 Haskell 함수의 몇가지 측면을 살펴봅니다.

첫째로, 두 인수를 더하는 함수의 정의를 생각해봅시다:

   1 add        :: Integer -> Integer -> Integer
   2 add x y    =  x + y

이것은 커리 (curried) 함수의 예입니다. (Curry라는 이름은 이 발상을 퍼뜨린 학자 Haskell Curry의 이름에서 따왔습니다.) Uncurried 함수의 효과를 얻기 위해 다음과 같이 튜플을 사용할 수 있습니다:

   1 add (x,y)    =  x + y

하지만 이렇게 하면 add 함수의 이번 정의는 실제로는 단지 하나의 인수만을 받아들이는 함수임을 알 수 있습니다!) 함수 add를 적용하는 형식은 add e1 e2이고, 함수의 적용은 왼쪽부터 결합하기 때문에 이는 다시 (add e1) e2와 동일합니다. 다시 말해서, 함수 add를 하나의 인수에만 적용하면 이것은 두 번째 인수에 적용될 수 있는 새로운 함수가 됩니다. ->는 오른쪽부터 결합하기 때문에, 이것은 add의 타입인 Integer->Integer->IntegerInteger->(Integer->Integer)와 동일하다는 사실과 상통합니다. 사실, 함수 add를 사용하면 함수 inc를 종전과는 다른 방식으로 정의할 수 있게됩니다:

   1 inc    =  add 1

이것이 커리 함수의 부분 적용(partial application)의 한 예이고, 함수가 반환값으로 리턴될 수 있는 방법 중 하나입니다. 함수를 인수로 전달하는 경우를 생각해봅시다. 잘 알려진 map 함수가 완벽한 예입니다:

   1 map             :: (a->b) -> [a] -> [b]
   2 map f  []       =  []
   3 map f (x:xs)    =  f x : map f xs

함수 적용은 어떠한 중위 연산자보다도 연산 우선 순위가 높습니다. 따라서 두 번째 등식의 우변은 (f x) : (map f xs)로 파싱됩니다.

함수 map은 polymorphic하며, 그 타입을 통해 첫 번째 인수가 함수라는 사실을 명백하게 알 수 있습니다. 여기서 물론 두 개의 a들은 같은 타입이어야 합니다. (b들도 마찬가지.) 함수 map을 사용하는 예로서 리스트의 각 원소들을 모두 1씩 증가시켜봅니다:

이런 예들은 함수가 first-class라는 사실을 보여주는 것이며, 이렇게 사용되는 경우에는 흔히 고계 (higher-order) 함수라고 부릅니다.

3.1 λ Abstractions

함수를 정의할 때 등식 대신 λ abstraction을 통해 ‘익명으로’ 정의할 수도 있습니다. 예를들어 inc와 똑같은 함수를 \x -> x+1이라고 쓸 수도 있습니다. 마찬가지로 함수 add\x -> \y -> x+y와 동일합니다. 이렇게 중첩된 λ abstraction을 짧게 표현하는 표기로 \x y -> x+y가 있습니다. 사실, 등식:

   1 inc x      =  x+1
   2 add x y    =  x+y

들은 아래 등식들의 축약형입니다:

   1 inc    =  \x   -> x+1
   2 add    =  \x y -> x+y

이런 동일성에 대해서는 나중에 더 이야기하겠습니다.

일반화하여 말하자면, t1 타입의 xt2 타입의 exp가 주어졌을 때 \x->exp의 타입은 t1->t2입니다.

3.2 중위 연산자

중위 연산자는 실제로는 단지 함수일 뿐이며 등식을 써서 정의할 수 있습니다. 예를 들어, 리스트 결합 연산자의 정의는:

   1 (++)            :: [a] -> [a] -> [a]
   2 []     ++ ys    =  ys
   3 (x:xs) ++ ys    =  x : (xs++ys)

구문구조상, 알파벳과 숫자로 이루어진 보통의 identifier들과는 반대로, 중위 연산자들은 완전히 ‘기호’로만 이루어져 있습니다. (§2.4) 중위일 수도 있고 전위일 수도 있는 뺄셈 기호 (-)만 제외하고, Haskell에는 일체의 전위 연산자가 없습니다.

또다른 예로서, 함수에 대한 중요한 중위 연산자로서 함수 합성 연산자가 있습니다:

   1 (.)      :: (b->c) -> (a->b) -> (a->c)
   2 f . g    =  \x -> f (g x)

3.2.1 섹션 (Sections)

중위 연산자는 함수일 뿐이기 때문에, 이들도 역시 부분적으로 적용할 수 있습니다. Haskell에서 중위 연산자의 부분 적용을 섹션(section)이라고 합니다. 예를 들어:

반드시 괄호를 붙여야만 합니다.

위 섹션들 중 마지막 형태는 중위 연산자를 강제로 동치의 함수로 바꿔버리며, map (+) [1,2,3]의 경우 처럼 중위 연산자를 함수의 인수로 전달하려고할 때 유용합니다. (이 결과가 함수들의 리스트라는 점을 알 수 있어야합니다!) 앞에서 든 (++)(.)의 예에서처럼 함수의 타입 시그너춰를 적어줄 때에도 필요합니다.

이제 앞에서 정의했던 add는 그저 (+)일 뿐이고 inc는 그저 (+1)일 뿐임을 알 수 있습니다! 실제로 이렇게 정의해도 됩니다:

   1 inc    =  (+ 1)
   2 add    =  (+)

중위 연산자를 함수로 바꿀 수는 있는데, 그럼 그 반대도 가능할까요? 그렇습니다. 함수에 연계(bound)된 identifier를 역따옴표로 감싸기만 하면 됩니다. 예를 들어, x `add` yadd x y와 같습니다. (작은 따옴표가 아니라 역따옴표로 감싼다는 사실에 주의하십시오. 'f'는 문자고 `f`는 중위 연산자입니다. 다행스럽게도 대부분의 아스키 터미널들은 이 두 종류의 기호를 이 문서에 사용된 글꼴보다 더 잘 구분해서 보여줍니다.) 어떤 함수들은 이렇게 함으로써 가독성이 증가됩니다. 리스트 원소 판별을 위한 내장 술어(predicate)인 elem이 좋은 예로서, x `elem` xs는 직관적으로 “x는 xs의 원소다” 라고 읽게됩니다.1

전위/중위 연산자 -가 연루된 섹션에 관해서는 특별한 규칙이 있습니다. §3.5§3.4를 보십시오.

이쯤 되면 함수를 정의하는 여러가지 방법들 때문에 혼란스러울지도 모릅니다! 이렇게 여러가지 기전들을 제공하는 것은 역사적인 관례 때문이기도 하고 일관성에 대한 바램 (예를 들어 중위 연산자와 일반 함수의 취급법에 관해서) 때문이기도 합니다.

3.2.2 결합특성 (Fixity) 선언

어떠한 중위 연산자나 생성자에도 (`elem`처럼 일반적인 identifier로부터 만들어진 것까지 포함해서) 결합특성 (fixity) 선언을 해줄 수 있습니다. 이 선언은 0부터 9까지의 우선순위 수준과 (9가 가장 우선순위가 높습니다: 함수 적용은 일반적으로 우선순위 10을 갖는다고 가정합니다) 좌, 우, 혹은 비결합 방향을 표시합니다. 예를 들어 ++.에 대한 결합특성 선언은 다음과 같습니다:

   1 infixr 5 ++
   2 infixr 9 .

이 경우 둘 다 결합 법칙 상 우측 우선 결합을 하며, 첫번째 것은 우선 순위 5, 두번째 것은 우선 순위 9입니다. 좌측 우선 결합은 infixl로 표현하고 비결합은 infix로 표현합니다. 같은 결합특성 선언으로 둘 이상의 연산자의 결합특성을 표시할 수 있습니다. 만일 어떤 연산자에 대해서 결합특성 선언이 없으면 기본적으로 infixl 9가 선택됩니다. (결합 규칙에 관한 더 상세한 정의는 §4.4.2를 참조하십시오.)

3.3 함수는 non-strict합니다.

bot이 다음과 같이 정의된다고 생각해봅시다:

   1 bot    =  bot

즉, bot은 종료되지 않는 (non-terminating) 표현식입니다. 추상화시켜서, 종료되지 않는 표현식의 을 ⊥ 로 표시하고 “bottom”이라고 읽습니다. 1/0처럼 런타임 에러 계열을 발생시키는 표현식도 바로 이 값을 갖습니다. 이런 오류는 복구할 수 없습니다: 즉, 프로그램은 이런 오류를 지나치면서 계속 수행될 수 없습니다. End-of-file 오류처럼 입출력 시스템이 만나는 오류들은 복구할 수 있고, 다른 방식으로 다루게 됩니다. (이런 입출력 오류는 사실은 결코 오류(error)가 아니며, 오히려 예외(exception)에 해당합니다. 예외에 관해서는 7장에서 훨씬 더 자세하게 다룹니다.)

만일 함수 f가 종료되지 않는 표현식에 적용됐을 때 적용 결과 역시 종료되지 않는다면 함수 fstrict하다고 말합니다. 다시 말해서, 함수 f가 strict하다면 f bot의 값은 ⊥ 이고, f bot의 값이 ⊥ 라면 함수 f는 strict합니다. 대부분의 프로그래밍 언어에서는 모든 함수가 strict합니다. 그러나 Haskell에서는 그렇지 않습니다. 간단한 예로, 다음과 같이 정의되는 상수 함수 const1을 생각해봅시다:

   1 const1 x    =  1

Haskell에서 const1 bot의 값은 1입니다. Operational하게 말하자면, const1은 그 인수의 값을 ‘필요’로하지 않기 때문에 결코 인수를 평가하려고 시도하지 않으며, 따라서 종료되지 않은 계산 따위에 빠지는 일은 절대 없습니다. 이런 이유로 non-strict 함수들은 “lazy functions”라고도 불리며, 이런 함수들은 그 인수를 ‘게으르게 (lazily)’ 혹은 ‘필요할 때에만 (by need)’ 평가한다고 말합니다.

Haskell에서는 오류(error)와 비종결값(non-terminating values)이 의미론상 같기 때문에, 윗 단락의 이야기는 오류에 대해서도 해당됩니다. 예를 들어 const1 (1/0)는 올바르게 1로 평가됩니다.

Non-strict한 함수들은 문맥을 다양화시키는 데에 유용합니다. 가장 주된 장점은 프로그래머로 하여금 평가 순서에 연연하지 않게 해준다는 점입니다. 계산하는 데에 부하가 많이 걸리는 값이라 하더라도, 만일 그 값이 필요하지 않다면, 값이 계산될지도 모른다는 우려 없이 함수에 인수로 전달할 수 있습니다. 이런 경우의 중요한 예로 무한 자료 구조가 있을 수 있습니다.

Non-strict한 함수에 대해 설명하는 또다른 방법으로서, Haskell은 다른 전통적인 언어들처럼 대입(assignments)을 통해 계산하는게 아니라, 대신 정의(definitions)를 이용해서 계산합니다. 다음과 같은 선언은:

   1 v    =  1/0

“1/0을 계산해서 그 결과를 v에 저장하라”라고 읽는게 아니라 “v를 1/0이라고 정의하노라”라고 읽어야합니다. v의 값(정의)이 필요한 경우에만 division by zero 오류가 발생합니다. 이 선언은 그 자체로는 어떠한 계산도 유발하지 않습니다. 대입을 사용하는 프로그래밍이라면 대입 순서에 무척 주의를 기울여야만 합니다: 프로그램의 의미가 대입이 실행되는 순서에 의존하고 있기 때문이죠. 반면에 정의에 의한 프로그래밍은 훨씬 단순합니다: 정의는 어떤 순서로 존재해도 프로그램의 의미에 영향을 주지 않습니다.

3.4 “무한” 자료 구조

Haskell의 non-strict한 특성이 가져다주는 장점 중 하나로 자료생성자 역시 non-strict하다는 점을 들 수 있습니다. 생성자라는 것은 결국 조금 특별한 종류의 함수일 뿐이라는 점을 생각해보면 별로 놀라운 일도 아닐겁니다. (다른 점이 있다면 생성자는 패턴 매칭에 사용될 수 있다는 정도입니다.) 예를 들어, 리스트 생성자인 (:)는 non-strict합니다.

Non-strict한 생성자는 (개념적으로) 무한한 자료 구조를 정의할 수 있게 해줍니다. 1들의 무한 리스트는 아래와 같습니다:

   1 ones    =  1 : ones

함수 numsFrom은 좀 더 재미있을겁니다:

   1 numsFrom n    =  n : numsFrom (n+1)

여기서 numsFrom nn으로 시작하고 1씩 순차적으로 증가하는 정수들의 무한 리스트입니다. 이로부터 제곱들의 무한 리스트를 만들어낼 수 있습니다:

   1 squares    =  map (^2) (numsfrom 0)

(섹션 사용법에 주목하십시오. ^는 중위 지수 연산자입니다.)

물론, 실제 연산을 위해서 상기 리스트 중 유한한 일부분을 뽑아내고싶어질 수도 있으며, Haskell에는 take라든가 takeWhile, filter처럼 이런 역할을 하는 함수들이 많이 정의되어 있습니다. Haskell에는 대규모의 내장 함수와 타입 집합이 정의되어 있으며, 이를 “Standard Prelude”라고 합니다. Haskell Report의 부록 A에는 Standard Prelude 전체가 수록되어 있습니다. 리스트에 관한 여러가지 유용한 함수들은 PreludeList 부분에 들어있습니다. 예를 들어, take는 리스트로부터 처음 n 개의 리스트를 취합니다:

위의 ones의 정의는 원형 리스트(circular list)의 예입니다. 리스트를 실제로 원형리스트로 구현할 수 있고 따라서 공간을 절약할 수 있기 때문에, 대부분의 경우 laziness는 효율성에 중요한 영향을 미칩니다.

원형 리스트를 사용하는 또다른 예로서, 피보나치 수열은 아래 무한 리스트를 통해 효율적으로 계산될 수 있습니다:

   1 fib    =  1 : 1 : [ a+b | (a,b) <- zip fib (tail fib) ]

여기서 zip은 Standard Prelude에 정의되어있는 함수로서, 두 개의 리스트 타입 인수들의 각 원소를 짝지어 리턴합니다:

   1 zip (x:xs) (y:ys)    =  (x,y) : zip xs ys
   2 zip  xs     ys       =  []

무한 리스트인 fib가 마치 자기 자신의 ‘꼬리를 물듯이’ 그 자신을 사용하여 정의되고 있음을 주목하십시오. 이 계산을 아래 그림과 같이 그려볼 수 있습니다.

그림 1

무한 리스트를 적용하는 다른 예는 4.4절에서 찾으실 수 있습니다.

3.5 Error 함수

Haskell에는 String->a 타입의 내장 함수 error가 있습니다. 조금 이상해보일 것입니다: 타입 대로라면 이 함수는 polymorphic 타입의 값을 리턴하며, 그런 타입의 값을 인수로 받지 않기 때문에 이 타입에 대해서 전혀 알 수가 없습니다!

사실, 모든 타입들이 ‘공유’하는 값이 딱 하나 있습니다: 바로 ⊥ 입니다. 실제로 의미론상 (semantically) 이 값이야말로 error 함수가 언제나 리턴하는 값입니다. (모든 오류는 ⊥ 값을 갖는다는 사실을 상기하시기 바랍니다.) 그러나, 제대로 된 구현물에서는 분석 목적을 위해 error에 준 인수 문자열을 인쇄하는 것이 좋을겁니다. 따라서 이 함수는 뭔가 ‘잘못 됐을 때’ 프로그램을 종료시키길 원할 경우 유용합니다. 예를 들어, Standard Prelude에 있는 head 함수의 실제 정의는 다음과 같습니다:

   1 head (x:xs)    =  x
   2 head  []       =  error "head{PreludeList}: head []"


  1. 이 문장의 영어 원문은 “x is an element of xs.”로서 x `elem` xs라는 코드와 직관적인 일관성이 있습니다만, 한국어로 번역해 놓으면 전혀 그렇지 않습니다. — 역자 주 (1)


이전 다음 위로
A Gentle Introduction to Haskell, Version 98

Copyright © 1999 Paul Hudak, John Peterson and Joseph Fasel

Permission is hereby granted, free of charge, to any person obtaining a copy of “A Gentle Introduction to Haskell” (the Text), to deal in the Text without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Text, and to permit persons to whom the Text is furnished to do so, subject to the following condition: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Text.