본문 바로가기

CS/프로그래밍언어개론(OCaml)

[프로그래밍언어개론] OCaml basic - 3

 

Recursive function

OCaml에서 재귀함수(Recursive function)은 rec 키워드와 함께 named function으로 정의되어야 한다. 아래는 코드 예제이다.

(* 아래 코드는 rec 키워드 없이 함수가 자기자신을 호출하므로 에러 발생 *)
let mult_all x =
  if x = 1 then 1
  else x * (mult_all (x - 1))
in
Format.printf "Result: %d\n" (mult_all 10)

(* 재귀함수는 아래와 같이 rec 키워드와 함께 사용 *)
let rec mult_all x =
  if x = 1 then 1
  else x * (mult_all (x - 1))
in
Format.printf "Result: %d\n" (mult_all 10)

 

 

Modules

OCaml에서는 모듈 시스템을 지원한다. 모듈이란 변수, 함수, 타입, 값 등을 그룹화하여 관리하는 단위이다. 이러한 변수, 함수, 타입, 값 등이 모인 모듈이 모여 또다른 모듈을 구성하기도 한다. .ml 파일 또한 여러개의 모듈로 구성된 모듈이다.

📝 Modules

🔍각각의 모듈은 자료(변수)와 행동(함수)로 구성

🔍모듈의 이름은 반드시 대문자로 시작

▶operation.ml -> Operation

🔍모듈 내의 변수 또는 함수에 접근 : [module_name].[var_name]

(* operation.ml 파일의 내용 *)
let def_val = 99
let add x y = x + y

(* main.ml 파일의 내용 *)
let _ =
  let _ = Format.printf "Result: %d\n" Operation.def_val in
    Format.printf "Result: %d\n" (Operation.add 3 7)

위에서 언급했듯이, 모듈이 모여서 모듈을 구성하기도 한다. 이는 모듈 내부에 모듈이 정의되어 있을 수 있다는 의미이며 이러한 모듈 내부에 정의된 모듈을 Nested module이라고 한다.

📝 Nested modules

🔍 module [module_name] = struct [defs] end definition

▶파일이 곧 모듈이므로, 위 definition으로 생성된 모듈은 nested module이다.

▶module_name : 모듈의 이름이며 만드시 대문자로 시작해야 한다.

▶defs : definition들의 나열이다.

🔍 Nested 모듈에 접근: [module_name].[nested_module_name].[var_name]

(* operation.ml *)
module IntOp = struct
  let add x y = x + y
end

module FloatOp = struct
  let add x y = x + y
end

(* main.ml *)
let _ =
  let _ = Format.printf "Result: %d\n" (Operation.IntOp.add 3 7) in
  Format.printf "Result: %f\n" (Operation.FloatOp.add 3.0 7.0)

모듈을 open하면 다른 모듈의 변수에 접근할 때 모듈 이름을 생략할 수 있다.

📝 Module opening

🔍 open [module_name]: 현 scope에서 모듈 내 변수를 모듈 이름 없이 접근

▶모듈 내의 definition들을 해당 위치에 붙여넣기 한 효과

(* Operation 모듈 개방 -> Operation 내부의 변수, 함수 등 앞에 "Operation." 생략하고 접근가능 *)
open Operation

let _ =
  let _ = Format.printf "Result: %d\n" (IntOp.add 3 7) in
  Format.printf "Result: %d\n" (FloatOp.add 3.0 7.0)

그러나, 위와 같은 방식에는 문제가 있다. 여러 모듈을 open하여 사용할 때, 같은 이름의 변수나 함수가 존재하면 conflict가 발생하게 된다. 아래 예제를 보자.

open Operation
open IntOp
open FloatOp

let _ =
  let _ = Format.printf "Result: %d\n" (add 3 7) in
  Format.printf "Result: %d\n" (add 3.0 7.0)

위 예제의 경우, IntOp와 FloatOp 두 모듈 모두 add라는 함수가 존재함에도 불구하고, 마지막으로 open된 FloatOp의 add만 사용하게 된다. 결국 FloatOp.add의 인자로 int가 전달되어 컴파일 오류가 발생하게된다.

이러한 경우에는 let - in expression을 사용하여 특정 scope내에서만 모듈을 개방해주면 된다.

📝 Local module opening

🔍 let open [module_name] in [expression]

▶module_name: 개방할 모듈 이름

▶expression: 계산할 expression으로 module_name이 개방된 상태에서 계산

let _ =
  let open Operation.IntOp in
  let _ = Format.printf "Result: %d\n" (add 3 7) in
  let _ = Format.printf "Result: %d\n" (add 1 2) in
  let _ = Format.printf "Result: %d\n" (add 4 12) in
  Format.printf "Result: %d\n" (add 2 5)

모듈의 이름이 긴 경우 약자를 이용하여 모듈을 지칭할 수도 있다.

📝 Module renaming

🔍 module [abbreviation] = [module_name]

▶ abbreviation: 모듈의 이름을 지칭할 약자

▶ module_name: 모듈 이름

module OI = Operation.IntOp
module F = Format

let _ = F.printf "Result: %d\n" (OI.add 3 7) in
let _ = F.printf "Result: %d\n" (OI.add 1 4) in
let _ = F.printf "Result: %d\n" (OI.add 4 12) in
F.printf "Result: %d\n" (OI.add 2 5)

 

 

Pattern matching

Pattern matching은 값의 형태에 따라 다른 행동을 수행하게 만드는 expression이다. match - with expression을 사용하며, 그 자체로 하나의 expression이므로 값으로 계산된다.

