함수의 타입은 화살표 형태로 표기한다.
(* increase : int -> int *)
let increase x = x + 1
화살표 앞의 타입은 매개변수(parameter)들의 타입이고, 맨 뒤의 타입은 함수의 반환값이 무슨 타입인지가 적힌다.
굳이 앞과 맨 뒤로 나눈 이유는 매개변수가 여러개 올 수 있기 때문이다.
예를들어 변수 3개를 받아 모두 더해서 넘기는 함수를 만든다고 하자.
let sum x y z = x + y + z
이런 함수는 타입을 어떻게 적어야할까?
그냥, int,int,int -> int 이렇게 하면 되는걸까?
아쉽게도 Ocaml에서는 매개변수가 2개 이상인 경우 curried form으로 표시한다.
Curring은 N개의 매개변수를 갖는 하나의 함수가 있을때, 1개의 매개변수를 갖는 N개의 함수로 바꾸는 것을 말한다.
위의 sum함수를 예로 들어보자면, 매개변수를 3개를 가지고있기 때문에 3개의 함수로 쪼갤것이다.
다른 언어에서 사용하는 람다의 방식으로 한번에 표현해서 이해를 돕고자 한다.
int sum =
x: int -> {
y: int -> {
z: int -> {
return x + y + z
}
}
}
맨 위의 람다 함수부터 맨 아래의 람다함수까지 인자는 모두 하나씩 받는다.
차이점이 있다면 사용되는 스코프에 의해 위에서 받았던 인자를 내부의 람다 함수에서는 알 수 있다.
이제 람다함수의 인자와 반환에 대해 논해보자.
x: int -> {
y: int -> {
z: int -> {
return x + y + z
}
}
}
맨 위의 람다함수는 x를 매개변수로 갖고, 반환은 y를 매개변수로 받는 함수를 반환한다.
이해하기가 힘들 수 있는데, int형 인자를 받아서 함수를 뺀다고 이해하면 된다.
즉 function(x) = x -> function(y) 라고 할 수 있다.
y: int -> {
z: int -> {
return x + y + z
}
}
다음 함수도 y를 매개변수로 갖고, z를 매개변수로 갖는 함수를 반환한다.
따라서 function(y) = y -> function(z) 이며, 위의 x -> function(y)에서 function(y)를 치환하면
function(x) = x -> y -> function(z) 이다.
z -> {
return x + y + z
}
마지막은 쉽다.
맨 처음에 소개했던 타입으로 만들 수 있기 때문이다.
function(z) = z -> int 이다.
이제 위의 x -> y -> function(z) 에서 function(z)를 치환해보자.
따라서 최종적으로 function(x) = x -> y -> z -> int 이다.
이때 x, y, z는 모두 int형을 갖기 때문에 다시 쓰면 function(int) = int -> int -> int -> int 이다.
즉, sum 함수의 타입은 int -> int -> int -> int 이며, curried form으로 표현한 형태이다.
다시 처음으로 돌아가서 강조해보면
화살표 앞의 타입은 매개변수(parameter)들의 타입이고, 맨 뒤의 타입은 함수의 반환값이 무슨 타입인지가 적힌다.
따라서 sum 함수의 타입을 위 문장의 색깔에 맞춰서 표시하자면 int -> int -> int -> int 임을 알 수 있다.
Tuple (튜플)
튜플이란 연속된 값을 저장하는 자료구조이다.
Ocaml의 튜플은 파이썬의 튜플과 동일하다.
생성은 다음과 같이 할 수 있다.
(* 괄호가 없는 경우 *)
let tup = 1, 2, 3
(* 괄호가 있는 경우 *)
let tup2 = (1, 2, 3)
둘의 차이는 없다.
튜플은 꼭 원소들의 타입이 같지 않아도 된다.
따라서 int, char, string이 모두 들어있을 수 있다.
튜플은 타입을 다음과 같이 정의한다.
// int * char * string * float
let tup = 1, 'c' , "Hello!", 1.42356
함수의 타입 얘기를하면서 왜 갑자기 튜플이 나왔는지 의아해할 수 있다.
하나의 튜플을 인자로 받게되는 함수를 curried form으로 만들어보면서 파악해보자.
아까와 똑같이 sum함수를 정의하려한다.
근데 파라피터로 튜플 한개를 받을 생각이다.
그럼 다음과 같이 작성한다.
let sum (x, y, z) = x + y + z in
Format.printf "%d\n" (sum (1,2,3))
위 함수의 타입은 int * int * int -> int 이다.
그냥 3개의 매개변수를 갖는 함수였을때보다 형태가 간단해졌다.
또한 우리가 다른 언어에서 사용했던 형태로 매개변수에 값을 넣어줄 수 있게된다.
함수를 curried form으로 정의하느냐, tuple로 인자를 받도록 정의하느냐는 함수를 어떻게 사용하고 싶은지에 따라 선택하면된다.
함수형 언어에서는 curried form이 일반적이긴 하지만, tuple로 인자를 받아서 즉시 활용할 수 있게하는 형태로 만들 수도 있다는 것을 알아두자.
High order function
High order function이란 함수를 매개변수로 가지는 함수나, 함수를 반환하는 함수를 말한다.
함수를 매개변수로 가지게 되면, 해당 함수의 타입은 어떻게 표시해야할까?
매개변수가 함수일 경우, 매개변수의 타입은 이후의 코드에서 어떻게 사용되느냐에 따라 다르다.
하지만 공통적으로 함수는 괄호를 쳐서 표시한다.
만약 매개변수가 하나라면, "(type -> type)" 과 같이 표시한다는 의미이다.
아래는 예시이다.
// int -> int
let for_apply_func x = x + x
// (a' -> b') -> a' -> b'
let apply_func func x = func x
// int -> (int -> int)
let return_func x = fun x -> x + x
타입 추론에 대하여
Ocaml의 컴파일러는 타입 추론을 한다.
// int -> int
let for_apply_func x = x + x
// (a' -> b') -> a' -> b'
let apply_func func x = func x
위의 코드를 예시로 두고, 설명을 이어나가려 한다.
위의 apply_func 함수는 매개변수들이나 그로인한 반환값 타입은 정의만 봐서는 알 수가 없다.
근데 for_apply_func는 코드만 봐도 바로 타입 추론을 해낸다.
왜? 내가 실수를 더하면 어쩌려고?
좀 깊게 들어가자면, '+' 연산자 자체도 하나의 함수다.
'+' 연산자는 타입이 int -> int -> int 이다.
따라서 for_appy_func의 매개변수는 무조건 int형이고, 반환값도 int형임을 추론할 수 있다.
참고로 실수는 아예 다른 연산자("+.")를 사용한다.
다만 우리가 만든 apply_func 함수는 추론이 불가능하다.
그래서 타입이 정말 a' 나 b' 로 추론된다. 컴파일러도 런타임에나 알 수 있다고 말하는 것이다.
근데 들어오는 변수가 함수인건 또 어떻게 알아냈을까?
Ocaml에서는 함수의 호출은 "함수이름 인자 인자.." 식으로 한다.
함수 호출하는 형태가 하나이기 때문에 컴파일러는 사용하는 방식을 확인하고 추론을 한다.
다르게 말하자면, 잘못 작성하면 함수 호출인줄 알고 다르게 실행할 수도 있다는 말이 된다.
apply_func를 좀 변형해보자.
let appliy_func func x = (func x) + x
이러면 컴파일러는 apply_func의 타입은 뭐라고 추론할까?
(int -> int) -> int -> int 로 curried form 형태를 갖춰서 추론한다.
'언어 > Ocaml' 카테고리의 다른 글
[Ocaml] Pattern matching (0) | 2023.04.06 |
---|---|
[Ocaml] Ocaml의 컨셉에 대해 (2) | 2023.03.24 |