1 하스켈 기초 2 변수와 함수

Author
cablin
Date
2019-05-17 18:03
Views
713
2 변수와 함수
원문: http://en.wikibooks.org/wiki/Haskell/Variables_and_functions

변수(variable)
하스켈 소스 파일
주석(comment)
명령형 언어의 변수
함수(function)
평가(evaluation)
여러 개의 매개변수
함수 합성에 대한 의견
지역 정의(local definition)
where절
스코프(scope)
요약
노트
이 장의 모든 예제는 하스켈 소스 파일에 입력하고 그 파일을 GHC나 Hugs로 불러와서 평가할 수 있다. 입력의 시작부에 있는 "Prelude>" 프롬프트는 소스에 포함하는 게 아니다. 이 프롬프트가 보이면 GHCi 같은 환경에 코드를 칠 수 있다. 아니면 그 코드를 파일에 넣고 그 파일을 실행해야 한다.

변수(variable)
GHCi를 계산기로 활용하는 방법을 봤다. 물론 이런 것은 짧은 계산에나 쓸모가 있지 우리는 더 긴 계산을 하거나 하스켈 프로그램을 작성하기 위해서는 중간 결과들을 유지하고 싶다.

중간 결과는 변수에 저장할 수 있다. 변수는 그 이름에 의해 참조된다. 변수는 값을 포함한다. 값은 그 변수가 사용될 때 변수 이름 대신 대체되는 것이다. 예를 들어 다음 계산을 살펴보자.

ghci> 3.1416 * 5^2
78.53999999999999
이 값은 반지름 5인 원의 대략적인 넓이로, A=πr2 이라는 공식에 따른 것이다. π≒3.1416의 자릿수들을 입력하는 일이나 아예 기억하는 일부터가 성가시다. 사실 프로그래밍의 중요한 동기는 쓸모없는 반복과 기계적인 암기를 기계에 위임해 우리의 정신을 자유롭게 하고 더 흥미로운 생각들에 집중하기 위한 것이다. 지금 같은 경우 하스켈에는 이미 pi라는 변수가 포함되어 있고 이 변수는 π의 수십 자리수를 보관한다.

ghci> pi
3.141592653589793
ghci> pi * 5^2
78.53981633974483
변수 pi와 그 값 3.141592653589793는 계산할 때 상호 교환할 수 있음을 숙지할 것.

하스켈 소스 파일
두고두고 쓸 코드를 작성했으면 확장자가 .hs인 하스켈 소스 파일에 그 코드를 저장한다. 기본적으로 .hs 파일은 평문(plain text)이다. 코딩에 적합한 텍스트 에디터를 제안받고 싶다면 텍스트 에디터에 대한 위키피디아 글이 합리적인 출발점이다. 하스켈 프로그래머 사이에선 Vim과 Emacs가 인기 있다. 제대로 된 소스 코드 에디터라면 읽고 이해하기 쉽도록 코드를 색칠하는 구문 강조(syntax highlighting) 기능을 지원할 것이다.

상황을 깔끔하게 유지하기 위해, 컴퓨터에 디렉토리(즉 폴더)를 하나 만들고 앞으로 이 책에서 연습하며 만들 하스켈 파일들을 여기에 저장하라. 그 디렉토리를 HaskellWikibook 같은 것으로 부르자. 이 디렉토리에 Varfun.hs라는 새로운 파일을 만들고 다음 코드를 입력하라.

r = 5.0
이 코드는 변수 r을 5.0이 되도록 정의한다. 우리만의 변수를 설정하면 앞으로의 계산을 더 쉽게 만든다.

잠깐: 줄의 시작 부분에 공백이 없는지 확인하라. 하스켈은 공백에 민감하다.

그 다음 터미널에서 HaskellWikibook 디렉토리로 이동하여 GHCi를 열고 :load 명령으로 Varfun.hs 파일을 불러온다.

Prelude> :load Varfun.hs
[1 of 1] Compiling Main ( Varfun.hs, interpreted )
Ok, modules loaded: Main.
:load는 :l로 축약할 수 있다. (:l Vafun.hs처럼)

GHCi가 Could not find module 'Varfun.hs' 같은 오류를 내뱉는다면 잘못된 디렉토리에 온 것이다. GHCi 안에서 디렉토리를 변경하기 위해 :cd 명령을 쓸 수 있다. (예: :cd HaskellWikibook)

파일을 불러왔으면 새로 정의한 변수 r을 계산할 때 활용할 수 있다.

