OCaml 튜토리얼

   

소개

프로그래밍 언어 수업 실습 시간에는 OCaml 언어를 사용합니다. 지금까지의 Python 언어와 다르게 함수형 프로그래밍 패러다임을 따라서 프로그래밍하게 됩니다. 이를 위해 할당문, 반복문, 클래스, 그리고 객체를 사용하지 않습니다.

OCaml은 정적 타입이고 타입 안전합니다. 즉, 컴파일 시간타입 오류를 찾을 수 있고, 타입마다 가능한 연산이 제한되어 있습니다. Python은 타입 안전하지만 동적 타입입니다. 즉, 실행 시간에서야 타입 오류를 찾을 수 있습니다.

   

OCaml 설치

실습을 위해 OCaml 설치를 진행합니다. 아래 안내글은 윈도우를 기반으로 작성되었습니다.

1. WSL 설치

  1. 파워쉘을 관리자 계정으로 연다.
  2. wsl –install 입력; Ubuntu(22.04)을 설치하게 됩니다.

image.png

  1. 컴퓨터를 재시작합니다.

image.png

  1. 문제가 발생하면 토글을 열고 문제를 해결하세요.
    • WSL 문제 해결(0x80370102)

      image.png

      Linux용 Windows 하위 시스템 문제 해결

      1. Virtual Machine Platform Windows 기능
      2. BIOS 가상화 기능

      image.png

      image.png

      image.png

      bios.jpg

    • WSL 문제 해결(0x8000000d)

      image.png

      1. PowerShell을 관리자 모드로 연다.
      2. wslconfig.exe /u ubuntu 명령어를 입력한다.
      3. Ubuntu를 실행한다.
  2. 유닉스 계정을 생성합니다.

image.png

image.png

2. 유닉스 개발 환경 설정

  1. APT 패키지 관리자를 활용하여 유닉스 개발 환경을 설정합니다.

     sudo apt update
     sudo apt upgrade -y
     sudo apt install -y zip unzip build-essential
    

3. OPAM 설치

  1. OPAM은 Python의 pip과 유사한 역할을 수행합니다.
sudo apt install opam
opam init --bare -a -y
opam update

4. OPAM 스위치 설정

  1. OPAM 스위치는 Python의 venv와 유사한 역할을 수행합니다. OCaml 컴파일러와 패키지를 관리합니다.
  2. 이 수업에서 사용할 스위치인 ai20049를 생성합니다.
opam switch create ai20049 ocaml-base-compiler.5.2.0 
eval $(opam env --switch=ai20049)
opam install -y utop odoc ounit2 qcheck bisect_ppx menhir ocaml-lsp-server ocamlformat

5. Visual Studio Code 설정

  1. WSL 확장을 설치한다.

    image.png

  2. Ctrl + Shift + p로 Command Palette를 연다.
  3. WSL: Connect to WSL에 연결한다.

    image.png

  4. OCaml Platform을 설치한다.

    image.png

  5. New Terminal을 연다.

    image.png

  6. utop을 입력합니다.

    image.png

    image.png

   

   

OCaml 유니버설 톱레벨

이번 OCaml 튜토리얼 세션에서는 utop을 사용합니다. utop은 Python의 IDLE와 같이 읽기-평가-출력 반복(REPL)을 수행합니다. utop을 종료하기 위해서는 Ctrl-D#quit;;를 입력합니다. utop에서 표현식은 쌍세미콜론 ;;으로 끝냅니다. 아래 코드 블록에서 42라는 표현식을 입력했고 그것을 OCaml이 해석한 것을 아래에 보여줍니다. OCaml은 입력한 값이 이름이 지정되지 않은 int 타입 값 42라고 해석합니다.

utop ### 42;;
- : int = 42

let 정의를 사용하여 값에 이름을 지정할 수 있습니다. 값 x는 42를 가지는 int 타입이라는 것을 알 수 있습니다.

utop ### let x = 42;;
val x : int = 42