📝 Pattern matching

match expression with
| pattern1 -> expression1
| pattern2 -> expression2
...
| patternN -> expressionN

🔍코드의 수행 순서

▶expression 계산

▶계산 결과를 pattern1과 매칭

    - pattern1이 상수인 경우 : expression의 계산 결과와 상수 값이 매칭 확인

    - pattern1이 변수인 경우 : 변수에 expression의 계산 결과를 바인딩하고 매칭에 항상 성공

▶매칭 성공 시 expression1 실행 or 매칭 실패 시 pattern2와 매칭

🔍match - with expression은 하나의 expression이므로 expression1 ~ expressionN까지 동일한 타입을 가짐

위에서 설명했듯이, pattern에 변수가 들어갈 경우, 변수는 값과 바인딩되고 항상 매칭에 성공한다. 따라서 match with expression이 하나의 값을 보장하기 위해서 마지막 pattern에 변수를 넣어 매칭이 성공됨을 보장하도록 만드는 것이 일반적이다. 아래는 이에 대한 간단한 피보나치 코드 예제이다.

module F = Format
module Fib = struct
  let rec fib i =
    match i with
    | 0 -> 0 (* when i is 0 *)
    | 1 -> 1 (* when i is 1 *)
    | n -> fib (n - 2) + fib (n - 1) (* otherwise *)
end

let _ =
  let _ = F.printf "Res: %d\n" (Fib.fib 0) in (* 0 *)
  let _ = F.printf "Res: %d\n" (Fib.fib 1) in (* 1 *)
  let _ = F.printf "Res: %d\n" (Fib.fib 2) in (* 1 *)
  F.printf "Res: %d\n" (Fib.fib 3) (* 2 *)

만약, 매칭이 성공됨을 보장하되 값과 바인딩한 변수는 사용하고 싶지 않을 경우에는 wildcard(_)를 사용하면 된다. 

let _ =
  let even_or_odd i =
    match i mod 2 with 
    | 0 -> F.printf "Even\n" 
    | 1 -> F.printf "Odd\n" 
    | _ -> F.printf "Unknwon\n"

여기서 만약에 pattern matching의 마지막 pattern에 있는 wildcard를 빼면 컴파일 warning이 발생한다. 따라서 가급적이면 위와 같이 작성하는 것이 좋다.

 

Pattern matching을 사용할 때 ".." 기호를 사용하여 연속된 문자 또는 숫자에 대한 패턴을 생성할 수도 있다.

let _ =
  let is_lowercase c =
    match c with
    | 'a' .. 'z' -> true (* when c is 'a' or 'b' or ... or 'z' *)
    | _ -> false
  in
  let _ = F.printf "A : %b\n" (is_lowercase 'A') in (* false *)
  let _ = F.printf "W : %b\n" (is_lowercase 'W') in (* false *)
  let _ = F.printf "b : %b\n" (is_lowercase 'b') in (* true *)
  F.printf "z : %b\n" (is_lowercase 'z') (* true *)

튜플이나 리스트에 대해서도 패턴 매칭이 가능하다. 아래 코드는 튜플의 첫 번째 값이나 두 번째 값을 반환하는 코드이다.

let _ =
  let get_fst p =
    match p with (* p is a tuple containig two elements *)
    | (fst, _) -> fst (* fst is a binding occurrence for the first element *)
  in
  let get_snd p =
    match p with (* p is a tuple containig two elements *)
    | (_, snd) -> snd (* snd is a binding occurrence for the second element *)
  in
  let _ = F.printf "fst : %d\n" (get_fst (1, 3)) in (* 1 *)
  let _ = F.printf "fst : %d\n" (get_fst (2, 4)) in (* 2 *)
  let _ = F.printf "snd : %d\n" (get_snd (1, 3)) in (* 3 *)
  F.printf "snd : %d\n" (get_snd (2, 4)) (* 4 *)

아래는 리스트에 대한 패턴 매칭 코드 예제이다.

 

Type definition

OCaml은 사용자가 새로운 타입을 정의하는 것을 허용한다.

📝 Type definition

🔍 type [type_name] = [type]

▶ type_name: 새로운 타입 이름(소문자)

▶ type: 기존 타입

아래는 string list를 새로운 타입으로 정의하여 사용하는 간단한 코드 예제이다.

(* string list를 str_list라는 새로운 이름의 타입으로 정의 *)
type str_list = string list

let _ =
  let is_empty (x : str_list) =
    match x with
    | [] -> true
    | _ :: _ -> false
  in
  let _ = F.printf "Res: %b\n" (is_empty []) in
  F.printf "Res: %b\n" (is_empty ["welcome";"to";"this";"class"])

여러 타입을 묶어서 타입으로 정의할 수도 있다. 이렇게 정의한 타입을 disjoint union(variant records)라고 부른다.

📝 Disjoint union(Variant records)

🔍 type [type_name] = [name] (of [type]) | [name] (of [type])

▶ type_name: 새로운 타입 이름(소문자)

▶ name: 식별자(대문자로 시작)

     - Called "Constructor"

▶ type: 기존 타입

▶ of [type]은 생략가능

아래는 이에 대한 코드 예제이다.

type number =
  | Zero
  | Integer of int
  | Float of float

let _ =
  let x : number = Zero in
  let y : number = Integer 3 in 
  let z : number = Float 3.0 in
  let n_list : number list = [x; y; z] in
  ()