*Main> r
5.0
*Main> pi * r^2
78.53981633974483
반지름 5인 원의 넓이를 계산하려면 단지 r = 5.0 이라 정의하고, 원의 넓이에 대한 유명한 공식 A=πr2에 r을 쓸 수 있다. 5.0을 매번 쓸 필요가 없다. 아주 편하다!

재밌는데 다른 정의를 추가해보자. 소스 파일의 내용을 이렇게 바꿔보자.

r = 5.0
area = pi * r ^ 2
파일을 저장하고 GHCi에서 :reload(짧게는 :r) 명령을 입력하여 새로운 내용을 불러온다. (지금 우리는 최근 세션에서 이어서 하고 있는 것이다)

*Main> :reload
Compiling Main ( Varfun.hs, interpreted )
Ok, modules loaded: Main.
*Main>
이제 r과 area라는 두 변수를 쓸 수 있게 되었다.

*Main> area
78.53981633974483
*Main> area / r
15.707963267948966
잠깐

소스 파일 없이 GHCi 프롬프트에서 변수를 바로 정의하는 것도 가능하다. 자세한 것은 생략하고, let 키워드(특별한 뜻이 있는 단어)가 그러기 위한 문법이며 이렇게 생겼다.

Prelude> let area = pi * 5 ^ 2

편의를 위해 let 키워드를 이따금 이런 식으로 쓰겠지만, 좀더 복잡한 일을 할 때는 이런 것이 그다지 편하지 않다는 걸 느끼게 될 것이다. 그래서 소스 파일의 활용을 맨 처음부터 강조하는 것이다.


잠깐

GHC는 컴파일러로 쓸 수 있다. (즉 GHC를 사용해서, 하스켈 파일을 인터프리터에 의존하지 않고 실행되는 독립실행 프로그램으로 변환할 수 있다는 말이다) 그 방법은 나중에 독립실행 프로그램 장에서 설명할 것이다.

주석(comment)
계속하기 전에, 프로그램에 코드로 간주되지 않는 텍스트를 포함할 수 있다는 걸 알아두는 게 좋겠다. 바로 주석을 사용하는 것이다. 하스켈에서 주석은 --로 시작하고 줄이 끝나기 전까지 이어진다.

x = 5 -- 변수 x는 5이다.
y = 6 -- 변수 y는 6이다.
-- z = 7
이 경우 x와 y는 정의되지만 z는 정의되지 않는다. 주석은 다른 문법인 {- ... -} 을 쓰면 어디에든 넣을 수 있다.

x = {- 그냥 가능한 짓이라 해봤다. -} 5
대체로 주석은 프로그램의 일부가 그 자체로는 읽는이에게 혼란을 줄 수 있을 때 쓰인다. 하지만 남용하지는 말 것. 너무 많은 주석은 프로그램을 읽기 더 어렵게 만든다. 또한 관련된 코드를 변경할 때마다 주석도 최신화하여, 주석이 쓸모 없게 되고, 틀리고, 오해의 소지를 만들지 않도록 할 것.

명령형 언어의 변수
이미 C 같은 명령형 프로그래밍 언어에 익숙하다면 하스켈의 변수가 여러분이 알고 있는 그 변수와 상당히 다르다는 걸 눈치챘을 것이다. 이제 어떻게 다르고 왜 다른지를 설명하겠다.

프로그래밍 경험이 없다면 이 절을 건너뛰고 함수를 읽어도 좋다.

명령형 언어와 달리 하스켈의 변수는 변하지 않는다. 정의되고 나면 변수의 값은 절대 변하지 않는다. 변수는 변경 불가능하다(immutable). 예를 들어 다음 코드는 작동하지 않는다.

r = 5
r = 2
함수형 프로그래밍 언어의 변수는 컴퓨터 메모리의 변할 수 있는 위치보다는 수학의 변수와 연관이 있다. 수학 수업에서 한 문제 안에서 변수의 값이 바뀌는 걸 본 적이 없을 것이다. 비슷하게, 하스켈에서 컴파일러는 위의 코드에 오류로 반응한다. "multiple declarations of r". 컴퓨터에게 무엇을 할지 명확히 지시하는 명령형 프로그래밍에 익숙하다면, 이 코드를 먼저 r = 5로 설정하고 r = 2로 변경하라고 읽을 지도 모르겠다. 하지만 함수형 프로그래밍 언어에서는 컴퓨터의 메모리로 무엇을 할지 그 방안을 마련하는 책임이 프로그램에 있다.

다음은 명령형 언어와의 주된 차이점을 보여주는 또다른 예시다.

