KoreanFoodie's Study

OCaml 기초 2 - 자료형, Currying, match-with, polymopic type, reference 본문

Tutorials/Ocaml

OCaml 기초 2 - 자료형, Currying, match-with, polymopic type, reference

GoldGiver 2019. 9. 17. 17:21

이전 게시글에 이어 OCaml에서 꼭 알아야 할 중요한 개념들에 대해 더 알아보도록 하자.

주요 개념들 : Pair, Tuple, List, Currying, Inductive type, match-with 구문, Polymorphic type, try-with-raise, module system, reference.


OCaml Pair

Pair는 두 개의 값을 한번에 묶는 데 활용된다. 정의는 (x, y)같은 식으로 할 수 있으며, 타입은 a * b형태로 표기된다.

Pair의 각 항을 조회하는데 fstsnd함수를 사용할 수 있다.

let p:(int * string) = (1, "a")
let i = fst p (* int, 1 *)
let s = snd p (* string, "a" *)
let (i, s) = p (* 이렇게 해서 i 와 s값을 정의해줄 수도 있다.)

OCaml Tuple

Tuple은 Pair의 연장선이라고 볼 수 있다. Pair가 2개의 값을 묶을 수 있는 반면, Tuple은 그 이상의 값들을 한번에 묶을 수 있다.

대신, Pair의 fst, snd와 같은 함수는 사용할 수 없다.

let t:(int * string * float) = (1, "a", 1.5)
let (i, s, f) = t (* f가 1.5값이 된다 *)
let (_, _, f) = t (* 윗줄과 같음. _ 은 type과 값에 관계없이 자리를 채워준다 *)

OCaml List

List는 OCaml을 사용한다면 정말 많이 사용하게 되는 자료구조이다.

리스트는 빈 리스트 ([ ])이거나 원소와 리스트의 결합 (x::y)로 표현된다. 이때, x가 int 타입이면, y는 int list 타입이다.

리스트는 두 가지 방식으로 정의할 수 있다.

let x: int list = 1 :; 2:: 3:: []
let x: int list = [1; 2; 3]
  • List 다루기

리스트는 head와 tail로 나뉘게 된다. head는 리스트의 첫 항을 의미하며, tail은 head를 제외한 나머지 리스트를 의미한다.

예제를 통해 어떻게 동작하는지 자세히 알아보자.

let head_elem: int = List.hd [1; 2; 3] (* 1 *)
let tail_list: int list = List.tl [1; 2; 3] (* [2 ; 3] *)
let elem: int = List.nth [1; 2; 3] 1 (* 2. 두번째 원소를 리턴한다 *)
if (List.mem 1 [1; 2; 3]) then ... else ...

List의 원소는 모두 같은 타입이어야 한다.

단, tuple은 다른 타입의 여러 값들도 묶을 수 있다.


OCaml Currying

Currying은 기존 C, C++, Java, Python등에서 보기 힘든 함수형 언어(Scala 등)의 특색이라고 할 수 있다.

함수가 여러 개의 인자를 받아야 할 때, pair로 묶어서 한번에 받는다면 이런 식으로 쓸 수 있다.

let x (a, b) = a + b (* (int * int) -> int *)

하지만 currying을 사용하게 되면, 두 인자를 차례대로 받을 수 있다.

let y a b = a + b (* int -> int -> int *)

두 함수의 타입은 다르다! (중요)

그럼 두 번째 예시의 currying 타입을 이해해 보자.

y 함수는 int를 두 번 받아서 int를 결과값으로 내놓는 함수이다.

조금 더 살펴보면, int를 받아서 int -> int타입 함수를 내놓는 함수이다.

즉, a b두 개의 인자에서 a만 인자로 준다면, y 함수는 a + (앞으로 줄 인자)를 수행해 int를 뱉어내는 함수가 된다!

즉, 다음과 같이 함수를 쪼갤 수도 있는 것이다.

let incr = sum 1 (* incr: int -> int *)

OCaml - Inductive type

사용자가 직접 타입을 정의할 수도 있다. 이 타입 정의는 inductive한 형태도 가능하다.

예를 들어, 정수 이진 트리 타입을 정의한다고 생각해 보자.

type tree = Leaf of int
            | Node of int * tree * tree

