if_then_else c e1 e2 = ((c e1) e2)
true = (\x -> (\y -> x))
false = (\x -> (\ y -> y))
if_then_else true 1 2
if_then_else false 1 2
괄호를 쓰지 않더라도 함수 적용은 왼쪽에서부터 묶이며 람다계산법에서의 $\lambda$에 해당하는
\
로 정의하는 이름없는 함수는 오른쪽에서부터 묶인다.
그래서 아래와 같이 정의하더라도 위에서 정의한 것과 똑같다.
한줄 주석은 --
로 시작하며 여러 줄 주석은 {-
로 시작해서 -}
로 끝마친다.
if_then_else
의 경우처럼 함수 이름과 인자들을 =
왼쪽에 쓰고 오른쪽에 계산식을 쓰면 그것이 정의이다. true
나 false
의 경우처럼 함수 인자 없이 바로 이름만 써서 정의할 수도 있다.
IHaskell 환경에서는 =
없이 계산식만 나오면 계산식을 실행시켜 준다.
if_then_else c e1 e2 = c e1 e2 -- ((c e1) e2) 와 같다
true = \x -> \y -> x -- (\x -> (\y -> x)) 와 같다
false = \x -> \y -> y -- (\x -> (\y -> y)) 와 같다
{- 한번에 여러 개의 계산식을 실행해 보려면
아래와 같이 여러 줄에 써주면 한줄씩 처리한다.
자 이것이 여러 줄을 주석처리하는 방법이다! -}
if_then_else true 1 2
if_then_else false 1 2
인자가 여러 개인 이름없는 함수를 정의할 때 여러 번 \
를 쓰는 것이 번거로우므로 더욱 줄여 다음과 같이 써도 된다.
true = \x y -> x -- \x -> \y -> x 와 같다
false = \x y -> y -- \x -> \y -> y 와 같다
-- 이렇게 정의와 계산식을 하나의 셀에 같이 포함해도 된다
if_then_else true 1 2
if_then_else false 1 2
위의 if_then_else
도 함수 이름 다음에 인자를 써주는 대신에 람다식의 형태로도 정의할 수도 있으며 반대로 람다식으로 정의된 true
와 false
도 =
왼쪽으로 인자를 넘겨서 정의할 수 있다.
if_then_else = \c e1 e2 -> c e1 e2
true x y = x
false x y = y
if_then_else true 1 2
if_then_else false 1 2
위의 예제는 하스켈을 람다식 계산기로 활용할 수 있다는 것을 보여주기 위한 것이고 실제로 프로그램을 작성할 때 자주 쓰는 조건문과 진리값은 하스켈에서 이미 기본적으로 제공하고 있다.
if True then 1 else 2
if False then 1 else 2
하스켈 컴파일러에 어떤 정보를 문의해 알아본다거나 설정값을 바꾸거나 하는 등의
특별한 명령들은 :
로 시작한다.
예를 들면 다음과 같이 :type
이라는 명령으로 어떤 계산식의 타입이 무엇인지
하스켈 컴파일러에게 물어볼 수 있다.
:type True
:type False
진리값 상수인 True
와 False
의 타입은 Bool
이라는 것을 알 수 있다. ::
기호는 오른쪽의 계산식이 왼쪽에 있는 타입을 갖는다는 의미이다. 여태까지 타입을 하나도 적어주지 않아서 하스켈이 마치 Python이나 JavaScript 등의 동적 스크립트 언어와 같이 타입에 별로 신경쓰지 않는 언어로 오해했을 수도 있는데, 하스켈은 처음 설계할 때부터 정적으로 (즉, 프로그램 실행 전에) 엄격하게 타입을 검사하는 언어이다. 다만 타입을 프로그래머가 직접 적어주지 않더라도 알아서 유추하는 타입 유추 기능이 강력하기 때문에 여태까지의 프로그램에서는 타입을 전혀 표시하지 않아도 되었던 것 뿐이다. 방금 소개한 if ... then ... else
문도 타입을 엄격하게 검사하기 때문에 then
과 else
다음에 오는 식의 타입이 일치하지 않으면 다음과 같이 타입 오류가 발생한다.
if True then "hello" else (\x y -> y)
위 오류 메시지는 대략 then 다음에 오는 "hello"
가 문자열 타입(String
)이므로 else 다음에 오는 식도 문자열이어야 하는데 (\x y -> y)
는 인자를 두 개 받는 함수 타입(t0 -> t1 -> t1
)이라서 문자열 타입(String
)과 맞아떨어지지 않는다는 뜻이다.
수의 사칙연산 등도 물론 지원한다.
2 + 3 -- 덧셈
2 - 3 -- 뺄셈
2 * 3 -- 곱셈
2 / 3 -- 소수 나눗셈
2 `div` 3 -- 정수 나눗셈
2 `rem` 3 -- 정수 나머지
하스켈에서 수와 관련된 타입은 약간 복잡한 나름 이유가 있는 사정이 있다. 일단 이 과목에서 Haskell을 활용하기 시작할 때는 굳이 그것까지 알아야 할 필요는 없지만 그래도 궁금한 사람들은 아래를 실행해 보고 이게 무엇을 뜻하는지 하스켈 관련 책이나 자료를 스스로 찾아보거나 찾아봐도 잘 이해가 안되면 개인적으로 문의하시길. 나중에 관련된 사항에 대해서 한번 정리해서 설명할 예정이다.
:type (+)
:type (-)
:type (*)
:type (/)
:type div
:type rem
:type 1
:type 1.0
위와 같이 복잡한 사정에 대한 이해는 일단 접어두고, 앞에서 설명한 ::
기호를 코드상에서 써서 수의 타입을 간단하게 하나로 고정시킬 수 있다는 것은 알아두면 좋다.
:type (1 :: Int) -- 컴퓨터에서 제공되는 고정 크기 정수
:type (1.0 :: Double) -- 컴퓨터에서 제공되는 부동 소수점 수
그리고 (함수 포함) 어떤 이름을 정의할 때에도 ::
기호를 써서 직접 타입을 지정해 줄 수 있다.
x1, x2 :: Int
x1 = 3
x2 = 4
f1 :: Int -> Int -> Int
f1 x y = x^2 + y^2
:t x1
:t x2
:t f1
f1 x1 x2
f1 3.0 4.0 -- 타입 오류가 난다
data Nat = Z | S Nat deriving Show
위와 같이 새로운 데이타 타입으로 일진수 자연수를 정의할 수 있다. Nat
은 데이타 타입의 이름이다. Z
는 0을 나타내는 상수이며 S
는 오른쪽에 오는 자연수보다 하나 더 큰 수를 나타낸다. 예를 들면 (S (S (S Z))
은 3을 나타내는 일진수이다. 위의 데이타 타입 정의 끝부분에 나오는 deriving Show
는 컴파일러가 알아서 Nat
타입의 값을 출력하는 방법을 자동으로 만들어 달라는 뜻인데 아직까지는 자세한 것은 몰라도 된다. 궁금한 사람은 알아서 찾아보고 그래도 잘 모르겠으면 개인적으로 질문하시길.
:type Z
:type S
:type (S Z)
모든 일진수 자연수를 Haskell에서 제공하는 정수로 대응시킬 수 있다는 것을 수학적귀납법에 따라 증명하려면 다음과 같이 할 것이다. 일진수 자연수를 같은 값의 Haskell에서 제공하는 정수로 대응시키는 함수를 nat2int
라고 부르자. 이 때 nat2int
가 잘 정의될 수 있음을 보이는 수학적귀납법은 다음과 같다.
nat2int Z
의 값은 0으로 정의 된다.nat2int n
의 값을 구할 수 있다고 가정하면 n 다음에 오는 n보다 1만큼 큰 일진수 자연수에 대한 경우인 nat2int (S n)
을 귀납가정을 이용해 정의될 수 있음을 보이면 된다. 이것은 어렵지 않다 nat2int n
의 결과에 1을 더해주기만 하면 된다.위의 수학적귀납법스러운 구조를 Haskell 프로그램으로 아래와 같이 작성할 수 있다.
nat2int Z = 0
nat2int (S n) = 1 + nat2int n
nat2int Z
nat2int (S (S (S Z)))
위의 실행 예 중에서 nat2int (S (S (S Z)))
의 계산과정을 수식을 전개하듯 다음과 같이 손으로 풀어볼 수 있다.
nat2int (S (S (S Z)))
= 1 + nat2int (S (S Z))
= 1 + (1 + nat2int (S Z))
= 1 + (1 + (1 + nat2int Z))
= 1 + (1 + (1 + 0))
= 1 + (1 + 1)
= 1 + 2
= 3
우리들이 초등학교나 중학교 산수/수학 시간에 배운 등식을 바탕으로 수식 전개하는 익숙한 방법대로 프로그램의 실행에 대해 생각해 볼 수 있는 것이 함수형 프로그래밍의 특징이다. (어느 변수의 메모리의 내용을 변경시키는지 이런 기계중심의 사고방식이 아니라)
data List = Nil | Cons Int List deriving Show
리스트는 위와 같이 정의할 수 있다. 리스트는 빈 리스트를 나타내는 Nil
이거나 아니면 Cons
를 이용해 어떤 리스트의 맨 앞에 숫자 하나를 덧붙이면 그것이 또한 리스트가 된다. 4, 5, 6
이 차례로 들어 있는 리스트는 (Cons 4 (Cons 5 (Cons 6 Nil)))
로 나타낸다.
흥미로운 사실은 리스트의 구조가 일진수의 구조와 유사하다는 점이다. 구체적으로는 N이 Nil에, S가 Cons가 대응된다. 일진수와 리스트의 차이점은 크기 혹은 길이가 하나 더 큰 구조를 만들 때 일진수는 S
이외의 아무런 정보도 추가되지 않고 리스트는 Cons
뒤에 정수값이 하나 더 추가 정보로 따라붙는다는 점이다.
:type Nil
:type Cons
:type Cons 6 Nil
:type Cons 6
리스트의 길이를 구하는 함수 len
을 작성해 보자. 일진수와 비슷한 구조를 가졌으므로 수학적귀납법스럽게 정의할 수 있다.
귀납기초에 해당하는 Nil
의 경우 길이는 0이다. 귀납단계를 처리하기 위해서는 우선 어떤 리스트 xs
의 길이를 구할 수 있다는 귀납가정을 하고 그 가정을 바탕으로 계산하여 거기에 어떤 수 x
를 하나 덧붙인 리스트인 (Cons x xs)
의 길이를 구하는 방법을 계산식으로 나타낼 수 있으면 된다.
len Nil = 0
len (Cons x xs) = 1 + len xs
len Nil
len (Cons 4 (Cons 5 (Cons 6 Nil)))
리스트에 들어 있는 수들의 총합을 구하는 sumList
함수도 거의 같은 구조를 갖는다. 귀납가정에 해당하는 값에 1을 더하는 대신에 실제로 리스트에 들어 있는 수 x
를 더하면 된다.
sumList Nil = 0
sumList (Cons x xs) = x + sumList xs
sumList Nil
sumList (Cons 4 (Cons 5 (Cons 6 Nil)))
data Tree = Null | Node Int Tree Tree deriving Show
한 갈래로만 같은 구조가 반복될 수 있는 일진수나 리스트와 달리 이진트리는 두 갈래로 같은 구조가 반복될 수 있다.
따라서 한 갈래로만 생각하는 수학적 귀납법을 조금 더 일반화한 "구조적 귀납법"(structural induction)을 적용한다.
구조적 귀납법은 말로 설명하는 것보다 이진트리에 들어 있는 수들의 총합을 구하는 함수인
sumTree
의 정의를 살펴보면 직관적으로 이해할 수 있을 것이다. sumTree
와 앞서 정의한 sumList
의 차이점을 대조해 보라.
sumTree
에서는 귀납가정을 t1
과 t2
각각의 갈래에 들어 있는 부분 나무들에 대해 두번 적용된 것을 볼 수 있다.
이와 같이 한 갈래로만 적용하는 수학적 귀납법을 여러 갈래의 부분구조에 대한 경우까지 포함하여 일반화한 것이 구조적 귀납법이다.
sumTree Null = 0
sumTree (Node n t1 t2) = n + sumTree t1 + sumTree t2
sumTree Null
sumTree (Node 5 (Node 4 Null Null) (Node 6 Null Null))
이진트리의 높이를 구하는 height
함수를 작성해 보자.
귀납기초(induction base) 측 Null
의 높이는 0이다. 귀납단계(inductive step)인 Node n t1 t2
를 처리하기 위해서는 두 개의 귀납가정인 height t1
과 height t2
중에 더 큰 값, 즉 t1
과 t2
두 부분나무 중에 더 높은 높이값에 1을 더하면 전체 이진트리의 높이를 구할 수 있게 된다.
두 부분트리의 높이 중 더 큰 것을 선택하기 위해 max
라는 표준라이브러리 함수를 사용하였다.
height Null = 0
height (Node n t1 t2) = 1 + max (height t1) (height t2)
height Null
height (Node 5 (Node 4 Null Null) (Node 6 Null Null))
:type max
max 2 1
max 1 2
max
와 같은 함수를 정의하는 것도 어렵지 않다. max
와 똑같은 일을 하는 mymax
함수는 다음과 같이 비교 연산자와 조건문을 써서 작성 가능하다.
mymax x y = if x > y then x else y
:type mymax
:type (>)
mymax 1 2
mymax 2 1
(>)
이외에도 당연히 있어야 할 것 같은 다음 비교연산자들도 정수 등의 수 타입에 대해 정의되어 있다.
:type (>) -- 크다
:type (>=) -- 크거나 같다
:type (<) -- 작다
:type (<=) -- 작거나 같다
:type (==) -- 같다
:type (/=) -- 같지 않다