r = r + 1
이것은 "변수 r을 증가시킬 것"이 아니라 r을 이용해서 r을 정의하는 재귀적 정의다. (재귀recursion는 뒤에서 자세하게 설명할 것이다. 그저 명령형 언어에서 일어나는 일과는 근본적인 차이가 있다는 점만 알아두자) 어떤 값을 가지고 그 전에 r을 정의했다면, 하스켈에서 r = r + 1은 오류 메시지를 발생시킨다. 이는 수학적 문맥에서 5 = 5 + 1이라고 말하는 것과 비슷한데, 당연히 틀린 것이다.

변수의 값이 프로그램 내에서 변하지 않기 때문에 뒤죽박죽으로 정의될 수도 있다. 예를 들어 다음 코드들은 정확히 같은 일을 한다.

y = x * 2
x = 3
x = 3
y = x * 2
변수들을 원하는 순서대로 작성할 수 있는 것은 "y에 앞서 x를 선언"한다는 개념 같은 것이 없기 때문이다. 이것이 무언가를 한 번밖에 선언할 수 밖에 없는 이유이기도 하다. 가능하다면 상황이 모호해질 것이다. 물론 y를 사용하려면 x의 값이 필요하지만 여러분이 특정 숫자값이 필요하기 전까진 x의 값이 불필요하다.

변수가 변할 수 없다면 하스켈에서 도대체 무엇을 할 수 있는지 지금쯤 의문이 들 것이다. 하지만 우리를 믿어라. 이 책의 뒷부분에서 단 하나의 변수도 변경하지 않고 태양 아래 모든 프로그램을 작성할 수 있다는 것을 보여주겠다! 사실 변하지 않는 변수는 삶을 더 쉽게 만들어주는데 프로그램을 보다 예측 가능하게 만들기 때문이다. 이것이 순수 함수형 프로그래밍의 핵심이다. 여기서는 명령형 프로그래밍과 아주 다른 접근법과 사고방식을 가져야 한다.

함수(function)
이제 반지름이 서로 다른 여러 개의 원들을 가지고, 이것들의 넓이를 계산하려 한다고 가정하자. 가령 반지름이 3인 원의 넓이를 계산해보자. 방금 작성했던 프로그램에 기반해보자면, r은 이미 5로 정의되어있다. r = 3으로 변경할 수도 있지만 그러면 첫 번째 원을 계산할 수 없게 된다. 대안은 r2라는 새로운 변수를 정의하고 r2를 가지고 계산한 넓이에 대해 area2라는 또다른 변수를 정의하는 것이다.1 원이 두 개인 새로운 소스 파일은 다음과 같다.

r = 5
area = pi*r^2
r2 = 3
area2 = pi*r2^2
확실히, 원의 넓이 공식을 반복하고 있기 때문에 만족스럽지 않다. 이 쓸모없는 반복을 없애고, 한 번만 작성하고 서로 다른 반지름에 적용하고 싶다. 함수가 바로 이를 가능케 한다.

함수는 인자값(또는 매개변수)을 취해 결과값을 돌려준다(본질적으로 수학의 함수와 같은 것이다). 하스켈에서 함수를 정의하는 것은 쉽다. 변수를 정의하는 것과 비슷한데, 함수의 인자를 좌변에 놓아야 한다. 예를 들어 다음은 우리가 r이라고 명명한 인자를 가지고 넓이를 계산하는 함수의 정의다.

area r = pi * r^2
문법을 자세히 보자. 함수 이름이 맨 처음에 온다(여기서는 area). 그 다음 공백이 오고 그 다음은 인자(여기서는 r)다. = 기호 다음의 함수 정의는 한 공식으로서 그 인자를 이미 정의된 다른 용어들과 함께 사용한다.

이제 이 함수를 호출할 때 인자에 서로 다른 값들을 넣을 수 있다. 이 코드를 파일에 저장하고 GHCi로 파일을 불러와서 다음을 시도해보라.

*Main> area 5
78.53981633974483
*Main> area 3
28.274333882308138
*Main> area 17
907.9202768874502
여러 반지름을 가지고 이 함수를 호출하여 임의의 반지름을 가지는 원의 넓이를 계산할 수 있게 되었다.

우리의 함수는 수학적으로는 다음과 같이 정의된다.

A=πr2
수학에서는 A(5) = 78.54 또는 A(3) = 28.27 처럼 매개변수를 괄호로 감싼다. 하스켈 코드는 괄호가 있어도 작동하지만 대개는 생략된다. 하스켈이 함수형 언어이기 때문에 우리는 늘 함수를 사용할 것이고, 가능하면 부수적인 기호들을 최소화할 것이다.