함수는 다음과 같이 정의할 수 있습니다. OCaml에서는 함수도 42와 같은 값입니다. 값 incrementint 타입을 입력으로 받아 int 타입을 출력하는 함수라는 것을 알 수 있습니다.

utop ### let increment x = x + 1;;
val increment : int -> int = <fun>

OCaml에서는 함수를 아래와 같이 적용할 수 있습니다. OCaml 코딩 관례에서는 괄호가 필요하지 않다면 사용하지 않는 것을 선호합니다. 그렇기에 increment 21increment(21)보다 선호됩니다.

utop ### increment 0;;
- : int = 1
utop ### increment(21);;
- : int = 22
utop ### increment(increment 5);;
- : int = 7

utop에서 다른 파일을 불러올 수 있습니다.

mycode.ml이라는 파일을 만들고 아래 코드를 작성합니다.

let inc x = x + 1

utop에서 mycode.ml을 불러와 사용할 수 있습니다.

utop ### #use "mycode.ml";;
val inc : int -> int = <fun>
utop ### inc 3;;
- : int = 4

utop이 익숙해질 때까지 자유롭게 실습해봅니다. Python 인터프리터와 어떤 것이 다른지 생각해봅니다.

   

OCaml 프로그램 컴파일

Python과 마찬가지로 REPL 형식으로는 간단한 프로그래밍 이상을 수행할 수 없습니다. 이번에는 코드를 파일에 저장하고 컴파일하는 법을 알아봅시다.

터미널을 열고 hello-world 디렉터리를 만듭니다.

seongminkim@DESKTOP-0RHBQDL:~/ai20049$ mkdir hello-world
seongminkim@DESKTOP-0RHBQDL:~/ai20049$ cd hello-world/
seongminkim@DESKTOP-0RHBQDL:~/ai20049/hello-world$ 

hello.ml이라는 파일을 만들고 아래 코드를 입력합니다.

image.png

터미널에 아래 명령어를 입력하여 코드를 컴파일합니다.

ocamlc 컴파일러에게 hello.byte라는 이름으로 출력 파일을 생성하게 합니다.

seongminkim@DESKTOP-0RHBQDL:~/ai20049/hello-world$ ocamlc -o hello.byte hello.ml
seongminkim@DESKTOP-0RHBQDL:~/ai20049/hello-world$ ls
hello.byte  hello.cmi  hello.cmo  hello.ml
seongminkim@DESKTOP-0RHBQDL:~/ai20049/hello-world$ ./hello.byte 
Hello world!
seongminkim@DESKTOP-0RHBQDL:~/ai20049/hello-world$ 

   

Dune 빌드 시스템

Dune 빌드 시스템을 사용하면 위 과정을 더 쉽게 진행할 수 있습니다. hello.ml 코드와 같은 폴더에 dune 파일을 만들고 아래 코드를 입력하세요. 이는 Dune에게 hello라는 실행 파일을 생성하게 합니다.

(executable
(name hello))

dune-project라는 파일을 만들고 아래 코드를 입력하세요.

(lang dune 3.4)

이는 Dune에게 버전 3.4를 사용한다고 알려줍니다.

이제 dune build hello.exe 명령어를 입력하여 프로젝트를 빌드합니다. dune exec ./hello.exe 명령어를 입력하여 빌드와 실행을 동시에 수행할 수 있습니다.

dune clean 명령어를 수행하여 빌드를 삭제할 수 있습니다.

마지막으로 dune build --watch 명령어를 수행하여 소스 코드가 변경될 때마다 자동으로 빌드가 수행되게 할 수 있습니다.

   

표현식

함수형 프로그래밍 언어에서는 표현식을 값으로 계산합니다. 값은 더 이상 계산이 필요 없는 표현식을 의미합니다. 그렇기에 모든 값은 표현식이지만 그 역은 성립하지 않습니다.

원시 타입과 값

