F# 모듈
F# 모듈 (Modules)
F#에서 모듈(module) 은 값, 타입, 함수 값 같은 F# 코드를 하나로 묶는 단위예요. 코드를 모듈로 묶으면 서로 관련된 코드를 함께 두기 쉬워지고, 프로그램 안에서 이름이 충돌하는 문제도 피할 수 있어요. 이 글에서는 모듈을 선언하는 방법부터 중첩 모듈, 재귀 모듈까지 차근차근 살펴볼게요.
출처
원문: Modules - F# | Microsoft Learn
본문
구문 (Syntax)
모듈은 파일 전체를 담는 최상위 모듈과, 파일 일부를 담는 로컬 모듈로 나눠요.
// 최상위 모듈 선언.
module [accessibility-modifier] [qualified-namespace.]module-name
declarations
// 로컬 모듈 선언.
module [accessibility-modifier] module-name =
declarations
설명 (Remarks)
F# 모듈은 타입, 값, 함수 값, do 바인딩 속 코드 같은 F# 코드 구성 요소를 묶는 단위예요. 모듈은 공용 언어 런타임(CLR)에서 정적 멤버만 가지는 클래스로 구현됩니다. 모듈 선언에는 파일 전체를 모듈에 담느냐에 따라 두 종류가 있어요. 하나는 최상위 모듈 선언, 다른 하나는 로컬 모듈 선언이에요. 최상위 모듈 선언은 파일 전체를 모듈에 포함시키며, 파일에서 첫 번째 선언으로만 나타날 수 있어요.
최상위 모듈 선언의 구문에서 qualified-namespace는 모듈을 담고 있는 중첩된 네임스페이스 이름들의 나열이에요. 이 자격 있는 네임스페이스는 미리 선언해 둘 필요가 없어요.
최상위 모듈에서는 선언을 들여쓰지 않아도 돼요. 하지만 로컬 모듈에서는 모든 선언을 반드시 들여써야 해요. 로컬 모듈 선언에서는 그 모듈 선언 아래로 들여쓴 선언들만 그 모듈에 속하게 돼요.
코드 파일이 최상위 모듈 선언이나 네임스페이스 선언으로 시작하지 않으면, 파일의 전체 내용(로컬 모듈 포함)은 파일 이름에서 확장자를 뺀 뒤 첫 글자를 대문자로 바꾼 이름을 가진 암시적으로 만들어진 최상위 모듈의 일부가 돼요. 예를 들어 다음 파일을 살펴볼게요.
// program.fs 파일에 있는 내용.
let x = 40
이 파일은 사실 다음과 같이 작성된 것처럼 컴파일돼요.
module Program
let x = 40
파일 안에 여러 모듈이 있다면 각 모듈마다 로컬 모듈 선언을 사용해야 해요. 감싸는 네임스페이스가 선언되어 있으면 이 모듈들은 그 네임스페이스에 속하고, 감싸는 네임스페이스가 선언되어 있지 않으면 암시적으로 만들어진 최상위 모듈에 속하게 돼요. 다음 코드 예시는 여러 모듈을 담고 있는 코드 파일을 보여줘요. 컴파일러는 암시적으로 Multiplemodules라는 최상위 모듈을 만들고, MyModule1과 MyModule2는 그 최상위 모듈 안에 중첩돼요.
// multiplemodules.fs 파일에 있는 내용.
// MyModule1
module MyModule1 =
// 등호로 선언된 모듈 안의 모든 프로그램 요소는 들여써야 해요.
let module1Value = 100
let module1Function x =
x + 10
// MyModule2
module MyModule2 =
let module2Value = 121
// 자격 있는 이름을 사용해 MyModule1의 함수에 접근해요.
let module2Function x =
x * (MyModule1.module1Function module2Value)
프로젝트나 단일 컴파일에 파일이 여러 개 있거나, 라이브러리를 만들 때는 파일 맨 위에 네임스페이스 선언이나 모듈 선언을 꼭 넣어야 해요. F# 컴파일러가 모듈 이름을 암시적으로 정하는 경우는 프로젝트나 컴파일 명령줄에 파일이 하나뿐이면서 애플리케이션을 만들 때뿐이에요.
accessibility-modifier는 public, private, internal 중 하나가 될 수 있어요. 자세한 내용은 Access Control 문서를 참고하세요. 기본값은 public이에요.
모듈의 코드 참조하기 (Referencing Code in Modules)
다른 모듈의 함수, 타입, 값을 참조할 때는 자격 있는 이름(qualified name) 을 쓰거나 모듈을 열어야(open) 해요. 자격 있는 이름을 쓰려면 접근하려는 프로그램 요소의 네임스페이스, 모듈, 식별자를 모두 지정해야 해요. 자격 있는 경로의 각 부분은 점(.)으로 구분해요.
Namespace1.Namespace2.ModuleName.Identifier
코드를 단순하게 만들고 싶다면 모듈이나 네임스페이스 하나 또는 여러 개를 열 수 있어요. 네임스페이스와 모듈 여는 방법에 대한 자세한 내용은 Import Declarations: The open Keyword 문서를 참고하세요.
다음 코드 예시는 파일 끝까지의 모든 코드를 담고 있는 최상위 모듈을 보여줘요.
module Arithmetic
let add x y =
x + y
let sub x y =
x - y
같은 프로젝트의 다른 파일에서 이 코드를 사용하려면 자격 있는 이름을 쓰거나, 함수를 사용하기 전에 모듈을 열면 돼요. 다음 예시를 볼게요.
// 함수 이름을 완전히 자격 있는 이름으로 써요.
let result1 = Arithmetic.add 5 9
// 모듈을 열어요.
open Arithmetic
let result2 = add 5 9
중첩 모듈 (Nested Modules)
모듈은 중첩될 수 있어요. 내부 모듈은 새 모듈이 아니라 내부 모듈임을 나타내도록 바깥 모듈 선언만큼 들여써야 해요. 예를 들어 다음 두 예시를 비교해 볼게요. 아래 코드에서 모듈 Z는 내부 모듈이에요.
module Y =
let x = 1
module Z =
let z = 5
하지만 다음 코드에서 모듈 Z는 모듈 Y와 형제(sibling) 관계예요.
module Y =
let x = 1
module Z =
let z = 5
다음 코드에서 모듈 Z도 같은 이유로 형제 모듈이에요. 모듈 Y의 다른 선언들만큼 들여쓰지 않았기 때문이에요.
module Y =
let x = 1
module Z =
let z = 5
마지막으로, 바깥 모듈에 선언이 없고 바로 뒤에 다른 모듈 선언이 따라오면, 새 모듈 선언은 내부 모듈로 취급돼요. 하지만 두 번째 모듈 정의가 첫 번째보다 더 들여쓰지 않으면 컴파일러가 경고를 띄워요.
// 이 코드는 경고를 만들지만 Z를 내부 모듈로 취급해요.
module Y =
module Z =
let z = 5
경고를 없애려면 내부 모듈을 들여써야 해요.
module Y =
module Z =
let z = 5
파일의 모든 코드를 단일 바깥 모듈에 담고 그 안에 내부 모듈을 두고 싶다면, 바깥 모듈에는 등호가 필요 없고 내부 모듈 선언을 포함한 바깥 모듈에 들어갈 선언들도 들여쓰지 않아도 돼요. 다만 내부 모듈 선언 안쪽의 선언은 반드시 들여써야 해요. 다음 코드가 이 경우를 보여줘요.
// 파일 이름이 TopLevel.fs나 topLevel.fs이고 애플리케이션에서 유일한 파일이면
// 최상위 모듈 선언을 생략할 수 있어요.
module TopLevel
let topLevelX = 5
module Inner1 =
let inner1X = 1
module Inner2 =
let inner2X = 5
재귀 모듈 (Recursive modules)
F# 4.1부터는 담겨 있는 모든 코드가 서로 재귀적인(상호 참조 가능한) 모듈을 만들 수 있게 됐어요. 이건 module rec로 작성해요. module rec를 쓰면 타입과 모듈 사이에서 서로 참조하는 코드를 작성하지 못해 생기던 여러 불편을 덜 수 있어요. 다음이 그 예시예요.
module rec RecursiveModule =
type Orientation = Up | Down
type PeelState = Peeled | Unpeeled
// 이 예외는 아래의 타입에 의존해요.
exception DontSqueezeTheBananaException of Banana
type Banana(orientation : Orientation) =
member val Orientation = orientation with get, set
member val Sides: PeelState list = [ Unpeeled; Unpeeled; Unpeeled; Unpeeled ] with get, set
member self.IsPeeled =
self.Sides |> List.forall ((=) Peeled)
member self.Peel() =
BananaHelpers.peel self
|> fun peeledSides -> self.Sides <- peeledSides
member self.SqueezeJuiceOut() =
raise (DontSqueezeTheBananaException self)
module BananaHelpers =
let peel (banana: Banana) =
let flip (banana: Banana) =
match banana.Orientation with
| Up ->
banana.Orientation <- Down
banana
| Down -> banana
let peelSides (banana: Banana) =
banana.Sides
|> List.map (function
| Unpeeled -> Peeled
| Peeled -> Peeled)
banana |> flip |> peelSides
여기서 DontSqueezeTheBananaException 예외와 Banana 클래스가 서로를 참조하는 걸 볼 수 있어요. 또 BananaHelpers 모듈과 Banana 클래스도 서로를 참조해요. RecursiveModule 모듈에서 rec 키워드를 빼면 이런 방식은 F#에서 표현할 수 없어요.
이 기능은 F# 4.1의 네임스페이스에서도 사용할 수 있어요.