괄호는 표현식(값을 돌려주는 임의의 코드)을 묶어 함께 평가할 때에도 사용된다. 다음의 표현식들이 어떻게 다르게 표현되는지 주목하라.

5 * 3 + 2 -- 15 + 2 = 17 (곱셈이 덧셈보다 우선한다)
5 * (3 + 2) -- 5 * 5 = 25 (괄호 덕분에)
area 5 * 3 -- (area 5) * 3
area (5 * 3) -- area 15
하스켈 함수들이 +나 * 같은 연산자들의 우선순위를 어떻게 취급하는지에 주목하라. 예를 들어 수학에서와 같이 곱셈은 덧셈보다 먼저 수행된다.

평가(evaluation)
GHCi에 표현식을 입력하면 어떤 일이 일어나는지 정확히 이해해보자. 엔터 키를 누르고 나면 GHCi는 여러분이 준 표현식을 평가한다. 이 말은 각각의 함수를 그 정의로 치환하고 단일 값이 남을 때까지 결과를 계산한다는 뜻이다. 예를 들어 area 5의 평가는 다음과 같이 진행된다.

area 5
=> { replace the left-hand side area r = ... by the right-hand side ... = pi * r^2 }
pi * 5^2
=> { replace pi by its numerical value }
3.141592653589793 * 5^2
=> { apply exponentiation (^) }
3.141592653589793 * 25
=> { apply multiplication (*) }
78.53981633974483
여기서 볼 수 있듯이 함수를 적용(apply)하거나 호출(call)한다는 것은 함수 정의의 좌변을 우변으로 치환한다는 것을 뜻한다. 마지막 단계로 GHCi는 최종 결과를 화면에 출력한다.

여기 함수가 몇 개 더 있다.

double x = 2*x
quadruple x = double (double x)
square x = x*x
half x = x / 2
연습문제

GHCi가 quadruple 5를 어떻게 평가하는지 설명하라.
인자의 절반에서 12를 빼는 함수를 작성하라.
여러 개의 매개변수
물론 함수는 인자를 하나보다 많이 가질 수 있다. 예를 들어 이 함수는 직사각형의 길이와 너비로 그 넓이를 계산한다.

areaRect l w = l * w
*Main> areaRect 5 10
50
이 예제는 삼각형의 넓이 (A=bh2)를 계산한다.

areaTriangle b h = (b * h) / 2
*Main> areaTriangle 3 9
13.5
여기서 볼 수 있듯이 인수들은 공백으로 구분된다. 이것이 가끔 표현식을 묶기 위해 괄호를 써야 하는 이유다. 예를 들어 x를 네제곱하려면 단순히 이렇게 쓸 수는 없다.

quadruple x = double double x
이것은 double이라는 함수를 두 인자 double과 x에 적용한다는 뜻이다. 함수도 다른 함수의 인자가 될 수 있다(뒤에서 그 이유를 볼 것이다). 대신 인자를 괄호로 둘러싸야 한다.

quadruple x = double (double x)
인자는 항상 주어진 순서대로 전달된다. 예를 들어

subtract x y = x - y
*Main> subtract 10 5
5
*Main> subtract 5 10
-5
여기서 subtract 10 5 는 10 - 5 로 평가되지만 subtract 5 10은 5 - 10 으로 평가되는데 그 순서가 바뀌었기 때문이다.

연습문제

상자의 부피를 계산하는 함수를 작성하라.
기자(Giza)에 있는 유명한 피라미드를 이루는 돌은 대략 몇 개일까? 힌트: 피라미드의 부피와 각각의 블록의 부피를 추정해야 한다.
함수 합성에 대한 의견
이미 정의한 함수를 가지고 새로운 함수를 정의할 수 있다는 건 두 말할 필요도 없다. 덧셈 (+) 또는 곱셈 (*) 같은 미리 정의된 함수들을 이용할 수 있는 것처럼 말이다(하스켈에서는 연산자가 함수로 정의된다). 예를 들어 정사각형의 넓이를 계산할 때 직사각형의 넓이를 계산하는 함수를 재활용할 수 있다.

areaRect l w = l * w
areaSquare s = areaRect s s
*Main> areaSquare 5
25
무엇보다 정사각형은 변의 길이가 같은 직사각형일 뿐이다.

이 원칙이 별 쓸모 없어 보이겠지만 정말 강력한 것으로, 특히 숫자 말고 다른 객체를 가지고 계산하기 시작할 때 그렇다.

연습문제

