[PL] OCaml Basic2

👑 OCaml Basic 2


💡 Module 이란?

OCaml에서의 module : 자료들의 집합 (= 객체화가 불가능한 클래스)

OCaml은 Module system을 지원한다. 프로그램은 여러개의 모듈로 구성되어 있으며, 각각의 모듈은 자료(변수)와 행동(함수)으로 구성된다.

모듈은 그 자체로 타입은 아니며, 객체화가 불가능하여 모듈의 함수를 호출하거나 자료를 읽는 행위만 가능하다.

모든 소스파일은 그 자체로 모듈이다.

모듈에 접근할 때, 모듈 이름의 첫 글자를 대문자로 해야한다.

  • e.g. module1.ml 의 num에 접근 : Module1.num
(* module1.ml *)
let num = 10
let mul x y = x * y

(* module2.ml *)
let _ = Format.printf "Result : %d\n" Module1.num
let _ = Format.printf "Result : %d\n" (Module1.mul 2 4)


  • 모듈 내에 모듈을 정의할 수 있다. (Nested module)

    • C++ or Java의 inner class 와 유사하다.

    • module [module_name] = struct [defs] end 를 통해 생성 가능하다.

      • module_name 은 반드시 대문자로 시작해야 한다.
      (* module1.ml *)
      module IntAdd = struct
        let add x y = x + y
      end
    
      module FloatAdd = struct
        let add x y = x + y
      end
    
      (* module2.ml *)
      let _ = Format.printf "Result : %d\n" (Module1.IntAdd.add 2 4)
      let _ = Format.printf "Result : %d\n" (Module2.FloatAdd.add 2.0 4.0)
    


  • 모듈을 open 하여 모듈 접근 시에 모듈 이름을 생략할 수 있다.

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

    • C++ 의 namespace 와 유사하다.

    • 다른 여러 모듈들에 같은 이름이 있을 경우 comflict가 발생 가능함에 유의해야 한다.

      (* module2.ml *)
      open Module1
    
      let _ = Format.printf "Result : %d\n" (IntAdd.add 2 4)
      let _ = Format.printf "Result : %d\n" (FloatAdd.add 2.0 4.0)
    
    • 특정 scope 내에서만 모듈을 개방할 수도 있다.
      let _ =
        let open Module1.IntAdd in
        let _ = Format.printf "Result : %d\n" (add 2 4) in
        let _ = Format.printf "Result : %d\n" (add 1 2) in
        Format.printf "Result : %d\n" (add 4 5)
    


  • 모듈 이름이 긴 경우, renaming 이 가능하다.

    • module [abbreviation] = [module_name]
      (* module2.ml *)
      module M1I = Module1.IntAdd
      module M1F = Module1.FloatAdd
      module F = Format
    
      begin
        F.printf "Result : %d\n" (M1I,add 3 7);
        F.printf "Result : %d\n" (M1F.add 2.0 3.0)
      end
    


💡 Pattern Matching

OCaml은 C++, Java 의 switch-case와 유사한 match-with expression을 제공한다.

값의 “형태”에 따라 다르게 처리해야 하는 경우에 유용하게 사용된다.

  • 매칭되는 경우에 해당 expression을 계산하고, 그 결과가 전체의 결과이다.

  • patternconstantvariable로 구성된다.

    • 상수인 경우 : 계산 값이 패턴과 일치하는 지를 매칭

    • 변수인 경우 : 계산 값을 변수에 binding (항상 매칭에 성공)

      • 즉, 변수가 앞 패턴에 등장 -> 이후의 패턴은 매칭되지 않는다.
match expression with         (* 1. expression 계산 *)
| pattern1 -> expression1     (* 2. 계산 결과를 pattern1과 매칭 *)
| pattern2 -> expression2     (* 3. 매칭 실패 시 pattern2와 매칭 *)
.
.
.
| patternN -> expressionN     (* ... *)