정수 타입
utop ### 26 + 1;;
- : int = 27
utop ### 26 - 1;;
- : int = 25
utop ### 32 * 2;;
- : int = 64
utop ### 32 / 2;;
- : int = 16
utop ### 17 mod 3;;
- : int = 2
실수 타입(https://en.wikipedia.org/wiki/Double-precision_floating-point_format)

OCaml은 연산자 오버로딩을 지원하지 않습니다. 그렇기에 float 타입에 대한 연산자는 int와 다르게 정의되어 있습니다. 실수 리터럴과 마찬가지로 .이 추가로 붙습니다.

utop ### 3.14 +. 2.;;
- : float = 5.14000000000000057
utop ### 3.14 -. 2.;;
- : float = 1.14000000000000012
utop ### 3.14 *. 2.0;;
- : float = 6.28
utop ### 3.14 /. 2.;;
- : float = 1.57

3.14 + 2.;; 을 실행할 수 있나요?

불리언 타입
utop ### true;;
- : bool = true
utop ### false;;
- : bool = false
utop ### true && false;;
- : bool = false
utop ### true || false;;
- : bool = true
문자 타입

Python과 다르게 char 타입과 string 타입이 구분되어 있습니다. 작은따옴표를 사용함을 유의합시다.

utop ### 'a';;
- : char = 'a'
utop ### 'b';;
- : char = 'b'
utop ### 'Z';;
- : char = 'Z'
문자열 타입

큰따옴표를 사용하여 문자열을 구성할 수 있습니다. 문자열 연결 연산자 ^를 지원합니다.

utop ### "University";;
- : string = "University"
utop ### "University" ^ " of " ^ "Seoul";;
- : string = "University of Seoul"

아래와 같이 0부터 시작하는 인덱싱으로 각각의 문자에 접근할 수 있습니다.

utop ### "abc".[0];;
- : char = 'a'
utop ### "abc".[2];;
- : char = 'c'
utop ### "abc".[3];;
Exception: Invalid_argument "index out of bounds".
타입 변환

내장 함수를 사용해서 타입 변환을 진행할 수 있습니다. 어떤 변환들이 가능한지 시도해봅니다.

utop ### char_of_int 64;;
- : char = '@'
utop ### int_of_char 'A';;
- : int = 65
utop ### float_of_int 3;;
- : float = 3.
utop ### string_of_bool false;;
- : string = "false"

동등/부등 연산자

구조적 동등성

값이 같은지 확인합니다.

  • Python

    == and !=

  • OCaml

    = and <>

물리적 동등성

같은 메모리에 존재하는지 확인합니다.

  • Python

    is and is not

  • OCaml

    == and !=

utop ### 3 = 2;;
- : bool = false
utop ### 3 <> 2;;
- : bool = true
utop ### [3] == [3];;
- : bool = false
utop ### [3] = [3];;
- : bool = true

단언

assert는 표현식을 계산한 뒤 그 값이 false라면 예외를 발생시킵니다.

utop ### assert(2=3);;
Exception: Assert_failure("//toplevel//", 1, 0).
utop ### assert(2=2);;
- : unit =()

아래와 같이 단위 테스트를 수행할 때 사용할 수 있습니다.

let() = assert(f input1 = output1)
let() = assert(f input2 = output2)
let() = assert(f input3 = output3)

if 표현식

if e1 then e2 else e3e1true이면 e2로 값이 정해지고 false라면 e3로 정해집니다. 이때 e2e3의 타입은 같아야 합니다. 우리는 e1을 if 표현식의 조건식이라고 합니다. 맨 마지막 else는 반드시 입력해주어야 합니다.

utop ### if 3 > 2 then "yeah" else "boo";;
- : string = "yeah"
utop ### let color = "red";;
val color : string = "red"
utop ### if color = "red" then 0
       else if color = "blue" then 1
       else if color = "green" then 2
       else -1;;
- : int = 0

표현식은 값을 산출하며 if 표현식도 마찬가지로 값을 산출합니다. if 문장이 아닌 if 표현식이기에 값을 산출한다는 점을 유의합시다.

utop ### let make_it_even x = x + if x mod 2 = 0 then 0 else 1;;
val make_it_even : int -> int = <fun>
utop ### make_it_even 2;;
- : int = 2
utop ### make_it_even 3;;
- : int = 4

let 표현식

지금까지의 let 사용은 let 정의입니다. 이제 let 표현식을 살펴보겠습니다. let 표현식은 값을 이름에 바인딩하고, 그 바인딩된 값을 다른 표현식을 계산하는 데 사용합니다.

(* let definition *);;
utop ### let x = 42;;
val x : int = 42
(* let expression *);;
utop ### let x = 42 in x + 1;;
- : int = 43

let 표현식은 표현식이기에 값을 산출합니다. let 정의는 값을 산출하지 않습니다.

utop ###(let x = 42) + 1;;
Error: Syntax error
utop ###(let x = 42 in x) + 1;;
- : int = 43

let x = e1 in e2에서 e1을 바인딩 표현식, e2를 본문 표현식이라고 합니다. 여기서 x는 식별자이며 보통 snake_caseCamelCase보다 선호됩니다. e1을 계산하고 그 값을 e2에서 x가 쓰이는 자리에 치환합니다.

    let x = 1 + 4 in x * 3
-->  (evaluate e1 to a value v1)
    let x = 5 in x * 3
-->  (substitute v1 for x in e2, yielding e2')
    5 * 3
-->  (evaluate e2' to v2)
    15
     (result of evaluation is v2)

스코프

아래 코드가 어떻게 실행될까요?

let x = 5 in
 ((let x = 6 in x) + x)

x에 새로운 바인딩이 생기면 기존 값은 가려집니다. 하지만 두 번째 let 표현식의 스코프에서 벗어나면 다시 기존 바인딩을 되찾게 됩니다.

let x = 5 in
 ((let x = 6 in 6) + 5)

타입 주석

(expression : type)으로 명시적으로 타입을 표기할 수 있습니다. 이는 Python의 타입 힌트와 유사합니다.

utop ### let x : int = 42;;
val x : int = 42

   

함수

함수 정의

비재귀 함수는 다음과 같이 정의합니다. 이때 따로 타입을 명시하지 않았지만, 2를 더할 수 있는 x의 타입은 int뿐이라는 정보를 사용하여 fint를 입력받아 int를 출력하는 함수라고 타입이 추론됩니다. 이를 타입 추론이라고 하며 정적 타입인 OCaml의 특성을 보여줍니다.

utop ### let f x = x + 2;;
val f : int -> int = <fun>

위에서 배운 타입 주석을 사용하여 명시적으로 타입을 명세할 수도 있습니다.

utop ### let f(x: int) : int = x + 2;;
val f : int -> int = <fun>

재귀 함수는 다음과 같이 정의합니다.

utop ### let rec sum n = if n <= 0 then 0 else sum(n-1) + n;;
val sum : int -> int = <fun>

image.png

상호 재귀 함수는 and 키워드를 사용하여 정의할 수 있습니다. even을 정의하기 위해 odd의 정의가 필요하고, odd를 정의하기 위해 even의 정의가 필요한 상황입니다.

utop ### let rec even n = n = 0 || odd(n - 1)
and odd n = n <> 0 && even(n - 1);;
val even : int -> bool = <fun>
val odd : int -> bool = <fun>

익명 함수

OCaml에서 값을 바인딩하지 않을 수 있는 것처럼 함수도 특정 이름에 바인딩하지 않을 수 있습니다. 이를 익명 함수라고 합니다. OCaml에서는 fun 키워드를 사용하여 익명 함수를 정의할 수 있습니다. 이는 식별자에 바인딩되지 않은 함수입니다. 익명 함수는 람다 표현식이라고도 불립니다.

utop ### 42;;
- : int = 42
utop ### fun x -> x + 1;;
- : int -> int = <fun>

이를 사용하여 두 가지 방식으로 함수를 정의할 수 있습니다.

utop ### let inc x = x + 1;;
val inc : int -> int = <fun>
utop ### let inc = fun x -> x + 1;;
val inc : int -> int = <fun>

파이프라인

OCaml에서는 내장 중위 연산자를 활용해서 여러 함수를 순차적으로 적용할 수 있습니다. 이를 파이프라인 연산자라고 합니다. 여러 함수를 순차적으로 적용할 때 매우 유용합니다.

utop ### let square x = x * x;;
val square : int -> int = <fun>
utop ### 5 |> inc |> square |> inc |> inc |> square;;
- : int = 1444
utop ### square(inc(inc(square(inc 5))));;
- : int = 1444

다형 함수

항등 함수를 정의해보겠습니다. 이때 'a는 알파라고 읽으며 타입 변수라고 합니다. 이 함수는 여러 타입에 사용될 수 있습니다. 이 함수를 여러 타입의 값에 적용할 수 있으므로 이를 다형 함수라고 합니다.

utop ### let identity_function x = x;;
val identity_function : 'a -> 'a = <fun>
utop ### identity_function 42;;
- : int = 42
utop ### identity_function true;;
- : bool = true
utop ### identity_function "cida";;
- : string = "cida"

아래 예제에서는 반대로 타입 변수가 OCaml의 타입 추론 알고리즘에 의해 특정 타입으로 바인딩되는 것을 볼 수 있습니다.

utop ### let increment : 'a -> 'a = fun x -> x + 1;;
val increment : int -> int = <fun>

타입 주석을 사용하여 다형 함수에 제약을 가할 수 있습니다. 가령 아래 코드는 identity_functionint에 대해서만 작동하도록 합니다.

utop ### let identity_function_int : int -> int = identity_function;;
val identity_function_int : int -> int = <fun>
utop ### identity_function_int 3;;
- : int = 3
utop ### identity_function_int false;;
Error: This expression has type bool but an expression was expected of type int

레이블/선택 인자

OCaml은 레이블 인자를 지원합니다. 이때 인자들의 순서는 중요하지 않습니다.

utop ### let build_full_name ~first_name:arg1 ~last_name:arg2 = arg1 ^ " " ^ arg2;;
val build_full_name : first_name:string -> last_name:string -> string = <fun>
utop ### build_full_name ~first_name:"Seongmin" ~last_name:"Kim";;
- : string = "Seongmin Kim"
utop ### build_full_name ~last_name: "Ko" ~first_name:"Sang-Ki";;
- : string = "Sang-Ki Ko"

위의 예제에서 first_namelast_name은 레이블이고 arg1arg2는 변수 이름입니다. 이 두 개는 같을 수 있고, 같을 때 더 쉽게 코딩할 수 있도록 문법적 설탕 형태를 지원합니다.

utop ### let build_full_name ~first_name:first_name ~last_name:last_name = first_name ^ " " ^ last_name;;
val build_full_name : first_name:string -> last_name:string -> string = <fun>
utop ### let build_full_name ~first_name ~last_name = first_name ^ " " ^ last_name;;
val build_full_name : first_name:string -> last_name:string -> string = <fun>

위의 예제에서 연결 연산자 (^)가 쓰이는 것을 토대로 OCaml은 인자들이 string이라는 것을 유추합니다. 이를 명시적으로 표기하려면 다음과 같이 할 수 있습니다.

utop ### let build_full_name ~first_name:(arg1 : string) ~last_name:(arg2 : string) = arg1 ^ " " ^ arg2;;
val build_full_name : first_name:string -> last_name:string -> string = <fun>

선택 인자는 다음과 같이 지정할 수 있습니다. 선택 인자 없이 함수를 호출했을 경우에는 기본값이 사용됩니다. 이때 선택 인자는 명시적으로 레이블을 붙여주어야 합니다.

utop ### let f ?gate:(arg1=0) arg2 = arg1 * arg2;;
val f : ?gate:int -> int -> int = <fun>
utop ### f 7;;
- : int = 0
utop ### f 1 7;;
Error: The function applied to this argument has type ?gate:int -> int
This argument cannot be applied without label
utop ### f ~gate:1 7;;
- : int = 7

부분 적용

모든 OCaml 함수는 오직 하나의 인자만을 받습니다. add_xint를 입력으로 받아 int -> int 함수를 출력합니다. 즉, int -> int -> intint -> (int -> int)로 볼 수 있습니다. 이를 통해 우리는 OCaml이 함수를 입력으로 받거나 출력할 수 있음을 알 수 있습니다.

utop ### let add x y = x + y;;
val add : int -> int -> int = <fun>
utop ### let add_x x = fun y -> x + y;;
val add_x : int -> int -> int = <fun>

다음 예제를 통해 명시적으로 이것을 확인할 수 있습니다. 이것을 부분 적용이라고 합니다.

utop ### let add x y = x + y;;
val add : int -> int -> int = <fun>
utop ### let add_5 = add 5;;
val add_5 : int -> int = <fun>
utop ### add_5 2;;
- : int = 7

함수를 연산자로 사용하기

중위 연산자는 전위 연산자로도 작동할 수 있습니다.

utop ###( + );;
- : int -> int -> int = <fun>
utop ###( ^ );;
- : string -> string -> string = <fun>
utop ###( + ) 3 4;;
- : int = 7
utop ###( ^ ) "Seongmin" "Kim";;
- : string = "SeongminKim"
utop ### let add3 =( + ) 3;;
val add3 : int -> int = <fun>
utop ### add3 2;;
- : int = 5

사용자 정의 중위 연산자를 정의할 수도 있습니다.

utop ### let( ^^ ) x y = max x y;;
val( ^^ ) : 'a -> 'a -> 'a = <fun>
utop ### 27 ^^ 51;;
- : int = 51

꼬리 재귀

수업 시간에 배웠던 다음 함수를 OCaml로 구현해보겠습니다.

image.png

utop ### let rec sum n = if n <= 0 then 0 else sum(n - 1) + n;;
val sum : int -> int = <fun>

작은 인수에 대해서는 문제없이 작동했지만 인수가 $10^9$를 넘어가니 오류가 발생합니다.

utop ### sum 10;;
- : int = 55
utop ### sum 100_000;;
- : int = 5000050000
utop ### sum 1_000_000;;
- : int = 500000500000
utop ### sum 1_000_000_000;;
Stack overflow during evaluation(looping recursion?).

이는 함수 호출을 실행할 때마다 메모리에 스택 프레임이 추가되기 때문입니다. 이 문제는 재귀 부분이 끝나고 다시 계산을 하는 부분이 있기 때문입니다. 그렇기에 현재 스택 프레임을 유지해야 합니다. 무엇을 더해야 하는지(+ n 부분)를 기억해야 하기 때문입니다.

image.png

이러한 문제는 꼬리 재귀로 해결할 수 있습니다. 이는 재귀가 끝나고 더 계산할 것이 없도록 하는 것입니다. 코드를 다음과 같이 수정해보겠습니다. 아까와는 다르게 큰 인수에 대해서도 문제를 잘 해결하는 모습을 보여줍니다.

utop ### let rec sum_helper n acc = if n <= 0 then acc else sum_helper(n - 1)(acc + n);;
val sum_helper : int -> int -> int = <fun>
utop ### let sum n = sum_helper n 0;;
val sum : int -> int = <fun>
utop ### sum 1_000_000_000;;
- : int = 500000000500000000

위 함수는 누산기 인자를 추가하여 재귀 호출이 끝난 후에 추가 계산이 필요 없도록 하였습니다. 컴파일러는 이 코드를 보고 꼬리 재귀임을 인지한 후 하나의 스택 프레임을 계속해서 사용합니다. 이 과정을 꼬리 호출 최적화라고 합니다.

   

출력

print_endline 함수는 줄바꿈 문자 \n를 포함해서 문자열을 출력합니다. 이것 말고도 print_char, print_string, print_int, print_float 등 다양한 함수들이 있습니다.

unit 타입은 오직 하나의 값 ()만을 가지며 다른 언어의 voidNone처럼 작동합니다.

utop ### print_endline "Hello, World!";;
Hello, World!
- : unit =()
utop ### print_endline;;
- : string -> unit = <fun>

세미콜론을 사용하여 unit 타입을 반환하는 함수들을 하나로 묶을 수 있습니다.

utop ### print_endline "Camels";
print_endline "are";
print_endline "bae";;
Camels
are
bae
- : unit =()

하지만 unit이 아닌 것에 세미콜론을 사용해서 묶으면 경고를 받습니다.

utop ### 3; 5;;
Line 1, characters 0-1:
Warning 10 [non-unit-statement]: this expression should have type unit.

Line 1, characters 0-1:
Warning 10 [non-unit-statement]: this expression should have type unit.

- : int = 5

이럴 때는 내장 함수인 ignore를 사용해서 해결할 수 있습니다.

utop ### ignore;;
- : 'a -> unit = <fun>
utop ###(ignore 3); 5;;
- : int = 5

Printf

Printf 모듈을 사용하면 printf 함수를 사용할 수 있습니다.

utop ### Printf.printf "%s %i %f" "string" 123 123.;;
string 123 123.000000- : unit =()

혹은 sprintf 함수로 문자열로 저장할 수 있습니다.

utop ### Printf.sprintf "%s %i %f" "string" 123 123.;;
- : string = "string 123 123.000000"

   

리스트

OCaml의 리스트는 Python과 다르게 모두 같은 타입을 가져야 합니다. []은 빈 리스트를 의미하며 nil이라고 발음합니다. 리스트와 원소가 주어졌을 때 elt :: lst로 원소를 리스트 맨 앞에 추가할 수 있습니다. 이때 ::를 cons라고 합니다. [1; 2; 3; 4]는 cons와 nil로도 쓸 수 있습니다. 그렇기에 이 문법은 문법적 설탕입니다.

utop ### [];;
- : 'a list = []
utop ### 1 :: [];;
- : int list = [1]
utop ### [1; 2; 3; 4];;
- : int list = [1; 2; 3; 4]
utop ### 1 :: 2 :: 3 :: 4 :: [];;
- : int list = [1; 2; 3; 4]

OCaml의 리스트는 연결 리스트이기에 인덱싱을 통한 접근이 불가능합니다. 그렇기에 우리는 리스트가 nil과 cons로만 이루어졌다는 것을 이용하여 패턴 매칭으로 접근합니다. 여기서 cons 연산자의 정의에 따라서 h는 원소를, t는 나머지 리스트를 의미합니다. 이때 각각의 pattern -> expression을 분기 또는 케이스라고 합니다.

utop ### let rec sum lst =
  match lst with
  | [] -> 0
  | h :: t -> h + sum t;;
val sum : int list -> int = <fun>
let rec length lst =
  match lst with
  | [] -> 0
  | h :: t -> 1 + length t
let rec append lst1 lst2 =
  match lst1 with
  | [] -> lst2
  | h :: t -> h :: append t lst2

내장 연산자 @를 사용하여 두 리스트를 이을 수 있습니다.

utop ### [1; 2] @ [3; 4];;
- : int list = [1; 2; 3; 4]
utop ###( @ ) [1; 2] [3; 4];;
- : int list = [1; 2; 3; 4]

다른 모든 것들이 OCaml에서 그러하듯이 리스트 또한 불변입니다. 리스트에 값을 추가하는 것은 사실 기존 리스트에 값을 추가한 새로운 리스트를 반환하는 것입니다.

아래와 같이 마지막 인자에 대해서 바로 패턴 매칭을 수행하는 경우 OCaml은 문법적 설탕 형태를 지원합니다.

let rec sum lst =
  match lst with
  | [] -> 0
  | h :: t -> h + sum t
let rec sum = function
  | [] -> 0
  | h :: t -> h + sum t

   

변형 타입

변형 타입은 여러 값 중에 하나의 값을 가질 수 있는 타입입니다. 이때 변형 타입 안의 값 이름을 생성자라고 합니다.

utop ### type day = Sun | Mon | Tue | Wed | Thu | Fri | Sat;;
type day = Sun | Mon | Tue | Wed | Thu | Fri | Sat
utop ### let d = Tue;;
val d : day = Tue

패턴 매칭 기능을 쓰면 생성자에 따른 동작을 쉽게 정의할 수 있습니다.

let int_of_day d =
  match d with
  | Sun -> 1
  | Mon -> 2
  | Tue -> 3
  | Wed -> 4
  | Thu -> 5
  | Fri -> 6
  | Sat -> 7
val int_of_day : day -> int = <fun>
utop ### int_of_day d;;
- : int = 3

   

레코드

레코드는 여러 타입의 데이터로 이루어집니다. 이때 레코드 안에 있는 요소들을 필드라고 합니다. student 레코드는 namegpa라는 필드를 가지고 있습니다.

utop ### type student = {name : string; gpa : float};;
type student = { name : string; gpa : float; }
utop ### let seongmin = {name  = "Seongminkim"; gpa = 2.2};;
val seongmin : student = {name = "Seongminkim"; gpa = 2.2}

레코드에서 특정 필드에 접근하기 위해서는 점 표기법을 사용합니다.

utop ### seongmin.gpa;;
- : float = 2.2

혹은 패턴 매칭을 활용할 수도 있습니다. 여기서 ng는 패턴 변수이며, 필드 이름과 같게 한다면 생략해도 괜찮습니다.

utop ### match seongmin with {name = n; gpa = g} -> g;;
- : float = 2.2
utop ### match seongmin with {name; gpa} -> gpa;;
- : float = 2.2

아래와 같이 레코드에서 특정 필드의 값을 바꾼 새 값을 만들 수도 있습니다.

utop ### let minsu = {seongmin with name = "Minsukim"};;
val minsu : student = {name = "Minsukim"; gpa = 2.2}

   

튜플

튜플은 레코드와 마찬가지로 서로 다른 타입들을 조합해서 만들어집니다. 하지만 레코드와 다르게 이름으로 요소들에 접근하는 것이 아닌 위치로 요소들에 접근합니다. 이때 타입은 각 타입의 곱으로 나타납니다. 데카르트 곱을 생각하시면 됩니다.

utop ###(1, 2, 10);;
- : int * int * int =(1, 2, 10)
utop ###(true, "Hello");;
- : bool * string =(true, "Hello")

패턴 매칭은 다음과 같이 진행할 수 있습니다.

utop ### match(1, 2, 3) with(x, y, z) -> x + y + z;;
- : int = 6

   

옵션 타입

옵션은 타입 생성자로 타입을 입력으로 받아 새로운 타입을 생산합니다. 하지만 옵션 자체는 타입이 아닙니다. 옵션으로 만들어진 타입은 두 가지 상태를 가질 수 있습니다.

utop ### Some 42;;
- : int option = Some 42
utop ### None;;
- : 'a option = None

옵션은 패턴 매칭으로 쉽게 판별할 수 있습니다.

utop ### let extract o =
    match o with
    | Some i -> string_of_int i
    | None -> "";;
val extract : int option -> string = <fun>

이러한 옵션은 다음과 같이 빈 리스트에서 최댓값을 반환하는 등의 상황에서 활용될 수 있습니다.

utop ### let rec list_max = function
    | [] -> None
    | h :: t -> begin
        match list_max t with
        | None -> Some h
        | Some m -> Some(max h m)
    end;;
val list_max : 'a list -> 'a option = <fun>

   

참고 자료