주류 언어 바깥에 있는 모듈 시스템의 개념을 OCaml을 통해 살펴보고, 시그니처, 포함(include), 중첩 모듈, 그리고 펑터(functor)까지 모듈형 추상화가 제공하는 표현력을 정리한다.
모든 성공한 프로그래밍 언어는 서로 비슷하지만, 성공하지 못한 프로그래밍 언어는 각자 저마다의 방식으로 성공하지 못한다.
소프트웨어의 역사는 동시에 진행되지만 분리된 또 하나의 대화와 나란히 흐른다. 그것은 우리가 계산을 어떻게 생각하는지, 그리고 그 아이디어를 다른 사람들과 어떻게 소통하는지에 대한 이야기다. 그리고 계산의 이야기는 우리가 코드라고 부르는, 매우 새롭고 기묘한 인간 표현 형식의 진화에 관한 이야기이기도 했다. 나는 21세기의 프로그래머란 기원전 3200년 무렵 고대 이집트의 왕실 서기관과 비슷한 처지일 것이라고 종종 생각한다. 대부분의 사람들은 알지 못하는 새로운 소통 방식이 존재하지만, 그 존재 자체가 동시에 상업과 문화와 문명이 번성하도록 만든다.
그리고 자연어에서 보아왔듯이, 의미론과 문법이 점차 수렴하여 유럽 언어에서 라틴어가 차지하던 위치와 비슷한 무언가로 향하고 있다. 이 언어들은 C에서 영감을 받은 명령형 패러다임에 약간의 객체지향 기능이 섞인 범주에 들어간다. C++, Go, Python, Ruby, Rust, Java, Javascript 같은 언어들이 여기에 해당하며, 이들이 합쳐 오늘날 작성되는 신규 코드의 대부분을 이룬다. 프로그래밍 활동의 상당 부분이 이 생산성의 국소 최댓값으로 수렴해버린 한편, 주류의 바깥 경계에는 훨씬 더 난해하고, 주류에서 발견되는 의미론과 문법과는 크게 다른 아이디어를 담은 언어들도 존재한다.
전문 프로그래머로 살아오며 나는 프로그래머가 크게 두 부류로 나뉜다는 것을 깨달았다. 프로그래밍 언어를 주로 인간 이성의 도구로 보는 사람들과, 특정 작업을 수행하기 위한 생산 수단으로 보는 사람들이다. Pirahã, Navajo, Klingon, Berik처럼 화자가 극히 제한된 자연어가 있듯이, 채택이 제한적인 프로그래밍 언어들도 있다. 그럼에도 그런 언어들은 매우 흥미로운 의미론적 구조를 담고 있으며, 이를 주류 언어로 옮겨 담으려 할 때 종종 번역 과정에서 사라져버리곤 한다.
Advent Blogging(심심하고 봉쇄 중이라서)을 하면서, 소프트웨어 문화의 주변부에 있는 일곱 가지 언어 의미론 기능에 대해 써보려 한다. 로마인(즉 C)이 정복하지 않았다면 어땠을지라는 상상을 따라, 기상천외한 아이디어들로 여행을 떠나게 해줄 것들이다.
모듈 시스템과 모듈형 프로그래밍부터 시작해보자. 모듈의 핵심 아이디어는 코드를 모듈이라 불리는 재사용 가능한 구성 요소로 쪼개는 것이다. 언어 기능으로서의 모듈은 Modula-2와 Pascal에서 처음 발전했는데, 이는 컴파일 단위를 구분하기 위한 방법으로 개발되었다. 이 개념은 1984년 Standard ML에서 성숙했으며, 모듈의 추상화와 매개변수화를 가능하게 하도록 더 발전했다. 오늘날 완전한 모듈 시스템은 F#, OCaml, Standard ML 같은 ML 계열 언어들에서 볼 수 있고, Agda처럼 이를 갖춘 다른 언어도 몇몇 있다. 여기서는 타입 추론을 갖춘 정적 타입 ML 방언인 OCaml을 살펴보겠다.
module MyModule = struct
(* Set of Definitions *)
end
모듈 안에 담길 수 있는 정보는 값과 타입 모두다. 예를 들어 타입 t와, 단일 인자를 받는 함수 square가 있다.
module MyModule = struct
type t = int
let square x = x * x
end
module MyModule = struct
type t = int
let square x = x * x
end
모듈의 구성 요소는 시그니처(signature)라고 불리는 기술(description)에 바인딩될 수 있는데, 이는 모듈 내부 심볼의 가시성을 제약하고 모듈 전반에 걸쳐 일관된 인터페이스를 강제한다.
module MyModule : sig
val square : int -> int
end =
struct
let square x = x * x
end
또는 모듈 시그니처를 타입 레벨에서 특정 이름에 바인딩하고, module type 문법을 사용해 여러 모듈에 걸쳐 독립적으로 정의할 수도 있다. 이는 명세(specification)와 구현(implementation)을 분리한다. 여기서는 추상 타입 t를 정의하는데, 이는 시그니처 내부에서 추상적으로 참조되어 이후 구체 타입으로 인스턴스화되는 것을 가리키게 된다. 항 s는 명세에서 “타입 t의 값”으로 정의된다.
(* Specification of MySig *)
module type MySig = sig
(* Set of Type Signatures *)
type t
val s : t
end
(* Implementation of MySig *)
module MyModule : MySig = struct
(* Set of Definitions *)
type t = int
let s = 0
end
모듈은 open 문법으로 열 수 있다. 이는 노출된 모든 심볼을 해당 스코프 안에서(또는 toplevel에서 사용하면 전역으로) 스코프에 들여온다.
open MyModule;; (* Toplevel *)
let open MyModule in expr;; (* In let binding *)
또한 점(dot) 문법으로 모듈 안으로 투영(projection)하여 모듈 스코프의 특정 심볼을 가져올 수도 있다. 예를 들어:`
module Foo = struct
let a = 5
let b = 6
end;;
print_int(Foo.a);;
print_int(Foo.(a*b));;
모듈의 시그니처는 모듈 안에서 정의된 모든 심볼을 반드시 제약할 필요는 없다. 시그니처는 특정 매개변수나 타입의 구현을 봉인(seal)하여, 모듈 내부의 구현 세부사항을 외부로 노출하지 않을 수 있다. 예를 들어 다음 모듈에서 Hello 시그니처는 hello 함수가 사용하는 message를 숨기며, 다운스트림 사용자가 message의 내부를 수정할 수 없게 한다. 사용자는 함수를 호출할 수만 있다.
module type Hello = sig
val hello : unit -> unit
end
module Impl : Hello = struct
(* Private variable message *)
let message = "Messaage is sealed Inside Impl"
let hello () = print_endline message
end;;
Impl.hello();;
모듈 자체도 다른 모듈 안에 임의의 깊이로 중첩될 수 있다. 이 예시에서는 Outer 모듈로 투영하여 Inner 모듈과 그 내용을 가져올 수 있다.
module Outer = struct
let a = 1
module Inner = struct
let b = 2
end
end
let c = Outer.Inner.b;;
중첩뿐만 아니라, 모듈은 다른 모듈의 내용을 포함할 수도 있다. 이는 모듈 정의 내부에서 일시적으로 값들을 스코프에 들여오거나, include 문법을 사용해 주어진 모듈 스코프의 내용을 새 정의로 복사하는 방식으로 가능하다.
module A = struct
let a = 10
end
module B = struct
let b = 20
end
module Arith1 = struct
include A
include B
let c = a+b
end
module Arith2 = struct
open A
open B
let c = a+b
end;;
Arith1.a;;
Arith2.a;; (* Not in scope *)
모듈 자체는 타입(그리고 다른 모듈들)을 매개변수로 받아 매개변수화될 수도 있다. 이를 펑터(functor)라고 하며, 모듈 매개변수가 반드시 만족해야 하는 인터페이스를 지정함으로써 모듈을 인스턴스화할 수 있게 해준다. 이 예시에서 매개변수 M은 시그니처 S를 준수하는 추상 모듈이다. 주어진 모듈 A는 이 시그니처의 구현이며, FA를 인스턴스화할 때 매개변수 M으로 전달될 수 있다. F의 정의 안에서는 M으로 투영하여 s와 t 매개변수를 추상적으로 가져온다. 펑터는 사실상 모듈에서 모듈로 가는 함수다.
module type S = sig
type t
val s : t
end
module A : S = struct
type t = int
let s = 0
end
(* Functor *)
module F (M : S) = struct
type a = M.t
let b = M.s
end
(* F applied to module A *)
module FA = F(A);;
보통 동일한 펑터를 두 번 호출하면, 동일한 추상 타입을 담는 모듈들이 생성된다. 하지만 추가 매개변수 ()로 표시되는 생성적 펑터(generative functor)라는 다른 부류의 펑터를 정의할 수도 있는데, 이는 서로 같지 않은 추상 타입을 담은 출력 모듈을 만든다. 이 예시에서 F와 G의 출력 타입은 서로 구분된다. 이는 특히 변이와 유일성이 필요한 참조(reference)를 다룰 때 중요하다.
module G (M : S) () = struct
type a = M.t
let b = M.s
end
module GA = G(A)();;
이것이 모듈 시스템의 핵심이다. 많은 언어들이 이름공간을 제공하고 코드 단위를 캡슐화하는 간단한 방법을 갖고 있지만, ML 계열은 모듈 자체를 1차 객체로 만들고 매개변수화 가능하게 함으로써 이를 풍부하게 만든다. 이는 다른 곳에서는 좀처럼 보기 드문 매우 강력한 기법이며, 펑터로 프로그래밍한다는 개념은 코드 재사용을 위한 풍부한 추상화들의 집합을 낳는다. 모듈 시스템은 함수형 프로그래밍의 필수 요소이며, 미래의 언어들은 이 풍부한 설계 공간에서 배워야 하고 새로운 가지들을 탐색해야 한다.
External References