Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

이색적인 프로그래밍 아이디어: 1부 (모듈 시스템)

성공한 프로그래밍 언어는 모두 서로 닮았지만, 성공하지 못한 프로그래밍 언어는 저마다의 방식으로 성공하지 못합니다.

소프트웨어의 역사는 동시에 진행되지만 별개인 또 하나의 대화와 나란히 흘러왔는데, 그것은 우리가 계산을 어떻게 사고하고 그 아이디어를 다른 사람들과 어떻게 소통하는지에 관한 이야기입니다. 그리고 계산의 이야기는 우리가 코드라고 부르는, 매우 새롭고 독특한 인간 표현 형식이 진화해 온 이야기였습니다. 21세기에 프로그래머로 산다는 것은 기원전 3200년 고대 이집트에서 왕실 서기로 사는 것과 비슷할 것이라고 생각합니다. 대부분의 사람들은 알지 못하는 새로운 소통 방식이 있지만, 그 존재 덕분에 상업과 문화와 문명이 동시에 번성할 수 있습니다.

그리고 자연어에서와 마찬가지로, 우리는 의미론과 문법이 한곳으로 수렴하여 마치 유럽 언어에서 라틴어가 차지했던 것과 같은 존재가 되어 가는 모습을 보아 왔습니다. 이러한 언어들은 C에서 영감을 받은 명령형 패러다임에 몇 가지 객체 지향 기능을 더한 패러다임에 속합니다. 여기에는 C++, Go, Python, Ruby, Rust, Java, Javascript 같은 언어가 해당하며, 이들이 오늘날 새로 작성되는 코드의 대부분을 차지합니다. 프로그래밍 활동의 대부분이 이러한 생산성의 국소 최댓값으로 수렴했지만, 프로그래밍의 변두리에는 주류에서 볼 수 있는 것과는 전혀 다른 의미론과 문법을 지닌, 보다 난해한 아이디어들이 존재합니다.

전문 프로그래머로 살아오면서, 저는 프로그래머에게 서로 다른 두 부류가 있다는 것을 깨달았습니다. 프로그래밍 언어를 주로 인간 이성의 도구로 보는 사람들과, 특정 작업을 위한 생산 수단으로 보는 사람들입니다. 피라항어, 나바호어, 클링온어, 베릭어를 구사하는 사람이 극히 적은 것처럼, 그만큼 채택이 제한적인 프로그래밍 언어들도 있습니다. 그럼에도 이러한 언어들은 매우 흥미로운 의미 구조를 담고 있으며, 이를 주류 언어로 옮기려 할 때 그 구조는 번역 과정에서 사라지는 경우가 많습니다.

어드벤트 블로깅으로 (지루하기도 하고 봉쇄 중이기도 해서) 소프트웨어 문화의 변두리에 있는 일곱 가지 언어 의미론 기능에 대해 쓰고자 합니다. 로마인(즉, C)이 정복하지 않았다면 어떤 모습이었을지를 탐험하는 여행으로 여러분을 안내할, 엉뚱하고 기발한 아이디어를 지닌 것들입니다.

모듈 시스템

모듈 시스템과 모듈식 프로그래밍부터 시작합시다. 모듈의 핵심 아이디어는 코드를 모듈이라고 부르는 재사용 가능한 구성 요소로 나누는 것입니다. 언어 기능으로서의 모듈은 Modula-2와 Pascal에서 처음 개발되었으며, 컴파일 단위를 구분하는 방법으로 고안되었습니다. 이 개념은 1984년 Standard ML 언어에서 성숙기를 맞이했는데, 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

모듈의 구성 요소는 시그니처라고 하는 명세에 묶일 수 있으며, 시그니처는 모듈 내 심볼의 가시성을 제한하는 동시에 모듈 전체에 일관된 인터페이스를 강제합니다.

module MyModule : sig
 val square : int -> int
end =
struct
  let square x = x * x
end

