OCaml에 대해 실전 속성 압축으로 익혀보기

2025. 6. 22. 00:47·개발이야기

저는 지난 봄학기 어느 전공과목에서, OCaml이라는 언어를 이용하여 과제를 해야했습니다. 당연히 이전까지 OCaml을 몰랐고, 딱 과제를 하는데에 지장없는 수준의 실력을 갖게 된 것 같습니다. OCaml의 모든 것을 알고싶지는 않지만, 당장 OCaml로 뭔가를 해야하는 누군가에게 이 글이 도움이 되기를 바라며 작성해봅니다. 저도 절대 OCaml의 전문가가 아님을 다시 한 번 밝힙니다. 

 

여기 코드들을 VSCode에서 실행해보고 싶으시면, 이 익스텐션을 설치하시면 됩니다.

[오-카믈] 이라고 읽는듯하다. Camel과 비슷해서 낙타인가보다.

OCaml의 컨셉과 문법

함수형 프로그래밍

OCaml은 철저히 함수형 프로그래밍 언어입니다. 함수형 프로그래밍이 무엇인지도 익숙치 않을 독자를 위해 간단히 예를 들어 보겠습니다.

 

보통 일반적인 C나 python등 우리에게 익숙한 명령형 프로그래밍 언어들은 명령어의 나열식으로 코드를 작성합니다.
"일단 냄비에 물을 부어. 스프도 넣어. 불을 올리고, 물이 끓으면 면을 넣어. 3분 정도 지나면 계란을 넣고, 다시 1분 뒤에는 라면을 완성해"

 

하지만 함수형 프로그래밍 언어인 OCaml은 값 위주로 코드를 작성합니다.
"라면은 냄비에 물과 스프를 넣고 끓던 것에, 추가로 면을 3분 넣고 끓여진 것에, 추가로 계란을 넣고 1분 더 기다려진 것이야"

 

적절한 비유였는지 모르겠습니다. 핵심은 명령형 프로그래밍은 명령의 나열이라는 것입니다. 아래의 python 코드를 생각해봅시다.

a = 1
b = 6
if b > 5:
    a = a + 1

def sum(a, b):
    return (a + b)

print(f'Sum of {a} and {b} is {sum(a, b)}')

반면, 동일한 구현체를 함수형 프로그래밍 스타일의 OCaml로 작성해보겠습니다. 함수형 프로그래밍은 값.값.값.의 나열입니다.

let b = 6
let a = if b > 5 then 1 + 1 else 1

let sum x y = x + y
  
let () = Printf.printf "Sum of %d and %d is %d\n" a b (sum a b)

let은 변수의 선언입니다. OCaml에서 변수에 담긴 값은 변경할 수 없는 성질, 불변성을 갖습니다. (ref 키워드를 이용하면 변경가능한 변수를 만들수는 있으나, 함수형 프로그래밍의 철학에는 맞지 않습니다.) 그래서, 위의 Python 코드와는 달리 한 번 선언한 변수 a의 값이 변경되는 것을 허용하지 않았습니다. 또한, b의 크기 조건에 따라 a의 값을 업데이트 하는 코드 역시, a의 값을 정하는 표현식에 in-line으로 포함되었습니다.

 

함수 역시 "값" 으로 취급됩니다. Python에서는 변수와 함수를 선언하는 방법이 각각 다릅니다. 그러나, 함수형 프로그래밍에서는 함수도 값이며, 일급 함수라고 부릅니다. 위의 코드에 표현하지는 않았으나, 함수가 값이기 때문에, 어떤 함수는 인자로 다른 함수를 받을 수도 있습니다. 그 함수는 고차 함수라고 부릅니다.

 

눈여겨 볼 점은 함수 옆의 인자가 sum(x,y) 처럼 괄호로 묶이지 않고, sum x y로 나열되기만 했다는 것입니다. 보통 sum 함수의 타입 혹은 인터페이스를 표현하라고 하면, 우리는 (int, int) -> int 처럼 표현하고 생각하는 것이 익숙합니다. 그러나 OCaml에서는 int -> int -> int 로 표현됩니다. 인자1->인자2->반환값의 형태입니다. 함수형 프로그래밍에서 커링(currying)이라는 용어로 불립니다. 이것을 이해하고, 곧 설명할 OCaml의 인터페이스 선언하는 법과 모듈 시스템을 읽어주세요. 잠깐 모듈 시스템을 설명하기 전에 몇개의 OCaml 코드조각들을 더 소개하고 넘어가겠습니다.

 

패턴 매칭

let describe_list my_list =
    match my_list with
    | [] -> "비어있는 리스트"
    | [x] -> "원소가 하나"
    | x :: y :: [] -> "앞에서부터 각각 x, y 원소 두 개"
    | x :: xs -> "x는 맨앞 원소, xs는 x를 제외한 나머지 리스트"

