cv.cm

코딩 · 프로그래밍 언어

초보자를 위한 OCaml 기초 튜토리얼

9분 분량

핵심 답변

OCaml은 ML 계열의 정적 타입 함수형 언어입니다. 컴파일러가 타입을 추론하고, 프로그램을 실행하기 전에 많은 실수를 잡아내며, 빠른 네이티브 코드를 생성합니다. 프로그램은 불변 값, 배리언트(태그 유니온), 패턴 매칭, 모듈로 구성됩니다. opam으로 설치하고 ocaml hello.ml로 파일을 실행하거나 ocamlopt로 컴파일합니다.

OCaml이란 무엇이며 누가 쓰는가

OCaml은 프랑스 Inria에서 만들었으며, 지금은 Jane Street와 Tarides 같은 기업이 참여하는 오픈 소스 커뮤니티가 개발합니다. 함수형 프로그래밍에 선택적인 명령형 기능(변경 가능한 참조, 배열, 반복문)과 대부분의 코드에서 필요 없는 객체 시스템을 결합했습니다. OCaml 5는 도메인과 이펙트 핸들러를 통한 공유 메모리 병렬 처리를 추가했습니다.

OCaml은 컴파일러와 프로그램 분석 도구, Rocq(예전 이름 Coq) 같은 증명 보조기, 트레이딩 시스템(Jane Street가 가장 잘 알려진 산업 사용자입니다), Meta의 Flow와 Infer 같은 개발자 도구에 쓰입니다. 함수형 프로그래밍과 타입 시스템을 가르치는 언어로도 흔히 쓰입니다.

OCaml 설치와 Hello, world 실행

OCaml을 설치하는 표준 방법은 OCaml 패키지 관리자인 opam입니다. opam을 설치한 뒤 opam init을 한 번 실행하면 컴파일러가 설정됩니다. 그다음 opam install dune utop으로 빌드 도구 dune과 더 편리한 REPL인 utop을 설치합니다.

(* hello.ml *)
let () = print_endline "Hello, world!"
ocaml hello.ml            # run with the interpreter
ocamlopt -o hello hello.ml
./hello                   # native executable
# Hello, world!

let () = ...는 진입점을 쓰는 일반적인 방식입니다. 표현식을 평가하고 unit(값 없음)을 반환하는지 확인합니다. REPL(utop 또는 ocaml)에서는 각 구문을 ;;로 끝내고, 소스 파일에서는 ;;가 필요 없습니다. 실제 프로젝트에서는 dune init project myapp으로 프로젝트를 만들고 dune build로 컴파일합니다.

값과 기본 타입

타입예시설명
int4264비트 시스템에서 63비트. 연산자 + - * / mod
float3.14자체 연산자 +. -. *. /.를 사용
string"hello"변경할 수 없는 바이트. ^로 연결
char'a'바이트 하나
booltrue, false&&, ||, not
unit()결과가 없는 표현식의 값
let name = "Ana"
let age = 30
let height = 1.68 *. 100.0
let () = Printf.printf "%s is %d, %.0f cm\n" name age height
(* Ana is 30, 168 cm *)
let count = ref 0
let () = incr count; Printf.printf "count = %d\n" !count

바인딩은 불변입니다. ref는 변경 가능한 셀로, !r로 읽고 r := v로 씁니다. OCaml은 int와 float를 자동으로 변환하지 않으므로 float_of_int와 int_of_float를 사용하세요.

제어 흐름과 함수

if는 표현식입니다. 함수는 let으로 정의하고, 재귀 함수는 let rec이 필요합니다. 인자는 공백으로 구분하며 모든 함수가 커리되어 있어서 부분 적용이 공짜입니다.

let sign n =
  if n > 0 then "positive"
  else if n < 0 then "negative"
  else "zero"
let rec factorial n = if n <= 1 then 1 else n * factorial (n - 1)
let add x y = x + y          (* int -> int -> int *)
let add5 = add 5
let () =
  for i = 1 to 3 do
    Printf.printf "%d squared is %d\n" i (i * i)
  done;
  Printf.printf "%s %d %d\n" (sign (-2)) (factorial 5) (add5 10)
(* negative 120 15 *)

sign (-2)의 괄호에 주의하세요. 괄호가 없으면 OCaml은 sign - 2를 뺄셈으로 읽습니다. unit을 반환하는 표현식을 이어 쓸 때는 ;를 사용합니다.

리스트, 배열, 튜플, 레코드

구조리터럴설명
List[1; 2; 3]변경할 수 없는 단일 연결 리스트. x :: xs는 앞에 추가
Array[| 1; 2; 3 |]고정 크기, 변경 가능, a.(0)으로 인덱싱
Tuple("Ana", 30)서로 다른 타입의 값을 고정된 개수로 묶음
Record{ name = "Ana"; age = 30 }이름 있는 필드. 타입을 먼저 선언
Hashtbl / MapHashtbl.create 16변경 가능한 해시 테이블 / 불변 균형 트리 맵
type person = { name : string; age : int }
let people = [ { name = "Ana"; age = 30 }; { name = "Ben"; age = 17 } ]
let () =
  people
  |> List.filter (fun p -> p.age >= 18)
  |> List.map (fun p -> p.name)
  |> List.iter print_endline
