오늘은 저번 게시글에 이어서 OCaml 언어에 대해 좀 더 알아보는 시간을 가지려고 한다. 목차는 아래와 같다.
📌목차
Comment
OCaml은 다음과 같은 multiline comment만 지원한다.
(* 주석주석주석주석주석 *)
Java의 multiline comment(/* 주석주석주석주석 */) 과 동일하게 동작한다.
Standard input/output
OCaml에는 다양한 입출력 방식이 존재하나, 간단한 형태의 입출력은 다음을 활용한다.
📝 Standard input/output
🔍Standard input : OCaml의 built-in 함수 readline
▶사용자 입력을 문자열로 반환
🔍Standard output : OCaml의 build-in 모듈 Format
▶모듈 Format에는 출력에 대한 다양한 함수를 지원
아래는 표준 입출력에 대한 간단한 예제코드이다.
let _ = Format.printf "Input: " in (* Write "Input" on the output buffer *)
let _ = Format.print_flush () in (* Flush the output buffer *)
let x = read_line() in (* Get an input from the user *)
Format.printf "The user input is \"%s\"\n" x (* Write the input on the output buffer *)
Variables
OCaml에서 변수(variables)는 특정 값에 묶인(혹은 연결된)(binding) 식별자(identifier)를 나타낸다. 다른 언어에서와 달리 변수가 메모리 공간을 가리키는 것이 아니라 변수 자체가 값과 묶이게되는 개념이기 때문에 한 번 묶인 변수의 값은 변경 불가능(immutable)하다. 또한 모든 변수는 선언과 동시에 값에 묶여야만 한다.
변수의 선언 및 묶기(binding)은 let (-in) expression을 사용한다. 아래 코드를 참고하자.
let x = 3 in (* 변수 선언 및 scope 설정*)
Format.printf "x : %d\n" x
let을 통해 x라는 변수에 3이라는 값을 묶었다. 그리고 in을 통하여 x라는 변수의 scope를 설정하였다. scope란 변수를 사용할 수 있는 영역을 의미한다. 해당 영역을 벗어나면 변수 x는 사용이 불가능하다.
let (-in) binding
Global let binding은 변수 자체와 값을 연결(binding)하는 definition이다. 값에 이름을 붙인다고 생각하면 이해하기 편하다. 이전 게시글에서 expression은 값을 반환하는 코드 형태라고 설명했었는데, 해당 내용을 기억한 상태로 아래 내용을 보면 이해하기 쉽다.
📝 형태
🔍기본형 : let [variable] = [expression]
▶expression을 계산하고 계산 값을 variable에 바인딩한다.
▶이후 variable이 사용되면 해당 variable을 바인딩된 값으로 치환하여 계산한다.
▶변수 뿐 아니라 이름있는 함수를 정의할 수도 있다.
▶variable 자리에 Wildcard(_)를 사용하여 단순히 expression을 실행하는 용도로도 활용한다.
(OCaml에서는 사용하지 않는 값을 변수에 바인딩 하는 것을 금지)
🔍기본형 : let [variable] = [expression1] in [expression2]
▶expression1을 계산하고 계산 값을 variable에 바인딩한다.
▶expression2를 계산하고 계산한 결과값을 반환한다.
▶이 때, variable이 사용되면 해당 variable을 바인딩 된 값으로 치환하여 계산한다.
▶결국, 그 자체로 하나의 expression이므로 expression2의 값을 반환한다.
다음은 let binding과 let-in binding 코드 예제이다.
let x = 3 (* 3을 x에 바인딩 *)
Format.printf "x : %d", x (* x에 바인딩된 값 3으로 치환되어 계산 *)
(*
바로 위의 코드는 expression이므로 값을 반환한다.
하지만 그 값을 사용하지 않기에 아래와 같이 수정해야 한다.
*)
let _ = Format.printf "x : %d", x
let x = 3 in let y = 1 in x + y
(*
1) x에 3이 바인딩되어 (let y = 1 in x + y)에서 사용된다.
2) y에 1이 바인딩 되어 (x + y)에 사용된다.
3) 전체 코드 자체가 expression이므로 x + y 결과 값(4)을 반환한다.
*)
Sequencing
OCaml에서의 sequencing은 연속적인 unit expression의 계산을 수행한다. 여기서 unit expression이란 unit 타입을 반환하는 expression을 의미한다. 이전 게시글에서 살펴보았듯이, unit타입은 값이 없는 것을 의미한다. 따라서 unit expression은 연산을 수행하지만, 반환값이 필요없는 expression을 의미한다.
이러한 unit expression을 연속적으로 수행하게 될 경우, 반환된 값 unit에 대해 어떤 처리도 하지 않았기 때문에 OCaml에서는 오류나 경고를 준다. 따라서, 모든 unit expression에 대한 처리가 필요한데, 이는 wildcard를 이용하거나 sequencing을 이용하여 처리할 수 있다. 아래 예제를 보자.
(* Sequencing 사용 *)
Format.printf "1\n";
Format.printf "2\n";
Format.printf "3\n"
(* Wildcard 사용 *)
let _ = Format.printf "1\n"
let _ = Format.printf "2\n"
let _ = Format.printf "3\n"
또한 sequencing을 사용할 땐, 아래와 같이 begin-end expression을 함께 사용하여 명시적으로 sequencing을 표기하는 것이 일반적이다. begin-end expression은 괄호 (...)와 동일한 효과이다.
begin
Format.printf "1\n";
Format.printf "2\n";
Format.printf "3\n"
end
Functions
이전 게시글에서 알아보았듯이 OCaml에서 함수는 first-class value이다. first-class value의 특징은 다음과 같다.
📝 First-class function
🔍함수 자체가 expression
▶함수를 변수에 할당할 수 있다.
▶함수를 다른 함수의 인자로 전달할 수 있다.
▶함수가 다른 함수의 반환값이 될 수 있다.
🔍기본형 : fun [param_list] -> [expression]
▶param_list: 인자가 바인딩 될 변수들의 나열 (띄어쓰기로 구분)
▶expression: 함수의 몸체이며, 해당 expression의 계산 결과가 곧 return value
하지만 fun을 이용해서 이름있는 함수를 정의하는 것은 불가능하다. 오로지 익명(anonymous) 함수만을 정의할 수 있다. 아래 코드를 확인하자.
(* anonymous함수 정의 *)
fun x -> x + 1
(* anonymous함수를 변수에 바인딩 했을 뿐, 여전히 함수의 이름은 없음 *)
let x = (fun x -> x + 1) in
Format.printf "result : %d" (x 3)
(* 여러개의 파라미터는 아래와 같이 뛰어쓰기로 구분 *)
let f = (fun x y -> x + y) in
Format.printf "result : %d" (f 3 5)
그렇다면, 이름있는 함수를 정의하는 방법은 무엇일까?
바로, let definition 혹은 let -in expression을 활용하면 된다.
📝 이름있는 함수의 정의
🔍기본형 : let [id] [param_list] = [expression]
▶id: 함수 이름
▶param_list: 인자가 바인딩 될 변수들의 나열 (띄어쓰기로 구분)
▶expression: 함수 몸체
🔍기본형 : let [id] [param_list] = [expression1] in [expression2]
▶id: 함수 이름
▶param_list: 인자가 바인딩 될 변수들의 나열 (띄어쓰기로 구분)
▶expression1: 함수 몸체
▶expression2: 함수 정의 후 실행할 expression
이름있는 함수를 정의하고 해당 함수를 사용하는 방식은 익명함수를 정의하고 변수에 할당하여 해당 변수를 사용하는 방식과 동일한 동작을 수행한다. 아래는 코드예제이다.
(* 익명함수를 정의하고 변수에 바인딩 *)
let sum = fun x y -> x + y
let _ = Format.printf "Result: %d\n" (sum 3 7)
let sum = fun x y -> x + y in
Format.printf "Result: %d\n" (sum 3 7)
(* ---------------------------------------- *)
(* 위 코드와 동일하게 동작함 *)
(* 이름있는 함수를 정의하고 사용 *)
let sum x y = x + y
let _ = Format.printf "Result: %d\n" (sum 3 7)
let sum x y = x + y in
Format.printf "Result: %d\n" (sum 3 7)
Function type
함수의 타입은 arrow 형태로 표기한다. (type1 → type2)
type1은 파라미터의 type을 의미하며, type2는 함수 몸체 expression의 type을 의미한다.
여러 파라미터를 받는 경우, curried form으로 표기한다. curried form이란 여러개의 인자를 파라미터로 받는 경우, 순차적으로 표기하는 형태이다. 예를 들어 int type의 파라미터를 2개 받으면 int → int → type2 로 표기한다.
아래 예제를 보자. 주석을 제대로 이해하는 것이 중요하다.
(* inc : int -> int *)
let inc x = x + 1
(* sum : int -> int -> int *)
let sum x y = x + y
let sum x y = x + y in (* sum : int -> int -> int *)
let sum' = sum 1 in
(*
sum' : int -> int
원래 sum이 int → int → int 이지만,
sum'에서 인자로 1 하나만 넘기고 있다.
따라서, sum 1의 연산 결과인 y → y + 1이 새로운 함수가 된다.
이를 sum 1에 대입하면 let sum' = y → y + 1 in 이 되므로
sum'의 type : int → int
*)
let res = sum' 3 in
(*
res : int
sum'에서 설명한 주석을 이해했다면, res의 type이 왜 int 인지 쉽게 이해가 가능하다.
sum'은 y → y + 1 인데, 여기에 3을 넘겼으므로 4라는 상수 함수가 된다.
이 결과를 sum' 3에 대입하면, let rest = 4 in 이 되므로
rest의 type : int
*)
Format.printf "Result: %d\n" res (* the result is 4 *)
Tuple & List
Tuple은 연속된 값을 저장하는 자료구조이다.
📝 Tuple
🔍기본형 : any1, any2, any3 or (any1, any2, any3)
▶쉼표를 사용하여 생성한다.
🔍서로 다른 타입을 하나의 tuple에 저장할 수도 있다.
▶ex) 0, (1, 2, 3, 'c', "hi")
🔍Tuple의 타입은 *를 사용하여 표기한다.
▶ex) 0, (1, "hi") : int * (int * string) / 튜플에 괄호를 사용했을 경우, 타입을 표기할 때도 괄호 사용
🔍let 또는 let - in으로 tuple의 각 원소를 변수에 바인딩 할 수 있다.
▶ex) let x, y = (1, 3)
List는 tuple과 다르게 모든 원소의 타입이 동일하다.
📝 List
🔍기본형 : [1; 2; 3; 4]
▶대괄호([])로 리스트 생성
▶세미콜론으로 원소 구분
🔍리스트의 타입 표기 : [type] list
▶type : 원소의 타입
▶ex) int list
🔍연산자 '::'
▶리스트 앞에 원소를 삽입하여 새로운 리스트 반환
▶ex) 0 :: [1; 2; 3] --> [0; 1; 2; 3]
🔍연산자 '@'
▶두 리스트를 연결하여 새로운 리스트 반환
▶ex) [1; 2; 3] @ [4; 5; 6] --> [1; 2; 3; 4; 5; 6]
OCaml은 List에 대한 내장 라이브러리를 지원하는데, 아래는 자주 사용하게 될 함수이니 참고하자.
📝 Library for List
🔍List.map [function] [list]
▶list에 존재하는 모든 원소에 대하여 function 수행 후 새로운 리스트 반환
▶type : ('a -> 'b) -> 'a list -> 'b list
('a -> 'b)는 파라미터로 받은 함수의 타입을 의미한다.
-> 'a list는 파라미터로 받은 리스트의 타입을 의미한다.
-> 'b list는 List.map의 수행 결과의 타입을 의미한다.
🔍List.fold_left [function] [init] [list]
▶list에 존재하는 모든 원소에 대하여 왼쪽 원소부터 init과 function 수행한 후 누적 값 계산
▶type : ('b -> 'a -> 'b) -> 'b -> 'a list -> 'b
('b -> 'a -> 'b)는 파라미터로 받은 함수의 타입을 의미한다.
-> 'b는 init의 타입을 의미한다.
-> 'a list는 파라미터로 받은 리스트의 타입을 의미한다.
-> 'b는 List.fold_left의 수행 결과의 타입을 의미한다.
🔍https://caml.inria.fr/pub/docs/manual-ocaml/libref/List.html
Conditional branch
if - then - else expression을 통해 분기문을 작성한다.
📝 if - then - else
🔍 기본형 : if [expression1] then [expression2] else [expression3]
▶Expression1 을 계산
(Expression1은 반드시 bool 타입)
▶계산된 결과가 true인 경우, expression2를 계산하고 이 결과가 전체 expression의 값
▶계산된 결과가 false인 경우, expression3을 계산하고 이 결과가 전체 expression의 값
if - then - else도 그 자체로 expression이므로, 하나의 값으로 계산된다. 즉, 값을 반환하는 expression이다. 이러한 expression은 항상 하나의 타입을 가져야 하므로, expression2와 expression3에나 같은 타입의 expression이 위치해야만 한다. if - then - else 전체의 타입을 통일해야 하기 때문이다.
아래는 해당 내용을 포함한 코드 예제이다.
let is_odd x =
if x mod 2 = 0 then false (* bool *)
else 0 (* int *)
in
Format.printf "Res: %b\n" (is_odd 3)
(*
위 코드는 에러가 발생한다.
expression2와 expression3의 타입이 동일하지 않기 때문이다.
따라서 아래 코드와 같이 수정해줘야 한다.
*)
let is_odd x =
if x mod 2 = 0 then false (* bool *)
else true (* bool *)
in
Format.printf "Res: %b\n" (is_odd 3)
'CS > 프로그래밍언어개론(OCaml)' 카테고리의 다른 글
[프로그래밍언어개론] Syntax and Parsing (1) | 2025.04.01 |
---|---|
[프로그래밍언어개론] OCaml basic - 3 (0) | 2025.03.18 |
[프로그래밍언어개론] OCaml basic - 1 (1) | 2025.03.15 |