명령형 프로그래밍의 if-else나 switch 문을 in-line으로 표현할 수 있게끔 지원하는 기능입니다. 위의 describe_list 함수는 인자로 my_list를 받아서, my_list의 특징을 설명하는 문자열을 반환합니다. 아래 코드처럼 패턴매칭에 타입을 이용할 수도 있습니다.

type shape =
    | Point
    | Circle of float
    | Rectangle of float * float

  let calculate_area s =
    match s with
    | Point -> 0.0
    | Circle r -> Float.pi *. r *. r
    | Rectangle (w, h) -> w *. h

재귀

  let rec sum list =
    match list with
    | [] -> 0
    | head :: tail -> head + (sum tail)

  let total = sum [1; 2; 3; 4; 5] (* 결과: 1 + (2 + (3 + (4 + (5 + 0)))) 이므로 15 *)

함수의 구현 속에서 함수 스스로를 호출하는 재귀 함수는, "rec" 키워드를 붙여줍니다.

let...in 키워드

let value1 = sqrt((3.0 * 3.0) +. (4.0 *. 4.0))

let value2 =
  let a = 3.0 in
  let b = 4.0 in
  let a_squared = a *. a in
  let b_squared = b *. b in
  sqrt (a_squared +. b_squared)

OCaml에서 *. 와 +. 연산자는 실수간의 연산, *와 +는 정수간의 곱셈을 의미합니다. 위 코드에서 value1과 value2가 의미하는 바는 같습니다. 하지만, 가독성 확보를 위해 value2처럼 작성하고 싶은 프로그래머를 위해, let...in 키워드가 존재합니다. let의 표현식 내부에서만 사용할 수 있습니다. "내부"의 의미를 같는 in 키워드이니, 저는 OCaml 코드를 처음 읽을 때 이렇게 이해하고 했습니다.
"value2는 a가 3.0인 세계관 안에서, b가 4.0인 세계관 안에서, a_squared, b_squared 라는 값은 a,b의 제곱인 세계관 안에서, a_squared와 b_squared에 루트(sqrt)를 씌운 값이야!"

 

모듈 시스템

앞서 예고했던, 모듈 시스템을 소개해보겠습니다. 어느 언어나 그렇듯이, OCaml도 리스트, 스택, 문자열, 입출력 등 유용한 표준 라이브러리들을 제공합니다. OCaml에서는 주로 모듈이라는 용어를 이용합니다. OCaml 5.3 기준 제공되는 모든 라이브러리의 링크를 달아둡니다.

 

OCaml에서 헷갈리는 것 중 하나가 리스트를 사용하는 법입니다. 보통 다른 언어들은 a라는 리스트가 있다면, a[0] 처럼 a의 첫 원소에 접근 가능하지만, OCaml은 그렇지 않기 때문입니다. OCaml은 반드시 List.nth 함수 혹은 List.hd를 이용해 접근해야 합니다.  List 모듈의 대표적인 함수들을 소개합니다.

let my_list = [10;20;30;40]

let second_elem = List.nth my_list 2
let head_elem = List.hd my_list
let length_of_list = List.length my_list
let exist_20 = List.mem 20 my_list (* true, 리스트에 20이 있으므로 *)

let combined_list1 = List.append my_list [50;60]
let combined_list2 = my_list @ [50;60] (* 두 코드는 동일한 의미 *)

let double = List.map (fun x -> 2 * x) my_list (* [20;40;60;80] *)
let filtered = List.filter (fun x -> x < 25) my_list (* [10;20] *)
let sum = List.fold_left (fun acc x -> acc + x) 0 my_list 	(* 100, 0+10+20+30+40 *)

보다시피 List 모듈을 이용할때는 "List." 을 붙이고, 모듈 내의 함수를 호출하면 됩니다. 내가 만든 모듈도 마찬가지 입니다. OCaml 파일의 확장자는 ".ml"입니다. 내가 "util.ml" 파일에 유틸함수들을 작성했다면(sum 함수를 만들었다면), 다른 곳에서 "Util.sum" 처럼 호출하면 됩니다. 그런데, util.ml에 선언한 모든 값을 외부에 노출하고 싶지 않을 수도 있습니다. 그럴때는 OCaml의 인터페이스 파일인 ".mli"파일을 이용합니다.

(* Util.ml *)
let squared_sum x y = (x * x) + (y * y)

let pitagoras x y = sqrt(squared_sum x y)

(* Util.mli *)
val pitagoras : int -> int -> int

이러면 다른 파일에서, Util.pitagoras는 이용할 수 있지만, Util.squared_sum은 이용할 수 없습니다. 인터페이스 파일에 pitagoras 만이 정의되었기 때문입니다. 

 