(* Ana *)
let older = { (List.hd people) with age = 31 }

파이프라인 연산자 x |> f는 f x를 뜻합니다. List 모듈은 map, filter, fold_left, find_opt 등 많은 함수를 제공합니다.

대표 기능: 배리언트, 패턴 매칭, 모듈

배리언트 타입은 값이 가질 수 있는 모든 경우를 나열합니다. 패턴 매칭은 값을 경우별로 분해하며, 빠진 경우가 있으면 컴파일러가 경고하므로 리팩터링이 안전합니다.

type shape =
  | Circle of float
  | Rectangle of float * float
let area = function
  | Circle r -> Float.pi *. r *. r
  | Rectangle (w, h) -> w *. h
let () =
  List.iter (fun s -> Printf.printf "%.2f\n" (area s))
    [ Circle 1.0; Rectangle (2.0, 3.0) ]
(* 3.14 then 6.00 *)
module StringMap = Map.Make (String)
let ages = StringMap.(empty |> add "Ana" 30 |> add "Ben" 17)
let () = Printf.printf "%d\n" (StringMap.find "Ana" ages)   (* 30 *)

모듈은 타입과 함수를 묶으며, 모든 파일이 하나의 모듈입니다(list.ml은 List가 됩니다). 펑터는 모듈에서 모듈로 가는 함수입니다. Map.Make (String)은 키가 문자열인 맵 모듈을 만듭니다. 시그니처(.mli 파일)는 구현 세부 사항을 숨깁니다.

오류 처리

OCaml에는 세 가지 도구가 있습니다. 값이 없을 수 있을 때의 option, 정보와 함께 실패할 수 있는 작업의 result, 예외적인 상황의 예외입니다.

let parse_age s =
  match int_of_string_opt s with
  | Some n when n >= 0 -> Ok n
  | Some _ -> Error "Age must not be negative"
  | None -> Error "Not a number"
let () =
  match parse_age "abc" with
  | Ok n -> Printf.printf "Age %d\n" n
  | Error msg -> print_endline ("Error: " ^ msg)
(* Error: Not a number *)
exception Empty_input
let first = function [] -> raise Empty_input | x :: _ -> x
let () =
  try print_int (first []) with Empty_input -> print_endline "empty list"

작은 완성 프로그램: 단어 빈도

let () =
  let text = "the cat and the hat and the bat" in
  let counts = Hashtbl.create 16 in
  String.split_on_char ' ' text
  |> List.iter (fun w ->
         let n = Option.value (Hashtbl.find_opt counts w) ~default:0 in
         Hashtbl.replace counts w (n + 1));
  Hashtbl.to_seq counts
  |> List.of_seq
  |> List.sort (fun (_, a) (_, b) -> compare b a)
  |> List.filteri (fun i _ -> i < 2)
  |> List.iter (fun (w, n) -> Printf.printf "%s %d\n" w n)
(* the 3
   and 2 *)

이 프로그램은 변경 가능한 해시 테이블로 단어를 세고, 테이블을 (word, count) 쌍의 리스트로 바꾸고, 횟수의 내림차순으로 정렬해 처음 두 개를 출력합니다. ~default:0은 레이블 인자입니다.

다음 단계

  • ocaml.org: 설치 가이드, A Tour of OCaml, Docs 아래의 다른 튜토리얼.
  • 전체 언어와 표준 라이브러리 레퍼런스인 OCaml 매뉴얼.
  • 프로젝트 빌드용 dune과 라이브러리 설치용 opam을 익히세요.
  • 더 큰 실전 예제를 위해 무료 온라인 책 Real World OCaml을 살펴보세요.

자주 묻는 질문

OCaml은 배우기 어려운가요?

문법은 작지만, 명령형 언어를 쓰다 왔다면 타입, 재귀, 패턴 매칭으로 생각하는 데 연습이 필요합니다. 타입 검사기의 오류 메시지는 금방 읽기 쉬워집니다.

OCaml은 왜 부동소수점 연산자가 따로 있나요?

OCaml은 산술 연산자를 오버로드하지 않으므로 +는 int에만, +.는 float에만 동작합니다. 덕분에 타입 추론이 단순하고 예측 가능합니다.

OCaml과 F#의 차이는 무엇인가요?

F#은 OCaml에서 파생되어 핵심 문법 대부분을 공유하지만, F#은 .NET에서 실행되고 들여쓰기가 의미를 가집니다. OCaml은 네이티브 코드나 바이트코드로 컴파일되며 펑터를 갖춘 더 강력한 모듈 시스템이 있습니다.

OCaml에서 ;;는 언제 필요한가요?

대화형 톱레벨(ocaml 또는 utop)에서만 필요하며, 입력한 내용을 평가하라고 REPL에 알려 줍니다. .ml 소스 파일에서는 보통 생략합니다.

코딩 튜토리얼 전체