[UPL] 함수형 언어 OCaml
Objective Caml. Caml 언어는 ML family에 속하는 프로그래밍 언어로, 안전한 소프트웨어 개발과 교육 용도로 활용된다.
Functional Programming Language - 함수를 일급 객체로 사용해 변수처럼 다룰 수 있다.
Strongly Typed Language - 자바처럼 컴파일 시점에 모든 변수와 표현식의 타입이 결정된다.
Pattern Matching - Case 구문의 상위 호환으로, 값의 구조에 따라 조건부 분기를 간결하게 구현할 수 있다.
Type Inference - 타입 시스템이 변수와 표현식의 타입을 추론해 코드의 가독성을 높인다.
Polymorphism - 함수와 데이터 타입이 여러 다른 타입에서도 재사용 할 수 있다.
명시적인 main 함수가 없고, 소스코드를 위에서부터 아래로 순차적으로 읽는 방식으로 실행되지만.. 자바처럼 컴파일 언어에 속하고, REPL을 제공해 스크립트 언어처럼 사용할 수도 있다. (빌드 시스템으로는 dune 사용)
타입은 값의 집합으로 이해하자.
하나의 타입은 특정 집합에 속한 값을 표현하며, 타 프로그래밍 언어에서의 void는 OCaml에서 unit으로 사용된다.
unit 타입은 "()" 하나로 구성된 singleton 집합으로 "아무것도 없음" 을 나타낼 때 사용된다.
공집합은 아니고.. () 자체가 유닛으로 해석된다.
함수형 언어에서, 모든 표현식에 대해 값이 존재해야 한다.
그냥 콘솔에 문자열을 출력하는 함수일지라도 함수형 언어에서는 해당 표현식에 대해 값이 존재해야 하고, 이런 consensus를 지키기 위해 unit 타입을 사용한다.
Primitive Type으로 unit, int, float, char, string, bool을 지원하고, type 키워드로 사용자 지정 타입을 만들어서 사용할 수 있다.
OCaml은 내부적으로 타입 변환을 해 주지 않아 int와 float 간 연산이 불가능하니 필요 시 타입을 맞춰 줘야 한다.
값 비교 시 = 를, 메모리 주소 비교 시 == 연산을 사용한다. (<>와 != 가 각 연산자의 반대 역할을 수행한다)
프로그래밍 언어에서 Statement는 실행될 때 프로그램의 메모리 상태를 변경하고 값을 반환하지 않고, Expression은 실행 시 값으로 계산되며 메모리 상태를 변경하지 않는다.
Expression으로만 구성된 언어는 Immutability가 보장되지만.. OCaml은 Pure Functional Language는 아니라.. 메모리 상태를 변경하는 Expression을 매우 제한적인 형태로 제공한다.
웬만하면 OCaml에서 메모리 상태를 변경하는 일은 거의 없다. 모든 변수는 선언과 동시에 값에 묶여야 하고, 한 번 묶인 변수의 값은 변경할 수 없다.
let square n = let result = n * n in result + 1;;
let in 구문으로 변수의 Scope를 설정한다.
let으로 선언한 변수는 in 다음에 오는 line 까지만 유효하고, = 연산은 기본적으로 equal 연산을 수행하지만 let in 구문에서는 변수 바인딩 역할을 수행한다.
let in 구문이 중첩될 때는 마지막 계산 값이 해당 expression의 결과가 된다. (해당 예시에서는 함수가 결과값)
변수명이 중복되는 경우 Variable Shadowing에 주의하자. 기존 변수는 가려질 뿐 변하지 않는다.
변수를 정의하고 해당 변수를 아래 표현식에서만 사용한다는 뜻으로..
OCaml은 표현식 기반 언어이고, 표현식의 결과를 어디에 쓸 지 명시해야 하기에 in을 사용한다.
파일의 전역 정의나 함수 정의에서는 in을 사용하지 않고 let만 사용해도 괜찮지만 함수 내부에서 변수 바인딩 시 in을 꼭 사용해 줘야 한다.
변수 선언조차 순수한 값이고, 모든 구문이 값을 돌려주는 표현식이니 변수를 바인딩하고 그 변수를 어디에 사용하는지 in으로 명시해 줘야 한다.
let describe_number n = match n with | 0 -> "Zero" | 1 -> "One" | _ -> "Other";;
단순히 표현식을 실행만 하고 반환값을 사용하지 않는 경우 wildcard (_) 를 사용한다.
OCaml에서는 사용하지 않는 변수에 값을 할당하는 행동을 금지하니, 필요 없는 값에는 _ 를 사용해 버려주자. (Print)
let _ = begin print_endline "Hello"; print_endline "World" end;;
expression을 연달아서 사용 시 함수 호출로 해석되니 어디서부터 어디까지가 함수 호출인지 명확하게 구별하자.
여러 표현식을 순차적으로 실행할 때는 ; 또는 begin end 구문을 사용하고, 괄호를 명시적으로 사용해 확실하게 표현하자.
함수의 타입은 type(파라미터) -> type(표현식 타입) 으로 표현한다. (함수의 타입과 함수의 리턴 타입은 다름)
파라미터가 여러 개인 경우 currying이 적용돼 첫 파라미터를 받아 새로운 함수를 반환하는 방식으로 정의된다.
즉.. 함수를 작은 단위로 조립해서 사용할 수 있다.
let add x y = x + y;; val add : int -> int -> int = <fun>
-> 연산은 오른쪽부터 적용돼 int -> ( int -> int ) 이런식으로 묶인 형태로 구성된다.
OCaml에서 Tuple 자료구조는 자료구조 내 서로 다른 타입을 저장할 수 있고, List 자료구조는 자료구조 내 모든 원소의 타입이 같아야 한다.
Tuple을 생성할 때는 ","를 List를 생성할 때는 ";"를 사용한다. (가독성을 위해 괄호를 적절하게 사용하자)
Tuple의 타입은 type * type 타입 곱으로 표현되고, 튜플을 함수의 파라미터로 전달하는 경우도 있다. (연쇄)
List의 타입은 T list 형태로 표현되고, 빈 리스트인 경우 'a list로 표현된다. (polymorphic)
if - then - else 구문을 사용해 분기문을 작성하는데, 이 때 사용되는 반환값은 모두 같아야 한다.
표현식은 항상 하나의 타입을 가져야 하니.. OCaml 사용 시 해당 표현식의 타입이 어떻게 나오는지 항상 생각하자.
OCaml은 모듈 시스템을 지원해 각 ml 파일을 모듈로 인식해서 사용한다.
자바의 클래스와 유사하지만 상태를 가지지 않고, 객체화 하는 것도 불가능하다.
모듈 안에 모듈을 넣는 nested 형태도 가능하고, 모듈명은 대문자로 시작한다.
모듈은 타입이 아니지만, OCaml에서 사용자 정의 타입을 선언할 때는 type 키워드를 사용한다.
type number = Zero | Integer of int | Real of float
예시처럼 여러 개의 타입을 하나의 타입으로 표현할 수 있다.
| 연산자를 사용해 서로 다른 타입을 하나의 타입으로 묶어 Disjoint Union 타입을 구성한다.
함수 오버로딩을 지원하지 않으니.. 모듈을 open 해서 사용할 시 자바의 static import 처럼 모듈명을 생략할 수 있지만 변수명 충돌이 발생할 수 있으니 주의하자.
Pattern Matching
값의 형태를 보고 그 형태에 따라 다른 행동을 수행할 때 사용하는 구문이다.
모든 경우의 수를 고려해야 하고, 고려되지 않은 경우의 수가 있을 때는 컴파일 에러가 발생한다.
패턴이 상수일 때는 계산 값이 패턴과 일치하는지를 검사하고, 패턴이 변수일 때는 계산 값을 변수에 바인딩한다.
즉, 패턴이 변수면 항상 매칭되니 변수를 앞 부분에 위치시키는 경우 매칭 기회를 잃게 된다. (unreachable)
이 때 when 가드를 추가하면 특정 조건을 만족해야지 실행되도록 설정할 수는 있다.
와일드카드를 사용해도 매칭할 수 있는데, 항상 매칭되지만 그 값이 중요하지 않을 때 와일드카드를 사용한다.
컴파일러는 표현식의 타입만 확인한다.
얼핏 보기에 unreachable로 보여도 패턴 매칭에서는 필요한 부분이 있을 수 있으니.. 와일드카드나 변수로 항상 모든 경우의 수를 고려하자.
match lst with | [] -> "Empty list" | x :: xs -> Printf.sprintf "H: %d, T: ..." x;;
:: 연산은 리스트에서 해당 리스트 앞에 원소를 삽입하는 연산이지만, 패턴 매칭에서는 리스트를 분해할 때 사용된다.
x는 첫 번째 원소, xs는 첫 번째 원소를 제외한 나머지 원소가 바인딩된다.
OCaml은 강력한 타입 시스템, 불변성, 패턴 매칭을 지원하는 언어로.. OCaml의 설계 철학은 실수를 줄이고 안전한 프로그램 작성에 집중한다.
자바에서도 OCaml 스타일대로 코드를 작성하면 실수를 줄이고 안전한 프로그램을 만드는 데에 도움이 될 수 있겠지만.. 자바는 기본적으로 Mutable하고 객체지향 패러다임을 중심으로 설계됐고, 패턴 매칭을 지원하지 않는 등 애초에 설계부터가 다르니 완전히 동일한 방식으로 코드를 작성하는건 힘들다.
Immutable 속성을 가지니 더 이상 접근할 수 없는 변수에 대해서는 불필요한 값으로 인식이 자바처럼 GC가 메모리를 관리한다.
let rec fib (n: int) : int = match n with | n when n < 0 -> -1 | 0 -> 0 | 1 -> 1 | _ -> fib (n - 1) + fib (n - 2) let fib_opt (n: int) : int = let rec fib_opt_impl a b cnt = if cnt = 0 then a else fib_opt_impl b (a + b) (cnt - 1) in if n < 0 then -1 else fib_opt_impl 0 1 n let rec last (ls: int list) : int = match ls with | [] -> failwith "The given list is empty" | [x] -> x | _ :: left -> last left let rec second_last (ls: int list) : int = match ls with | [] -> failwith "The given list is empty" | [_] -> failwith "The given list has a sole element" | [x; _] -> x | _ :: left -> second_last left let rec len (ls: int list) : int = match ls with | [] -> 0 | _ :: left -> 1 + len left let rec rev (ls: int list) : int list = match ls with | [] -> [] | first :: left -> rev left @ [first] let is_palindrome (ls: int list) : bool = ls = rev ls let compress (s: string) : (int * char) list = let rec rev (ls: (int * char) list) : (int * char) list = match ls with | [] -> [] | first :: left -> rev left @ [first] in let char_list = List.of_seq (String.to_seq s) in match char_list with | [] -> [] | first :: left -> let rec compress_impl acc_list count prev left_list = match left_list with | [] -> rev ((count, prev) :: acc_list) | head :: tail -> if head = prev then compress_impl acc_list (count + 1) prev tail else compress_impl ((count, prev) :: acc_list) 1 head tail in compress_impl [] 1 first left let sort (f: int -> int -> bool) (ls: int list) : int list = let rec bubble_impl (target_list: int list) (swapped: bool) : (int list * bool) = match target_list with | [] -> ([], swapped) | [first] -> ([first], swapped) | first :: second :: left -> if first = second || f first second then let (rest_list, swapped2) = bubble_impl (second :: left) swapped in (first :: rest_list, swapped2) else let (rest_list, swapped2) = bubble_impl (first :: left) true in (second :: rest_list, swapped2) in let rec bubble_sort lst = let (cur_list, is_swapped) = bubble_impl lst false in if is_swapped then bubble_sort cur_list else cur_list in bubble_sort ls type id = int type tree = Nil | N of id * tree * tree let rec traverse (t: tree) : id list = match t with | Nil -> [] | N (id, left, right) -> (traverse left) @ (traverse right) @ [id]
OCaml에는 루프를 재귀로 처리하는 경우가 많은데 이 부분이 좀 생소하니.. 연습해보자.
'Computer Science > Universial Programming Language' 카테고리의 다른 글
[UPL] Syntax & Parsing (0) | 2025.04.04 |
---|
댓글을 사용할 수 없습니다.