let _ =
  let check_value x =
    match x with
    | 0 -> 0
    | y -> y
    | 1 -> 1
  in
  Format.printf "Result : %d\n" (check_value 2)

-> warning 발생 [redundant-case]: this match case is unused.


  • pattern에 wildcard _ 사용 가능

    • 항상 매칭에 성공한다.

    • 값이 binding 되지 X -> 매칭된 값 사용 안함

    • 만약 아래의 코드에서 wildcard 패턴이 빠진다면 -> warning

      • i mod 2의 결과는 항상 0 or 1 이지만, 코드에 포함되어야 한다!
    • OCaml 컴파일러는 expression의 타입을 기반으로 패턴매칭의 완전성을 검사한다.

      • 예를 들어, expression이 int 타입인 경우 -> 모든 int 값을 커버할 수 있어야 함.
let _ = 
  let even_or_odd i =
    match i mod 2 with
    | 0 -> Format.printf "Even\n"
    | 1 -> Format.printf "Odd\n"
    | _ -> Format.printf "Unknown\n"
  in
  let _ = even_or_odd 0 in (* Even *)
  let _ = even_or_odd 1 in (* Odd *)
  even_or_odd 3 (* Odd *)


  • .. 기호를 사용하여 연속된 문자 혹은 숫자에 대한 패턴 생성 가능
let _ =
  let check_lower c =
    match c with
    | 'a' .. 'z' -> true  (* 'a' 부터 'z' 까지 *)
    | _ -> false
  in
  let _ = Format.printf "c : %b\n" (check_lower 'c') in   (* true *)
  Format.printf "C : %b\n" (check_lower 'C')              (* false *)


  • match-with expression도 이 자체로 expression 이기에 하나의 타입만을 가져야 한다.

    • 각 match case의 expression이 모두 동일한 타입을 가져야만 한다.
let _ =
  let check_lower c =
    match c with
    | 'a' .. 'z' -> true  (* 'a' 부터 'z' 까지 *)
    | _ -> 0
  in
  ...   (* 각각의 expression이 bool, int 타입으로 서로 다르기 때문에 에러 발생 *)


  • tuple 등의 자료구조에 대해서도 패턴 매칭이 가능하다.
let _ =
  let get_first t =
    match t with
    | (first, _) -> first
  in
  let get_second t =
    match t with
    | (_, second) -> second
  in
  let _ = Format.printf "first : %d\n" (get_first (2, 5)) in (* 2 *)
  Format.printf "second : %d\n" (get_second (3, 4))          (* 4 *)


💡 List

서로 다른 타입의 원소를 가질 수 있는 튜플과는 다르게,

리스트의 모든 원소는 동일한 타입을 가져야만 한다.

  • OCaml 에서 리스트의 생성은 []를 사용하고, 원소끼리의 구분은 ;을 이용한다.
