Ocaml은 ML 계열의 언어인 Caml에 객체지향 및 모듈 시스템을 추가한 함수형 프로그래밍 언어이다.
Pattern matching
Ocaml을 포함한 대부분의 함수형 프로그래밍 언어에서, 함수는 first-class values이다.
이는 함수가 일반 값과 마찬가지로 변수에 할당될 수 있고, 함수의 인자로 전달될 수 있으며,
함수의 반환값이 될 수 있다는 것을 의미한다.
Strongly typed language
Ocaml은 변수의 타입이 강력하게 지정되어 있는 언어로, 변수의 타입을 명시적으로 지정하고, 이를 강제하며, 타입이 일치하지 않는 연산이나 할당을 방지한다.
Type inference
OCaml에서는 타입 추론 기능을 제공하는데, 타입 시스템이 변수 및 표현식의 타입을 추론할 수 있다.
Polymorphism
OCaml은 다형성을 지원하는 언어로, 데이터 타입을 정의할 때 매개변수의 타입을 일반화한 다음,
실제 타입이 결정될 때까지 일반화된 상태로 둔다.
이를 통해 일반화된 함수나 데이터 타입을 다양한 타입의 인수에 대해 동작하도록 만들 수 있다.
Pattern matching
OCaml에서는 패턴 매칭 문법을 사용하여 각각의 Case를 분석할 수 있다.
Module System
OCaml은 객체 지향 언어로, 프로그램을 여러 모듈로 나누어 구현할 수 있다.
이외에도 Data abstract, Inheritance and reuse, Separate compilation 등
일반적인 객체 지향 언어에서의 개념들도 사용할 수 있다.
OCaml의 소스 코드 확장자 : .ml
OCaml은 명시적인 main 함수가 없고, 소스코드를 순차적으로 실행
OCaml 컴파일러 : ocamlc
OCaml 빌드 시스템 : dune
int type 내장 연산자 :
Unary arithmentic : -
Binary arithmentic : +, -, *, /, mod
Unary bitwise : lnot
Binary bitwise : lsl, lsr, asl, asr, land, lor, lxor
float type 내장 연산자 :
Unary arithmentic : -.
Binary arithmentic : +., -., *., /., mod
int type <-> float type
int -> float : float_of_int
float -> int : int_of_float
Escape sequence
String type 내장 연산자
^ : String concatenation
example)
"Hello" ^ "World!" -> "HelloWorld!" : String
.[n] : Random access
example)
"HelloWorld!".[5] -> 'W' : char
유용한 String 라이브러리 모듈
String.length : 문자열 길이 반환
example)
String.length "HelloWorld!" -> 11 : int
String.sub : 서브 문자열 반환
example)
String.sub "HelloWorld!" 5 9 -> "World" : String
Bool Type을 반환하는 비교 연잔자들
x < y : x is less than y
x > y : x is greater than y
x <= y : x is less than or equal to y
x >= y : x is greater than or equal to y
x = y : x equals y (structural equality)
x <> y : x does not equals y (structural equality)
x == y : x is identical with y (physical equality)
x != y : x is not identical with y (physical equality)
Ocaml 파일을 컴파일하기 위해서는, 다음과 같은 step을 따른다.
(단, dune, Opam 등 필요한 시스템은 모두 설치되었다고 가정)
mdkir OCaml_programming
vim dune
dune 파일 내용
(include_subdirs unqualified)
(executable(name hello_world))
vim hello_world.ml
dune build hello_world.exe (* dune 파일 내부의 name과 일치해야 함 *)
_build/default/hello_world.exe
※ 다음 명령을 통해 프로그램 빌드 및 실행을 한번에 진행할 수 있다.
dune exec ./hello_world.exe (* dune 파일 내부의 name과 일치해야 함 *)
프로그래밍 언어에서,
Expression은 어떠한 value를 반환하는 코드 구문이며,
Statement는 실행 가능한 작업이나 명령어를 나타내는 코드 구문이다.
Pure functional language(순수 함수형 언어)는 Expression으로만 구성되지만,
OCaml은 Pure functional language는 아니므로 매우 제한된 형태의 Expression이 존재한다.
OCaml에서 프로그래밍을 하기 위한 기본기에 대해 정리하였다.
Ocaml에서는 multiline comment만 지원한다.
(* ... *) (* ... 를 주석 처리한다. *)
입력
가장 기본적인 입력 방법은 readline 함수를 이용하는 것이다.
readline 함수는 사용자의 입력을 받아 String 문자열로 반환한다.(linefeed 제외)
출력
가장 기본적인 출력은 Format 모듈의 printf 함수를 이용하는 것이다.
Format.printf를 이용하게 되면 출력 버퍼에 출력 내용이 저장되며,
Format 모듈의 print_flush 함수를 통해 화면에 출력할 수 있다.
flush를 하지 않으면 버퍼가 일정량 채워졌을 때 자동으로 버퍼가 비워져
원하지 않는 결과가 생길 수 있으니, 출력 시에는 flush를 해 주는 습관을 들이는 것이 좋다.
I/O example)
let () =
Format.printf "Input : "; (* Gain output buffer *)
Format.print_flush (); (* Flush output buffer *)
let x : string = readline() in (* Input *)
Format.printf "The user input is %s\n" x; (* Gain output buffer *)
Format.print_flush (); (* Flush output buffer *)
in
variable(변수)는 특정 value에 바인딩 된 식별자를 의미한다.
기본적으로, OCaml에서 모든 변수는 값 변경이 금지되며, 선언과 동시에 바인딩되어야 한다.
변수를 선언하기 위해서는 let-in expression 을 사용한다.
example)
let x : int = 3 in (* 변수 x의 type은 int, value 3을 바인딩 *)
begin
Format.printf "x : %d\n" x;
Format.print_flush ();
(* x = 4; *) (* 변수 x는 불변이므로 에러 발생 *)
end
※ 변수를 선언할 때 사용되는 '='는 할당 연산자가 아닌 IsEqualTo 비교 연산자이다.
즉, let x : int = 3;
은 int type의 변수 x와 value 3이 같다는 것을 의미한다.
※ variable shadowing :
동일 scope 내에 가장 최근에 선언된 변수가 같은 이름의 다른 변수를 가리는 것
example)
let x : int = 3 in (* 변수 x 선언, x는 3에 바인딩됨 *)
begin
let x : int = x + x in (* 새로운 변수 x 선언, x는 6에 바인딩됨 *)
begin
Format.printf "x : %d\n" x; (* "x : 6\n"이 버퍼에 저장됨 *)
Format.print_flush (); (* x : 6 출력 *)
end
Format.printf "x : %d\n" x; (* "x : 3\n"이 버퍼에 저장됨 *)
Format.print_flush (); (* x : 3 출력 *)
end
기본형 : let [variable] = [expression]
Global let binding은 값에 이름을 붙이는 definition이다.
let Binding을 통해 변수를 선언하거나 함수를 정의할 수 있다.
example)
let x : int = 5;
let y : int = 3 + 7;
let z : string = "Hello";
let w : string = "Hello" ^ " World!";
Wildcard( _ )와 함께 사용하여 expression을 실행하는 용도로도 사용할 수 있다.
Wildcard 사용 : let _ = [expression]
OCaml에서는 사용되지 않는 값을 변수에 바인딩하는 것을 금지하므로,
단순히 Expression을 사용하기 위해 let Binding을 쓸 때에는 wildcard를 사용해야 한다.
example)
let _ =
begin
Format.printf "Hello World!";
Format.print_flush ();
end
Local let-in binding은 값에 이름을 붙인 후 세부 expression을 수행하는 expression이다.
기본형 : let [variable] = [expression1] in [expression2]
variable에 expression1의 계산 결과값을 바인딩하고, expression2를 계산할 때 variable이 사용되면 바인딩되었던 값으로 치환하여 계산한다.
example)
let x : int = 3 + 6 in (* 변수 x에 3 + 6의 계산 결과값인 9를 바인딩 *)
begin
Format.printf "x : %d\n" x; (* Output buffer에 "x : 9" 를 저장 *)
Format.print_flush (); (* x : 9 출력 *)
end
let-in Binding에서도 let Binding처럼 Wildcard를 이용할 수 있다.
Wildcard 사용 : let _ = [expression1] in [expression2]
example)
let _ = Format.printf "Input : " in
let _ = Format.print_flush () in
let x : int = int_of_string (read_line ()) in
begin
Format.printf "x : %d\n" x;
Format.print_flush ();
end
OCaml에서 Sequencing이란, 여러 개의 expressions 를 순서대로 실행하는 것을 의미한다.
OCaml에서는 세미콜론으로 연결된 Expressions를 순차적으로 수행하며,
마지막 Expression 뒤에는 세미콜론을 위치시키지 않는다.
🤔 왜 마지막 Expression 뒤에는 세미콜론을 위치시키지 않는가?
세미콜론을 Expression 뒤에 사용하게 되면, 그 다음 표현식이 실행되기 전에
이전 표현식의 결과를 discard한다.
따라서, 마지막 Expression 뒤에 세미콜론을 사용하면 해당 표현식의 결과 값이 반환되지 않으므로, 대부분의 경우 마지막 Expression 뒤에는 세미콜론을 위치시키지 않는다.
example)
let add x y = x + y (* add 함수는 x + y 를 반환한다 *)
let sub x y = x - y; (* sub 함수는 결과값을 반환하지 않고 unit 타입으로 처리된다 *)
begin-end expression을 함께 사용하여 명시적으로 sequencing을 표기하는 것이 일반적이다.
( begin-end expression은 괄호와 동일한 효과를 가짐 )
example)
let x : int = 9 in
begin (* begin-end expression *)
Format.printf "x : %d\n";
Format.print_flush ();
end (* begin-end expression *)
OCaml에서 함수는 first-class values이므로, 함수 자체가 expression이자 value이다.
따라서, 함수를 다른 함수의 인자로 전달하거나 반환할 수 있다.
기본형 : fun [param_list] -> [expression]
param_list: 인자가 바인딩 될 변수들의 나열 (공백으로 구분)
expression: 함수의 몸체이며, 해당 expression의 계산 결과가 곧 return value이다.
example)
fun x y -> x + y (* 덧셈 함수 *)
let result = (fun x y -> x - y) 5 3 in (* 뺄셈 함수 *)
begin
Format.printf "result is %d\n" result; (* Output buffer에 "result is 2\n" 저장 *)
Format.print_flush (); (* result is 2 출력 *)
end
let definition 혹은 let-in expression을 활용하여 이름있는 함수를 정의할 수도 있다.
이는 문법적 설탕(Syntactic sugar)이며, 내부적으로 익명 함수를 통해 작성된다.
🤔 문법적 설탕이란?
문법적 설탕(Syntactic sugar)이란, 프로그래밍 언어에서 일부 기능이나 문법을
좀 더 직관적이고 간결하게 작성할 수 있도록 제공하는 문법적인 기능을 말한다.
즉, 문법적 설탕은 코드의 가독성을 높이고 작성 속도를 향상시키는 역할을 한다.
example)
let add x y = x + y
(* 내부적으로 let add = fun x -> fun y -> x + y 으로 구현됨 *)
let definition 사용 : let [id] [param_list] = [expression]
let-in expression 사용 : let [id] [param_list] = [expression1] in [expression2]
example)
let add x y = x + y (* add 함수 정의 *)
let sub x y = x - y in (* sub 함수 정의 *)
begin
Format.printf "5 - 2 = %d\n" (sub 5 2); (* Output buffer에 "5 - 2 = 3\n" 저장 *)
Foramt.print_flush (); (* "5 - 2 = 3" 출력 *)
end
함수의 타입은 Arrow 형태로 표기한다 : type -> type
example)
let add (x : int) (y : int) : int = x + y
(* add : int -> int -> int *)
Tuple은 연속된 값을 저장하는 자료구조로, C++ STL 및 Python의 Tuple과 동일하다.
Tuple의 타입은 *
기호를 사용하여 표기한다 : type * type
let 또는 let-in을 통해 tuple의 각 원소를 변수에 바인딩할 수 있다.
example)
let tp : int * string = (1, "hello"); (* type of tuple *)
let x, y = (1, 3); (* x에 1 바인딩, y에 3 바인딩 *)
let a, b = (2, 5) in a + b (* a에 2 바인딩, b에 5 바인딩 *)
Currying이란 다중 인자(여러 원소를 가지는 가지는 튜플)를 인자로 받는 함수를
한 번에 하나의 인자만 받는 함수로 분해하는 것을 말한다.
Functional language에서는 Curried form이 일반적이다.
example)
1.
let print ((x, y) : int * int) : unit = (* Tuples in params *)
Format.printf "%d %d\n" x y;
Format.print_flush ();
2.
let print (x : int) (y : int) : unit = (* Curried Form *)
Format.printf "%d %d\n" x y;
Format.print_flush ();
High order function이란, 함수를 인자로 넘겨받거나 함수를 반환하는 함수를 의미한다.
example)
let add x y = x + y (* add 함수 정의 *)
let print_func_result (func : int -> int) (x : int) (y : int) : unit = // 함수를 인자로 받음
let result : int = func x y in
begin
Format.printf "result : %d\n" result;
Format.print_flush ();
end
Recursive function은 자기 자신을 호출하는 재귀 함수를 의미한다.
Recursive function을 사용할 때에는, 반드시 rec 키워드와 함께 named function으로 정의해야 한다.
example)
let rec print_int_list (il : int list) : int list = (* rec 키워드 사용 *)
match il with
| [] -> []
| hd::tl ->
begin
print_int hd;
print_string " ";
print_int_list tl; (* 재귀 호출 *)
end
OCaml에서는 if-then-else expression을 통해 분기문을 작성할 수 있다.
분기문 작성 : if [expression1] then [expression2] else [expression3]
expression1
을 계산하여 true
인 경우 : expression2
를 계산한 값이 전체 expression의 값이다.
expression1
을 계산하여 false
인 경우 : expression3
를 계산한 값이 전체 expression의 값이다.
💡 OCaml은 정적 타입 언어로, 모든 expression이 하나의 type을 가져야 하므로,
expression2
와 expression3
의 타입은 동일해야 한다.