1강의 모든 예제는 하스켈 소스 파일에 입력하고 그 파일을 GHC로 불러와서 평가할 수 있다.
어떠한 예제든 간에 "Prelude>" 프롬프트는 소스에 포함하는 것이 아니다.
이 프롬프트에서는 GHCi 같은 환경에 코드를 입력할 수 있다는 뜻이며
그 외의 경우에는 코드를 파일에 넣고 그 파일을 실행해야 한다.
1. 변수 (Variable)
앞선 장에서 GHCi를 계산기로 활용했는데, 물론 이런 것은 짧은 계산에나 쓸모가 있고, 더 긴 계산을
하거나 하스켈 프로그램을 작성하기 위해서는 중간 결과들을 보관해줘야 한다.
중간 결과에 이름을 할당하면 그 결과를 보관할 수 있다. 이런 이름을 변수라고 부른다.
프로그램을 실행하면 각 변수는 그 변수가 가리키는 값으로 치환된다.
Prelude> 3.141592653 * 5^2
78.539816325
이 값은 반지름5인 원의 대략적인 넓이로서 A=πr2이라는 공식에 따른 것이다.
3.141592653의 자릿수들을 입력하는 것은
성가시고 처음 몇 자리를 기억하는 것 조차 귀찮은 일이다.
프로그래밍은 무의미한 반복과 암기를 기계에 위임한다.
지금 같은 경우는 하스켈은 이미 π를 수십 자리까지 보간하는 pi라는 변수를 포함한다.pi코드를 사용하면 더 깔금함 뿐만 아니라 정밀도도 높다.
Prelude> pi
3.141592653589793
Prelude> pi * 5^2
78.53981633974483
변수 pi와 그 값 3.141592653589793는 계산에서 서로 바꾸면서 사용할 수 있다.
2. 하스켈 소스 파일
두고두고 사용할 코드를 작성했으면 확장자가. hs인 하스켈 파일에 그 코드를 저장한다. 기본적으로. hs 파일은
평문(plain text)인데, 코딩에 적합한 텍스트 에디터를 사용해 이 파일들을 작성하면 된다.
괜찮은 소스 코드 에디터들을 읽고 이해하기 쉽도록 코드를 색칠하는
구문 하이라이팅(sytax highlighting)을 제공한다.
하스켈 프로그래머 사이에서는 Vim과 Emacas가 있다.
깔끔하게 정리하기 위해서는 컴퓨터에 디렉토리를 하나 만들고, 앞으로 연습할 문제들을 풀면서
하스켈 파일들을 여기에 저장해둔다.
r = 5.0
예제 파일인 Varfun.hs파일을 만들고 위 코드를 입력한다.
이 코드의 변수 r을 값 5.0으로 정의한다.
그 다음 터미널에서 GHCi를 열고 :load 명령어로 Varfun.hs를 불러와준다.
Prelude> :load Varfun.hs
[1 of 1] Compiling Main ( Varfun.hs, interpreted )
Ok, one module loaded.
:load명령어는 :l로 축소할 수 있다.
GHCi가 "Colud not find module "Varfun.hs" 같은 오류를 내뱉는 다면 GHCi를
다른 디렉토리에 실행했거나 파일을 다른 디렉토리에 저장했을 가능성이 크다.
GHCi 안에서 :cd 명령어를 사용해 디렉토리를 이동한다.
파일을 불러왔다면 GHCi의 프롬포트가 "Predule"에서 "Main"으로 변경될때
새로 정의한 r을 계산에 활용할 수 있다
*Main> r
5.0
*Main> pi * r^2
78.53981633974483
잘 알려진 공식을 사용해 반지름 5.0인 원의 넒이를 계산했다. 이것이 작동하는 이유는
우리가 Varfun.hs 파일에 r을 정의했기 때문이다. 그리고 pi는 표준 하스켈 라이브러리에 있기 때문
넒이 공식에 대한 변수 이름을 정의해 이 공식에 접근하기 더 쉽게 만들어보자.
소스 파일의 내용을 이렇게 변경해준다.
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
잠깐
let 키워드(특별한 뜻이 있는 단어)로 GHCi 프롬프트에서 소스 파일 없이 바로 변수를 정의할 수 있다.Prelude> let area = pi * 5 ^ 2편리할 때도 있지만 GHCi에서 이런 식으로 변수를 할당하는 것은 복잡한 작업에는 실용적이지 않다. 보통은 소스 파일을 사용한다.
3. 주석
소스 파일은 작동하는 코드 외에 주석을 포함할 수 있다. 하스켈에서는 주석이 두 종류 있다. --으로
시작하고 그 줄이 끝날 때 까지 이어진다.
x = 5 -- x is 5
x = 6 -- x is 6
-- z = 7 -- z is not defined.
여기서 x와 y는 실제 하스켈 코드에 정의되지만 r은 그렇지 않다.
두 번째 종류의 주석은 {- ... -}로 감싸는 것이며 여러 줄로 확장할 수 있다.
answer = 2 * {-
block comment, crossing lines and...
-} 3 {- inline comment. -} * 7
주석은 프로그램 일부를 설명하거나 문맥상 어떠한 기록을 남기기 위해 사용된다.
주석이 너무 많으면 프로그램을 읽는 것이 오히려 힘들어지기 때문에 남용을 주의해야한다.
또한 코드를 변경할 시 그에 대응하는 주석도 신중히 수정해야 한다. 낡은 주석은 큰 혼동을
일으킬 수 있기 때문이다.
4. 명령형 언어의 변수
명령형(imperative) 프로그램에 익숙한 사람은 하스켈의 변수가 C같은 언어의 변수와 상당히
다르다는 것을 눈치챘을 것이다.
여러분의 프로그래밍 경험이 없다면 이 절을 건너뛰어도 되지만.
사람들이 하스켈을 다른 프로그래밍 언어와 비교하는 경우에서 일반적인 상황을
이해하는데에는 도움이 될 것이다.
명령형 프로그래밍은 변수를 컴퓨터 메모리 안의 변경 가능 장소로 취급한다. 이 접근법은 컴퓨터의
기본 동작 원리와 연결된다. 명령형 프로그램은 컴퓨터가 할 일을 명시한다.
고수준 명령형 언어들은 직접적인 컴퓨터 어셈블리 코드와 꽤 멀어졌지만 단계별로 생각하는 방식은
동일하다. 이와 달리 함수형 프로그래밍에서는 고수준 수학적 도구를 통해 사고하는 방법을 제공하며,
변수들이 서로 어떻게 연결되는지를 정의하고 이것을 컴퓨터가 처리할 수 있는 단계별 명령으로
번역하는 일은 컴파일러에게 맡긴다.
예시를 하나 들어보면, 다음 코드는 하스켈에서 작동하지 않는다.
r=5
r=2
명령형 프로그래머는 이것을 처음에는 r = 5로 설정하고 그 다음 r = 2로 변경한다고 읽을 것이다.
하스켈에서 위 코드는 "r의 중복 선언"이라는 오류를 뱉는다.
주어진 스코프 안에서 한 하스켈 변수는 한번만 정의할 수 있으며 변경 불가능하다.
하스켈의 변수는 거의 변수가 아닌 것처럼 보이지만 사실은 수학의 변수 같은 것이다.
수학 교실에서 우리는 한 문제 안의 어떠한 변수가 그 값을 바꾸는 것은 본 적이 없을 것이다.
정확히 말하면, 하스켈 변수는 불변(immutable)이다.
하스켈 변수는 우리가 프로그램에 입력하는 데이터에 의해서만 변한다. r을 한 코드에서
두 방법으로 정의할 수 없지만 파일을 변경해 변수의 값을 바꿀 수 있다.
r = 2.0
area = pi * r ^ 2
물론 이 부분은 문제가 없다. r이 정의된 곳에서 r을 수정하면 코드의 나머지 부분인 r을
이용하는 모든 값이 자동으로 갱신된다.
현실의 하스켈 프로그램에서는 일부 변수들을 코드에 명시하지 않는다. 그 값은
프로그램의 외부 파일, 데이터베이스, 사용자 입력으로부터 데이터를 얻을 때 정의된다.
하지만 지금은 변수를 내부에 정의하는 방식을 고수한다.
외부 데이터와의 상호작용은 나중에 다룰 것이다.
다음은 명령형 언어와의 주된 차이점을 보여주는 예시이다.
r = r + 1
이 하스켈 코드는 "변수 r을 증가시키는*(즉 메모리 내 값을 갱신) 것이 아닌 r의 재귀적 정의이다."
즉 r을 이용해 r을 정의하는 것으로 재귀는 나중에 자세히 설명하겠다.
여기서 만약 r을 다른 값으로 앞서 정의할 시 r = r + 1은 오류 메시지를 내보낼 것이다.
r = r + 1은 수학적으로 말하자면 5 = 5 + 1 같은 것인데, 분명 이것은 잘못된 것이다.
변수들의 값의 프로그램 내에서 변하지 않기에 어떤 순서로든지 정의가 가능하다.
예를 들어 다음 코드는 정확히 같은 일을 한다.
y = x * 2
x = 3
x =3
y = x * 2
하스켈에는 "x를 y보다 먼저 선언한다"는 개념은 없다. 물론 y를 사용하기 위해
여전히 x의 값이 피요하지만 이는 특정 숫자 값이 필요하기 전에는
중요하지 않은 일이다.
5.함수(function)
새 원의 넒이를 구할 떄마다 프로그램을 변경하는 것은 지루하고
한 번에 원 한 개만 계산한다는 한계가 있다.
코드를 복사해 두번째 원을 위한 세 변수 r2와 area2를
만들면 원 2개를 계산할 수 있다.
r = 5
area = pi * r ^ 2
r2 = 3
area2 = pi * r ^ 2
물론 이런 생각없는 반복을 없애기 위해서는 넓이를 계산하는 함수는
하나만 있고 이 함수를 서로 다른 반지름에 적용해야 한다.
함수는 인자(argument) 값 (또는(parameter)을 취해서 결과 값을 돌려준다.
하스켈에서 함수를 정의하는 것은 변수를 정의하는 것과 비슷하지만
좌변에 함수 인자를 놓는다는 점은 다르다. 예를 들면 다음은 r이라는
인자에 의존하는 함수 area를 정의한다.
area r = pi * r ^ 2
문법을 자세히 보면 함수 이름(area)이 가장 먼저 나오고 그 다음 공백 한 칸과
인자(여기서 r)이 나온다. =의 기호 다음의 함수정의는
그 인자를 이미 정의된 다를 용어들과 함께 이용하는 하나의 공식이다.
이제 인자에 서로 다른 값들을 넣어 이 함수를 호출할 수 있다.
이 코드를 파일에 저장후 GHCi로 파일을 불러와 다음을 시도해본다.
*Main> area 5
78.53981633974483
*Main> area 3
28.274333882308138
*Main> area 17
907.9202768874502
함수에 서로 다른 반지름을 넣고 호출하면 어떤 원의 넓이든 계산이 가능하다.
우리의 함수는 수학적으로 다음과 같이 정의된다.
수학에서는 A(5) = 78.54 또는 A(3) = 28.27 처럼 매개변수를 괄호로 감싼다.
하스켈 코드에서는 괄호가 있어도 작동하지만
이 예제에서 관례에 따라 생략한다. 하스켈은 어디서든 함수를 사용하며
우리는 가능하다면 여분의 기호를 줄이고자 한다.
반드시 함께 평가해야 하는 표현식(값을 돌려주는 임의 코드)들의 경우
여전히 괄호를 사용한다.
다음 표현식들이 어떻게 서로 다른지 해석하면서 눈여겨 보자.
one = 5 * 3 + 2; -- 15 + 2 = 17 (곱셈을 덧셈보다 먼저)
two = 5 * (3+2) -- 5 * 5 = 25 (괄호)
area = 5 * 3 -- (area 5) * 3
area2 = (5 * 3) -- area 15
하스켈 함수들은 +나 *같은 모든 연산자보다 우선순위를 부여받는 것에 유의
이는 수학에서 곱셈이 덧셈보다 먼저 되는 것과 같은 방식이다.
6.평가(evaluation)
GCHi에 표현식을 입력하면 정확히 무슨 일이 일어나는 것일까?
엔터 키를 누르면 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를 빼는 함수를 작성하라.
7. 다중 매개 변수
함수는 인자를 하나보다 많이 가질 수 있다. 예를 들어 이 함수는 직사각형의
길이와 너비를 받아 그 넓이를 계산한다.
areaRect l w = l * w
이 예제는 삼각형의 넓이 (A=bh/2)를 계산한다.
areaTriangle b h = (b * h) / 2
*Main> areaTriangle 3 9
13.5
여기서 볼 수 있듯이 인수들은 공백으로 구분된다. 이것이 표현식을 묶기 위해서
괄호로 쓰기도 해야하는 이유다. 예를 들어서 x를 4제곱하려면
단순히 이렇게 쓸 수 없다.
quadruple x = double double x -- error
이러면 double이라는 함수를 두 인자 double과 x에 적용하게 된다.
함수도 다른 함수의 인자가 될 수 있다는 것을 유의.
지금까지 이 예제가 작동하기 위해서는
인자를 괄호로 둘러싸야 한다.
quadruple x = double (double x)
인자는 항상 주어진 순서대로 전달된다. 예를 들어
*Main> subtract 10 5
5
*Main> subtract 5 10
-5
여기서 subract 10 5는 10 - 5로 평가되지만 subract 5 10은 5 - 10으로 평가되는데
그 순서가 바뀌었기 때문이다.
8.함수 합성에 관해
물론 우리가 앞서 정의한 함수들을 활용해 새로운 함수를 정의할 수도 있다.
이는 덧셈 + 또는 곱셈 *와 같은 미리 정의된 함수들을
활용할 수 있는 것과 같은 원리이다. 예를 들어 정사각형의 넒이를 계산하기 위해
우리는 직사각형의 넒이를 계산하는 함수를 재활용 할 수 있다.
areaRect l w = l * w
areaSquare s = areaRect s s
*Main> areaSquare 5
25
어쨌든 정사각형은 단지 두 변의 길이가 같은 직사각형일 뿐이다.
9.지역 정의(local definition)
While 절
함수를 정의할 때 그 함수에 한정된 중간 결과를 정의하고 싶을때가 있다.
예를 들어서 삼각형의 변 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 = (a + b + c) / 2 -- a, b, and c are not defined here
변수 a,b,c가는 heron 함수의 우변에서만 이용 가능하지만 여기서 s의 정의는
home의 우변의 일부가 아니다. s를 우변의 일부로 만들기 위해서는 where키워드를 사용해야 한다.
where와 지역 정의를 공백 4개로 들여씀으로써 이어지는 정의들과 구별한 것에 주목하라.
다음은 지역 정의와 최상위 정의를 섞어 쓰는 또다른 예시다.
10.스코프(scope)
앞선 예제를 자세히 들여다보면 변수 이름 a,b,c를 두 넓이 함수에 한 번씩, 총 두 번 씩 사용했다.
이게 어떻게 가능하냐면 다음 GHCI 시퀸스를 살펴보자.
Prelude> let r = 0
Prelude> let area r = pi * r ^ 2
Prelude> area 5
78.53981633974483
앞의 let r = 0 정의 떄문에 넒이가 0을 반환했다면 불편하고 놀라운 일이었을 것이다.
그런 일이 일어나지 않은 이유는 두 번째로 r을 정의할 때는 또다를 r에 대해 말하고 있기 때문이다.
햇갈릴 수 있으나 얼마나 많은 사람이 존이라는 이름을 가지고 있는지 생각해보면
존이 한명만 있는 문맥에서 우리는 아무 혼란 없이 "존"에 대해 말할 수 있다.
프로그래밍에서도 문맥과 비슷한 스코프라는 개념이 있다.
스코프의 기술적인 면면을 당장 설명은 불가하나. 지금은 함수를 호출할 때 전달한 것이
바로 매개변수의 값이며 함수 정의 에서 변수를 어떻게 부르는 지와 무관하다는 것만 알고 있자.
그렇기 하나 변수에 적절한 고유 이름을 붙이면 독자가
코드를 해석하기 쉬워진다.
요약
- 변수는 값을 저장한다. 값은 임의의 하스켈 표현식이다.
- 변수는 스코프 내에서 변하지 않는다.
- 함수는 재사용 가능한 코드 작성을 돕는다.
- 함수는 하나보다 많은 인자를 취할 수 있다.
그리고 소스 파일 안에서 코드가 아닌 텍스트인 주석도 배웠다.
강의 자료
'백엔드 > Haskell' 카테고리의 다른 글
[Haskell] 하스켈 기초반 3강 - 타입의 기초 (0) | 2023.01.12 |
---|---|
[Haskell] 하스켈 기초반 2강 - 진위값 (0) | 2022.12.29 |
[Haskell] 하스켈 이란? 함수형 프로그래밍 (0) | 2022.04.05 |