[1; 2; 3; 4], ['a'; 'b'; 'c'], ["Hello"; "World"]


  • 리스트 연산자

    • :: 연산자 : 리스트 앞에 원소를 삽입하여 새로운 리스트를 반환
    let lst = 0 :: [1; 2; 3]  (* [0; 1; 2; 3] *)
    


    • @ 연산자 : 두 개의 리스트를 연결하여 새로운 리스트를 반환
    let lst = [1; 2] @ [3; 4]  (* [1; 2; 3; 4] *)
    


  • OCaml List Library

    • OCaml은 List에 대한 내장 라이브러리를 제공한다.

    • 주요 함수

      'a 는 임의의 타입을 가리킴


      • List.iter : ('a -> unit) -> 'a list -> unit

        🔹 함수리스트를 인자로 받아, 각 리스트의 원소에 함수를 apply
        🔹 반환값이 unit = 반환값 없음

      let print_square x = Format.printf "%d " (x * x)
      
      let lst = [1; 2; 3; 4] in
      List.iter print_square lst
      
      (* 출력 : 1 4 9 16 *)
      


      • List.map : ('a -> 'b) -> 'a list -> 'b list

        🔹 함수리스트를 인자로 받아, 리스트의 각 원소에 함수를 apply한 새로운 리스트를 반환하는 함수

      let _ =
        let lst = [1; 2; 3] in
        let new_lst = List.map (fun x -> x + 1) lst
        ...
      
        (* new_lst = [2; 3; 4] *)
      


      • List.fold_left : ('acc -> 'a -> 'acc) -> 'acc -> 'a list -> 'acc

        🔹 함수, 초기값, 리스트를 인자로 받아, 리스트의 첫 원소부터 함수를 적용하여 누적 값을 갱신하고 최종 누적 값을 반환하는 함수

      let sum acc x = acc + x
      
      let lst = [1; 2; 3; 4] in
      let result = List.fold_left sum 0 lst in
      Format.printf "Sum : %d\n" result
      
      (* Sum : 10 *)
      (* - 첫번째 원소 1에 대해 0 (acc) + 1 -> acc = 1 *)
      (* - 두번째 원소 2에 대해 1 (acc) + 2 -> acc = 3 *)
      (* - 세번째 원소 3에 대해 3 (acc) + 3 -> acc = 6 *)
      (* - 네번째 원소 4에 대해 6 (acc) + 4 -> acc = 10 *)
      


  • List와 Pattern matching
let rec len lst = 
  match lst with
  | [] -> 0     (* list가 비어있을 경우 *)
  | _ :: t ->   (* list가 값을 가질 경우 *)
      1 + (len t)

(* -------------------- *)

let lst = [1; 2; 3; 4]

match lst with
| [] -> print_endline "Empty list"
| [x] -> print_endline "Single element list"
| x :: y :: rest -> print_endline "Two or More element list"

(*
[] 패턴 : 리스트가 비어 있는 경우
[x] 패턴 : 리스트에 하나의 요소만 있는 경우
x :: y :: rest 패턴 : 리스트에 적어도 두 개의 요소가 있는 경우

  'x'는 첫 번째 요소, 'y'는 두 번째 요소, rest는 리스트의 나머지 요소들
  위의 lst에서 x = 1, y = 2, rest = [3; 4]
*)


💡 Type Definition

OCaml 에서는 사용자가 새로운 타입을 정의할 수 있다.

type [type_name] = [type] 의 형태로 사용하며,

C/C++의 typedef와 유사하다.

일반적으로 OCaml은 강력한 타입 추론 기능을 가지고 있어, 타입을 명시적으로

지정해줄 필요가 없는 경우가 많지만, 새로운 타입을 정의한 경우, 코드의 가독성을

높이고, 의도를 명확하게 전달하기 위해 명시적으로 타입을 지정해주는 것이 좋다.

type INT = int

let add_two_integers (x : INT) (y : INT) : INT = x + y

let x : INT = 10
let y : INT = 20

let sum : INT = add_two_integers x y

Format.printf "sum : %d\n" sum


  • Disjoint union

    • OCaml에서 Disjoint union이란 여러 개의 다른 타입을 하나의 타입으로 묶는 데

      사용되는 개념이다. 이를 통해 하나의 값이 여러 다른 형식을 가질 수 있으며,

      Variant records 라고도 부른다.

    • type 키워드와 | 를 사용하여 정의한다.

    • 아래 코드에서 Circle, Rectangle 과 같은 식별자는 대문자로 시작해야하며,

      Constructor 라고 불린다.

      type shape =
        | Circle of float
        | Rectangle of float * float
    
      let cal_area = function
        | Circle r -> 3.14 *. r *. r
        | Rectangle (w, h) -> w *. h
    
      let circle_area = calculate_area (Circle 2.0)
      let rectangle_area = calculate_area (Rectangle (3.0, 4.0))
    

Categories:

Updated:

Leave a comment