let t1: tree = Leaf 1
let t2: tree = Node(1, Leaf 2, Leaf 3)

이때, 사용자 정의 타입의 constructor (Leaf, Node 등)는 대문자로 시작해야 한다.

match-with 구문

패턴 매칭은 케이스를 나누어 계산하는 편리한 문법 요소이다.

match-with 구문을 이용하여, 앞에서 정의한 정수 이진 트리 원소의 합을 구해보자.

type tree = Leaf of int
            | Node of int * tree * tree

let rec sum_of_tree: tree -> int = fun mytree ->
    match mytree with
    | Leaf i -> i
    | Node (i, l, r) ->
        i + (sum_of_tree l) + (sum_of_tree r)

이런 식으로 코드를 짜면, recursive하게 sum_of_tree함수가 Leaf가 나올 때가지의 값을 알아서 계산해 준다.

주의사항

match-with의 경우, 중첩이 가능하다. (match-with 아래에 match-with 사용 가능)

이때, 괄호를 잘 붙여 주어야 한다.

let foo x = 
    match x with 
    |A -> ...
    |B -> ...
    |C y ->
        (match y with
        |K -> ...
        |L -> ... )
    |D -> ...

OCaml Polymorphic type

Polymorhphic type을 활용하면, 함수가 인자를 받거나 할 때, 굳이 타입을 명시하지 않더라도 여러 타입을 받아 각 타입에 맞게 함수가 동작하도록 만들 수 있다.

예를 들어, 정수 리스트의 길이를 구하는 함수와, 문자열 리스트의 길이를 구하는 함수를 짠다고 가정하자.

let len = List.length [1; 2; 3] (* 3 *)
let len = List.length ["a"; "b"; "c"; "d"] (* 4 *)

굳이 타입에 맞춰 함수를 새로 구성할 필요없이, refactoring이 가능하다.

이는 List가 다형(Polymorphic) 타입이라서 가능하다.

type 'a list = [ ] | 'a ::'a list
(* Pseudocode, does not compile *)
(* 'a list 가 빈 리스트([ ]) 이거나, 'a 타입에 'a 타입 리스트를 붙인 리스트 형태라는 것을 의미한다 *)

이때, 'a는 임의의 type을 의미한다.

'a가 int면 정수 리스트, 'a가 string이면 문자열 리스트가 된다.

그러므로, list의 길이를 구하는 함수는 다음과 같이 한 번만 정의하고 써 주면 된다.

let rec list_len: 'a list -> int = fun mylist ->
    | [] -> 0
    | head::tail -> 1 + list_len tail

실제 OCaml 의 List 라이브러리도 비슷하게 ( = 다형 타입 덕분에 효율적으로) 구현되어 있다.


OCaml try-with(raise) 구문

다음으로는 예외처리에 대해 알아보자.

이는 Java/C++ 등의 예외 처리와 비슷하다.

try로 예외가 생길만한 부분을 묶고, with로 예외를 처리하며, raise는 예외를 발생시킨다.

let do_div: int -> int -> unit = fun x y ->
    try print_endline (string_of_int (x/y)) with
    Division_by_zero -> print_endline "Div by 0"

let f x = 
    if x < 0 then
        raise (Failure "invalid input")
    else x

Ocaml module

OCaml의 모듈은 관련 있는 코드들의 묶음이다. 파이썬을 써 본 사람은 매우 익숙할 것이다.

Signature는 모듈에서 드러내고 싶은 것만 드러낸다는 뜻이고, Functor는 모듈을 받아서 모듈을 내놓는 것을 의미한다.

모듈은 내용이 많으므로, 차후 다루도록 하겠다.


OCaml reference

OCaml은 값 중심 언어이지만, 명령형(imperative) 스타일로 쓰는 요소도 지원한다. 이는 C에서의 포인터와 비슷하다.

변수를 선언하고 값을 저장하는 스타일을 보자.

  • 0을 가리키는 참조자(reference) 정의
let int_ref = ref 0

값을 가져오려면 ! 연산자를 사용한다.

let value_of_ref: int = !int_ref

새 값을 저장할 때에는 :=연산자를 사용한다.

let _ = int_ref := !int_ref + 1 (* "i++" in C *)

 

추후에 더 많은 예제를 다뤄보도록 하자.

Comments