또는 module type 구문을 사용하면 모듈 시그니처를 타입 수준에서 특정 이름에 바인딩하고, 여러 모듈에 걸쳐 독립적으로 정의할 수 있습니다. 이렇게 하면 명세가 구현과 분리됩니다. 여기서는 추상 타입 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 구문을 사용하여 열 수 있습니다. 이렇게 하면 모듈 안에서 노출된 모든 심볼이 주어진 스코프 안으로 들어오며, 최상위 수준에서 사용하면 전역으로 들어옵니다.

open MyModule;;                (* Toplevel *)
let open MyModule in expr;;    (* In let binding *)

또한 점 구문으로 모듈에 투영하여 모듈 스코프 안의 특정 심볼을 가져올 수도 있습니다. 예를 들면 다음과 같습니다:`

module Foo = struct
  let a = 5
  let b = 6
end;;

print_int(Foo.a);;
print_int(Foo.(a*b));;

모듈의 시그니처가 모듈에 정의된 모든 심볼을 반드시 제약해야 하는 것은 아닙니다. 시그니처는 특정 매개변수나 타입의 구현을 봉인하여, 그 구현 세부 사항을 모듈 내부에 두고 외부에 공개하지 않을 수 있습니다. 예를 들어 다음 모듈에서 Hello 시그니처는 hello 함수가 사용하는 메시지를 숨기므로, 하위 사용자는 메시지 자체의 내부를 수정할 수 없고 함수를 호출하는 것만 할 수 있습니다.

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 *)

모듈 자체는 다른 모듈을 포함한 타입으로 매개변수화될 수 있습니다. 이를 함자라고 하며, 주어진 모듈 매개변수가 따라야 하는 인터페이스를 지정하여 모듈을 인스턴스화할 수 있게 해 줍니다. 이 예제에서 매개변수 M은 시그니처 S를 따르는 추상 모듈입니다. 주어진 모듈 A가 이 시그니처를 구현하며, 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);;

일반적으로 같은 함자를 두 번 호출하면 동일한 추상 타입을 담은 모듈이 만들어집니다. 그러나 생성적 함자라는 다른 종류의 함자를 정의할 수도 있습니다. 생성적 함자는 추가 매개변수 ()로 표시하며, 서로 같지 않은 추상 타입을 담은 출력 모듈을 만들어 냅니다. 이 예제에서 F와 G의 출력 타입은 서로 다릅니다. 이는 변경과 유일성이 필요한 참조를 다룰 때 특히 중요합니다.

module G (M : S) () = struct
  type a = M.t
  let b = M.s
end

module GA = G(A)();;

이것이 모듈 시스템의 본질입니다. 많은 언어가 코드 단위의 이름 공간을 나누고 캡슐화하는 간단한 방법을 제공하지만, ML 계열은 모듈 자체를 일급 객체이면서 매개변수화할 수 있는 대상으로 만들어 이를 더욱 풍부하게 합니다. 이는 다른 곳에서는 거의 볼 수 없는 매우 강력한 기법이며, 함자를 이용한 프로그래밍이라는 개념은 코드 재사용을 위한 풍부한 추상화를 낳습니다. 모듈 시스템은 함수형 프로그래밍의 핵심 요소이며, 앞으로의 언어들은 이를 배우고 이 풍부한 설계 공간에서 새로운 갈래를 탐구해야 합니다.

외부 참고 자료


원문 © Stephen Diehl, stephendiehl.com · 글 CC BY-SA 4.0, 코드 MIT · Stephen Diehl은 런던의 소프트웨어 엔지니어이자 작가입니다(저자가 정한 소개 문구). 이 글은 저자가 개인 자격으로 쓴 것이며 어떤 조직과의 연관도 밝히거나 암시해서는 안 됩니다 (라이선스와 고지).
이 페이지는 원문을 한국어로 옮기며 변경한 비공식 번역이며 같은 CC BY-SA 4.0으로 제공합니다 · yeokja 한국어 번역 · Anthropic 사의 claude-opus-5-5 모델을 활용하여 번역되었으며 학습을 모두 비허용한 상태로 작업하였습니다.