원통의 부피를 계산하는 함수를 작성하라. 원통의 부피는 원 모양의 바닥의 넓이(이 장에서 이미 작성한 함수이니 재활용하라) 곱하기 높이다.

지역 정의(local definition)
where절
함수를 정의할 때, 그 함수에 한정된 중간 결과를 정의하는 것은 흔한 일이다. 예를 들어 삼각형의 변 a, b, c로 그 삼각형의 넓이를 구하는 헤론의 공식을 고려해보자.

heron a b c = sqrt (s*(s-a)*(s-b)*(s-c))
where
s = (a+b+c) / 2
변수 s는 삼각형의 둘레의 절반인데 제곱근 함수인 sqrt의 인자로 s를 네 번을 쓰는 것은 지루한 일이다.

정의를 순서대로 쓰는 것은 먹히지 않는다...

heron a b c = sqrt (s*(s-a)*(s-b)*(s-c)) -- 여기선 s가 정의되지 않음
s = (a+b+c) / 2 -- 여기선 a, b, c가 정의되지 않음
...변수 a, b, c가 heron 함수의 우변에서만 이용 가능하지만 s의 정의는 heron의 우변의 일부가 아니다. s를 우변의 일부로 만들려면 where 키워드를 사용해야 한다.

where와 지역 정의를 둘 다 공백 4개로 들여써서, 이어지는 정의들과 구별한 것에 주목하라. 다음은 로컬 정의와 최상위 정의가 섞인 또다른 예시다.

areaTriangleTrig a b c = c * height / 2 -- 삼각함수 활용
where
cosa = (b^2 + c^2 - a^2) / (2*b*c)
sina = sqrt (1 - cosa^2)
height = b*sina
areaTriangleHeron a b c = result -- 헤론의 공식 활용
where
result = sqrt (s*(s-a)*(s-b)*(s-c))
s = (a+b+c)/2
스코프(scope)
이전의 예제를 자세히 들여다보면 변수 이름으로 a, b, c를 두 넓이 함수에 한 번씩, 총 두 번씩 사용했다는 것을 알아챘을 것이다. 이게 어떻게 작동하는 걸까?

다행히도 다음의 코드에는 안 좋은 쪽으로 놀라운 점은 없다.

Prelude> let r = 0
Prelude> let area r = pi * r ^ 2
Prelude> area 5
78.53981633974483
"의외의 놀람"이라 함은 let r = 0 정의가 끼어들어서 넓이가 0이 되는 그런 일이다. 이런 일이 일어나지 않는 이유는 두 번째로 r을 정의할 때는 다른 r을 말하기 때문이다. 현실에서도 일어나는 일이다. 이름이 John인 친구가 얼마나 많은가? 이름이 John인 사람들에 대해 흥미로운 점은 대부분의 경우 친구들에게 "John"에 대해 말할 수 있고 문맥에 따라 친구들은 여러분이 말하는 John이 누구를 말하는지 알 것이라는 점이다. 프로그래밍에도 문맥과 비슷한 스코프라는 개념이 있다.

스코프에 감춰진 기술적인 면을 설명하지는 않을 것이다(적어도 지금은). 엄밀히 말해 매개변수의 값은 함수를 호출할 때 여러분이 전달한 그것이고, 함수의 정의에서 말하는 그 변수와는 무관하다는 것만 숙지할 것.

요약
변수는 값을 저장한다. 사실 변수는 임의의 하스켈 표현식을 저장한다.
변수는 변하지 않는다.
함수는 재사용 가능한 코드 작성을 돕는다.
함수는 하나보다 많은 인자를 취할 수 있다.
그리고 주석은 소스 파일 안에서 코드가 아닌 텍스트라는 것도 배웠다.

노트
이 예제에서 볼 수 있듯이, 변수의 이름에 문자뿐 아니라 숫자도 들어갈 수 있다. 변수는 소문자로 시작해야 하지만 나머지는 문자, 숫자, 밑줄(_), 따옴표(') 무엇이든 가능하다. ↩

마지막 편집일시 : 2017년 10월 13일 7:55 오전

이상, https://wikidocs.net/1570 인용
Total 0

Total 3
Number Title Author Date Votes Views
3
1 하스켈 기초 2 변수와 함수
cablin | 2019.05.17 | Votes 0 | Views 713
cablin 2019.05.17 0 713
2
1 하스켈 기초 1 환경갖추기
cablin | 2019.05.08 | Votes 0 | Views 856
cablin 2019.05.08 0 856
1
하스켈 언어 알아보기
cablin | 2019.05.08 | Votes 0 | Views 1002
cablin 2019.05.08 0 1002