마지막으로, "List."나 "Util." 같은 접두어를 붙이는 것이 번거롭게 느껴질 때 사용할 수 있는 문법입니다. "Open" 키워드를 이용하면 됩니다. 다만, 서로 다른 모듈에 같은 이름의 함수가 있는 경우가 충분히 가능하므로, 주의해야 합니다.

(* open 문법 이용 X *)
let value = Util.pitagoras 3 4

(* open 문법 이용 *)
open Util
let value = pitagoras 3 4

Dune과 Opam: 코드 실행 및 외부 의존성

main.ml과 util.ml의 코드를 작성했다면, 컴파일 시에 둘을 합쳐야 합니다. C 언어에서 gcc를 사용하듯이, OCaml은 ocamlc를 이용합니다.

 

"ocamlc -o myapp util.ml main.ml" 은 두 OCaml 파일을 순서대로 합쳐, myapp이라는 이름의 목적 파일을 생성합니다. 하지만, 명령어도 너무 복잡할 뿐더러, 프로젝트가 복잡하면 해당 명령어를 이용하는 것은 정말 복잡해질 것입니다.

 

Dune은 이런 문제를 해결합니다. 프로젝트의 루트 경로에, 프로젝트 전반의 설정(dune-project)과 파일 빌드구조(dune)를 서술해두면, "dune build" 명령어로 프로젝트의 모든것을 빌드할 수 있습니다. 이 과정은 직접할 필요 없이 "dune init"을 통해 생성하면 됩니다. 뿐만 아니라 dune은, 빌드 결과물을 지우는 "dune clean", 테스트 코드를 실행하는 "dune test", 코드의 포맷팅(코드를 시각적으로 정돈하는 일)을 돕는 "dune fmt" 등을 지원합니다. 

 

또한, npm이나 pip 같은 외부 라이브러리나 의존성을 설치/관리 해주는 도구가 OCaml에도 역시 있습니다. Opam입니다.

  • 사실은 "dune fmt"를 실행할 때, "ocamlformat"이라는 의존성을 필요로 합니다. "opam install ocamlformat" 명령어로 설치할 수 있습니다.
  • JSON 파싱 라이브러리 "yojson"을 코드에서 사용하고 싶다면, 역시 "opam install yojson"을 하면 됩니다. 단, 코드에서 사용하려고 할때는 dune 파일에 해당 의존성을 사용하겠다는 내용을 명시해야합니다. 그 이후에는 다른 모듈처럼, "Yojson." 혹은 "open 키워드"로 사용하면 됩니다.
  • 코드에 의존성들에 대한 구체적인 정보는 ".opam" 파일에 기술됩니다. Node.js 진영의 package.json이나, Python 진영의 pyproject.toml 같은 파일을 떠올리시면 됩니다. 
저작자표시 (새창열림)

'개발이야기' 카테고리의 다른 글

네이버지도에서 매장 정보 수집을 실패하기까지의 고군분투 이야기  (0) 2025.12.31
개발 생산성을 높이는 MCP의 개념과 Cursor AI 설정법  (0) 2025.07.16
VSCode 환경에서 make로 빌드되는 c파일 gdb로 디버깅하기  (0) 2025.06.16
TailwindCss로 도막도막 끊기는 스크롤 화면 구현하기: Full Page Scroll과 Scroll snap  (1) 2025.05.17
속터지는 Google Play Console 본인인증하기  (3) 2025.02.06
'개발이야기' 카테고리의 다른 글
  • 네이버지도에서 매장 정보 수집을 실패하기까지의 고군분투 이야기
  • 개발 생산성을 높이는 MCP의 개념과 Cursor AI 설정법
  • VSCode 환경에서 make로 빌드되는 c파일 gdb로 디버깅하기
  • TailwindCss로 도막도막 끊기는 스크롤 화면 구현하기: Full Page Scroll과 Scroll snap
준별
준별
  • 준별
    준별개발
    준별
  • 전체
    오늘
    어제
    • 분류 전체보기 (58)
      • 개발이야기 (25)
        • 토막글 (11)
      • 일상이야기 (6)
      • 개인 공부 (23)
      • 생각과 기록 (2)
  • 블로그 메뉴

    • 홈
    • 방명록
    • Github
    • Linkedin
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    터미널세팅
    powerlevel10k
    이산구조
    조합형
    정보보호개론
    필수툴
    클램쉘
    맥북세팅
    데이터베이스
    http pipelining
    바이브코딩
    http1.1
    Zsh
    persistent connection
    맥북
    http2.0
    데스크셋업
    맥북초기세팅
    http1.0
    zsh세팅
    nestjs
    k9s
    http3.0
    전산기조직
    nodejs
    맥북터미널세팅
    zsh-autosuggestion
    artillery
    터미널꾸미기
    실전압축
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
준별
OCaml에 대해 실전 속성 압축으로 익혀보기
상단으로

티스토리툴바