모듈
모듈 (Modules)
프로그램이 커지면 이름을 어떻게 통제하느냐가 핵심이 돼요. 이 장은 Haskell의 모듈(module) 시스템 — 값·데이터타입·클래스를 묶고, 어떤 것을 밖으로 내보내고(export), 다른 모듈에서 무엇을 가져올지(import) 정하는 규칙 — 을 다뤄요. 네임스페이스를 통제하고 큰 프로그램에서 소프트웨어를 재사용하는 장치가 바로 모듈이에요.
본문
모듈은 import들의 집합(다른 모듈에서 스코프로 가져온 자원들)이 만든 환경에서 값·데이터타입·타입 별칭·클래스 등의 모음을 정의해요(4장). 그리고 이 자원 중 일부를 export해서 다른 모듈이 쓸 수 있게 해요. 우리는 모듈에서 정의되거나, import되거나, 어쩌면 export되는 값·타입·클래스를 가리키는 용어로 **개체(entity)**를 써요.
Haskell 프로그램은 모듈들의 모음이에요. 관례상 그중 하나는 반드시 Main이라 불러야 하고, 값 main을 export해야 해요. 프로그램의 값은 모듈 Main의 식별자 main의 값이에요. main은 어떤 타입 τ에 대해 타입 IO τ의 계산이어야 해요(7장). 프로그램이 실행될 때 계산 main이 수행되고, 그 결과(타입 τ)는 버려져요.
모듈은 명시적 import 선언으로 다른 모듈을 참조할 수 있는데, 각 선언은 import할 모듈의 이름과 import할 그 개체들을 지정해요. 모듈은 상호 재귀적(mutually recursive)일 수 있어요.
모듈은 네임스페이스 통제에 쓰이고, 일급 값(first class value)은 아니에요. 다중 모듈 Haskell 프로그램은, 각 개체에 고유 이름을 주고 모든 발생이 적절한 고유 이름을 참조하도록 바꾼 뒤 모든 모듈 본문을 이어붙이면(concatenate) 단일 모듈 프로그램으로 변환될 수 있어요.
예를 들어 여기 세 모듈 프로그램이 있어요:
module Main where
import A
import B
main = A.f >> B.f
module A where
f = ...
module B where
f = ...
이는 다음 단일 모듈 프로그램과 동등해요:
module Main where
main = af >> bf
af = ...
bf = ...
모듈이 상호 재귀적일 수 있기 때문에, 프로그램을 의존성과 무관하게 자유롭게 분할할 수 있어요.
모듈 이름(lexeme modid)은 대문자로 시작하는 식별자 하나 이상이 점으로 구분된 시퀀스인데, 사이에 공백이 없어요. 예를 들어 Data.Bool, Main, Foreign.Marshal.Alloc은 모두 유효한 모듈 이름이에요.
modid → {conid .} conid (modules)
모듈 이름은 계층(hierarchy)으로 배열된 것으로 생각할 수 있는데, 새 성분을 덧붙이면 원래 모듈 이름의 자식이 생겨요. 예를 들어 모듈 Control.Monad.ST는 Control.Monad 서브-계층의 자식이에요. 그러나 이것은 순전히 관례일 뿐이고 언어 정의의 일부가 아니에요. 이 리포트에서 modid는 평면 네임스페이스(flat namespace)를 차지하는 단일 식별자로 취급돼요.
구분된 모듈 하나, Prelude가 있는데, 기본적으로 모든 모듈에 import돼요(5.6절). 그리고 필요한 대로 import할 수 있는 표준 라이브러리 모듈 집합이 있어요(Part II).
5.1 모듈 구조 (Module Structure)
모듈은 값 바인딩·데이터 타입·타입 별칭·클래스 등의 선언을 포함하는 상호 재귀적 스코프를 정의해요(4장).
module → module modid [exports] where body
| body
body → { impdecls ; topdecls }
| { impdecls }
| { topdecls }
impdecls → impdecl1 ; … ; impdecln (n ≥ 1)
topdecls → topdecl1 ; … ; topdecln (n ≥ 1)
모듈은 헤더(header)로 시작해요: 키워드 module, 모듈 이름, 그리고 export할 개체 리스트(둥근 괄호로 감싼). 헤더 다음에는 import할 모듈을 지정하고 선택적으로 import된 바인딩을 제한하는, 아마 빈 import 선언 리스트(impdecls, 5.3절)가 따라와요. 그다음에는 아마 빈 최상위 선언 리스트(topdecls, 4장)가 따라와요.
모듈 본문만으로 이뤄진 축약 형태의 모듈도 허용돼요. 이게 쓰이면 헤더는 'module Main(main) where'로 가정돼요. 축약 모듈의 첫 lexeme이 {가 아니면, 레이아웃 규칙이 모듈의 최상위에 적용돼요.
5.2 export 리스트 (Export Lists)
exports → ( export1 , … , exportn [ , ] ) (n ≥ 0)
export → qvar
| qtycon [(..) | ( cname1 , … , cnamen )] (n ≥ 0)
| qtycls [(..) | ( var1 , … , varn )] (n ≥ 0)
| module modid
cname → var | con
export 리스트는 모듈 선언이 export할 개체들을 식별해요. 모듈 구현은 자신이 선언한 개체나 다른 모듈에서 import한 개체만 export할 수 있어요. export 리스트가 생략되면 모듈에 정의된 모든 값·타입·클래스가 export되지만, import된 것은 export되지 않아요.
export 리스트의 개체는 다음과 같이 이름 붙여질 수 있어요:
- 모듈 본문에서 선언됐든 import됐든, 값·필드 이름·클래스 메서드는 값의 이름을
qvarid로 줌으로써 이름 붙여질 수 있어요. 반드시 스코프 안에 있어야 해요. 연산자는qvarid로 만들기 위해 괄호로 감싸야 해요. data나newtype선언으로 선언된 대수적 데이터타입T는 세 가지 방식 중 하나로 이름 붙여질 수 있어요:- 형태
T는 생성자나 필드 이름 없이 타입만 이름 붙여요. 생성자 없이 타입을 export하는 능력 덕분에 추상 데이터타입을 만들 수 있어요(5.8절). - 형태
T(c1,…,cn)은 타입과 그 생성자·필드 이름의 일부 또는 전부를 이름 붙여요. - 축약 형태
T(..)는 타입과, 현재 스코프에 있는(한정됐든 아니든) 그 모든 생성자·필드 이름을 이름 붙여요. - 모든 경우에 (아마 한정된) 타입 생성자
T는 스코프 안에 있어야 해요. 두 번째 형태의 생성자·필드 이름ci는 비한정(unqualified)이에요. 이런 하위 이름 중 하나는 (a)T의 생성자나 필드를 이름 붙이고, (b) 그 생성자·필드가 한정되든 비한정되든 이름으로 스코프 안에 있을 때에만 합법이에요. 예를 들어 다음은 합법이에요:module A( Mb.Maybe( Nothing, Just ) ) where import qualified Data.Maybe as Mb
- 형태
- 데이터 생성자는 하위 이름(subordinate name)으로서를 빼고 export 리스트에서 이름 붙여질 수 없어요. 그렇지 않으면 타입 생성자와 구별할 수 없기 때문이에요.
type선언으로 선언된 타입 별칭T는 형태T로 이름 붙여질 수 있는데,T가 스코프 안에 있어야 해요.- 클래스 선언으로 선언된, 연산
f1,…,fn을 가진 클래스C는 세 가지 방식 중 하나로 이름 붙여질 수 있어요:- 형태
C는 클래스 메서드 없이 클래스만 이름 붙여요. - 형태
C(f1,…,fn)은 클래스와 그 메서드 일부 또는 전부를 이름 붙여요. - 축약 형태
C(..)은 클래스와 스코프에 있는(한정됐든 아니든) 그 모든 메서드를 이름 붙여요. - 모든 경우에
C는 스코프 안에 있어야 해요. 두 번째 형태에서 (비한정) 하위 이름fi중 하나는 (a)C의 클래스 메서드를 이름 붙이고, (b) 클래스 메서드가 한정되든 비한정되든 이름으로 모듈 본문에 스코프돼 있을 때에만 합법이에요.
- 형태
형태 "module M"은 비한정 이름 "e"와 한정 이름 "M.e" 둘 다로 스코프 안에 있는 모든 개체의 집합을 이름 붙여요. 이 집합은 비어 있을 수 있어요. 예:
module Queue( module Stack, enqueue, dequeue ) where
import Stack
...
여기서 모듈 Queue는 export 리스트에서 모듈 이름 Stack을 써서 Stack에서 import한 모든 개체를 축약해요.
모듈은 "module M" 문법에서 자기 이름을 써서 export 리스트에 자신의 지역 정의를 이름 붙일 수 있어요. 지역 선언이 한정·비한정 이름 둘 다를 스코프로 가져오기 때문이에요(5.5.1절). 예:
module Mod1( module Mod1, module Mod2 ) where
import Mod2
import Mod3
여기서 모듈 Mod1은 모든 지역 정의와 Mod2에서 import한 것들을 export하지만, Mod3에서 import한 것은 export하지 않아요.
M이 export 리스트를 담은 모듈이거나 적어도 하나의 import 선언(한정되든 아니든)이 M을 import하지 않는 한, export 리스트에서 module M을 쓰는 것은 오류예요.
export 리스트는 누적적이에요. export 리스트가 export하는 개체 집합은 리스트 개별 항목들이 export하는 개체들의 합집합이에요.
import하는 모듈에게 개체가 어떻게 export됐는지는 무관해요. 예를 들어 데이터 타입 T의 필드 이름 f는 개별로(f, 위 항목 (1)); 데이터 타입의 명시적으로 이름 붙은 멤버로(T(f), 항목 (2)); 암묵적으로 이름 붙은 멤버로(T(..), 항목 (2)); 또는 전체 모듈을 export해서(module M, 항목 (5)) export될 수 있어요.
모듈이 export하는 개체의 비한정 이름은 (각 네임스페이스 안에서) 모두 구별되어야 해요. 예를 들어:
module A ( C.f, C.g, g, module B ) where -- an invalid module
import B(f)
import qualified C(f,g)
g = f True
모듈 A 자체에는 이름 충돌이 없지만, export 리스트에는 C.g와 g 사이(그리고 module B와 C.f 사이)에 이름 충돌이 있어요.
5.3 import 선언 (Import Declarations)
impdecl → import [qualified] modid [as modid] [impspec]
| (empty declaration)
impspec → ( import1 , … , importn [ , ] ) (n ≥ 0)
| hiding ( import1 , … , importn [ , ] ) (n ≥ 0)
import → var
| tycon [ (..) | ( cname1 , … , cnamen )] (n ≥ 0)
| tycls [(..) | ( var1 , … , varn )] (n ≥ 0)
cname → var | con
한 모듈이 export한 개체는, 모듈 시작 부분의 import 선언으로 다른 모듈의 스코프로 들어올 수 있어요. import 선언은 import할 모듈의 이름을 붙이고, 선택적으로 import할 개체를 지정해요. 하나의 모듈은 둘 이상의 import 선언으로 import될 수 있어요. import된 이름은 최상위 선언 역할을 해요. 모듈 본문 전체에 스코프되지만, 지역 비-최상위 바인딩으로 가려질(shadow) 수 있어요.
여러 import 선언의 효과는 엄격히 누적적이에요. 한 개체는 모듈의 어느 import 선언이든 import하면 스코프 안에 있어요. import 선언의 순서는 무관해요.
어휘적으로 종단 기호 "as", "qualified", "hiding"은 각각 reservedid가 아니라 varid예요. 그것들은 import 선언 문맥에서만 특별한 의미를 가져요. 변수로도 쓸 수 있어요.
5.3.1 무엇이 import 되는가 (What is imported)
정확히 어떤 개체를 import할지는 다음 세 방식 중 하나로 지정할 수 있어요:
- import할 개체를 괄호로 나열해 명시적으로 지정할 수 있어요. 리스트 항목은 export 리스트의 것과 같은 형태인데, 한정자(qualifier)는 허용되지 않고 '
module modid' 개체는 허용되지 않아요. 타입이나 클래스에(..)형태의 import를 쓸 때,(..)는 그 모듈에서 export된 모든 생성자·메서드·필드 이름을 가리켜요. 리스트는 import된 모듈이 export한 개체만 이름 붙여야 해요. 리스트는 비어 있을 수 있는데, 그 경우 인스턴스를 빼고 아무것도 import되지 않아요. hiding(import1 , … , importn )형태를 써서 개체를 제외할 수 있어요. 이것은 이름 붙은 모듈이 export한 모든 개체를, 리스트에 이름 붙은 것들을 빼고 import하라고 지정해요. 데이터 생성자는 연관된 타입을 접두하지 않고 hiding 리스트에서 직접 이름 붙여질 수 있어요. 그래서:
에서 이름이import M hiding (C)C인 어떤 생성자·클래스·타입도 제외돼요. 반대로 import 리스트에서C를 쓰는 것은 클래스나 타입만 이름 붙여요.- 실제로 import된 모듈이 export하지 않는 개체를 hiding하는 것은 오류예요.
- 마지막으로
impspec이 생략되면 지정된 모듈이 export한 모든 개체가 import돼요.
5.3.2 한정 import (Qualified import)
5.3.1절 규칙 아래 import된 각 개체에 대해 최상위 환경이 확장돼요. import 선언이 qualified 키워드를 썼으면 개체의 한정 이름만 스코프로 들어와요. qualified 키워드가 생략되면 개체의 한정·비한정 이름 둘 다 스코프로 들어와요. 5.5.1절이 한정 이름을 더 자세히 설명해요.
import된 이름의 한정자(qualifier)는 import된 모듈의 이름이거나, import 문의 as 절(5.3.3절)이 준 지역 별칭이에요. 따라서 한정자는 반드시 개체가 원래 선언된 모듈의 이름일 필요는 없어요.
비한정 이름을 제외하는 능력 덕분에 프로그래머가 비한정 네임스페이스를 완전히 통제할 수 있어요. 지역 정의 개체가 한정 import와 같은 이름을 공유할 수 있어요:
module Ring where
import qualified Prelude -- All Prelude names must be qualified
import Data.List( nub )
l1 + l2 = l1 Prelude.++ l2 -- This + differs from the one in the Prelude
l1 * l2 = nub (l1 + l2) -- This * differs from the one in the Prelude
succ = (Prelude.+ 1)
5.3.3 지역 별칭 (Local aliases)
import된 모듈은 as 절을 써서 import 모듈에서 지역 별칭을 받을 수 있어요. 예를 들어:
import qualified VeryLongModuleName as C
에서 개체는 VeryLongModuleName. 대신 한정자 'C.'로 참조되어야 해요. 이 또한 VeryLongModuleName 대신 다른 모듈을, import된 모듈에 쓰인 한정자를 바꾸지 않고 대체할 수 있게 해 줘요.
스코프 안의 둘 이상의 모듈이 같은 한정자를 쓰는 것도 합법이에요. 모든 이름이 여전히 모호함 없이 해소될 수 있다면요. 예:
module M where
import qualified Foo as A
import qualified Baz as A
x = A.f
이 모듈은 Foo와 Baz가 둘 다 f를 export하지 않을 때에만 합법이에요.
as 절은 비한정 import 문에도 쓸 수 있어요:
import Foo as A(f)
이 선언은 f와 A.f를 스코프로 가져와요.
5.3.4 예시 (Examples)
위 import 규칙을 명확히 하기 위해, 모듈 A가 x와 y를 export한다고 가정해 보죠. 그러면 이 표는 지정된 import 문이 어떤 이름을 스코프로 가져오는지 보여줘요:
Import declaration Names brought into scope
import A x, y, A.x, A.y
import A() (nothing)
import A(x) x, A.x
import qualified A A.x, A.y
import qualified A() (nothing)
import qualified A(x) A.x
import A hiding () x, y, A.x, A.y
import A hiding (x) y, A.y
import qualified A hiding () A.x, A.y
import qualified A hiding (x) A.y
import A as B x, y, B.x, B.y
import A as B(x) x, B.x
import qualified A as B B.x, B.y
모든 경우에 모듈 A에 스코프된 모든 인스턴스 선언은 import돼요(5.4절).
5.4 인스턴스 선언 import·export (Importing and Exporting Instance Declarations)
인스턴스 선언은 import·export 리스트에서 명시적으로 이름 붙여질 수 없어요. 모듈 안에 스코프된 모든 인스턴스는 항상 export되고, 어떤 import든 import된 모듈에서 모든 인스턴스를 가져와요. 따라서 인스턴스 선언은, import 선언의 체인이 그 인스턴스 선언을 포함하는 모듈로 이어질 때에만 스코프 안에 있어요.
예를 들어 import M()은 모듈 M에서 어떤 새 이름도 스코프로 가져오지 않지만, M에서 보이는 인스턴스는 가져와요. 오직 인스턴스 선언을 제공하는 것이 목적인 모듈은 빈 export 리스트를 가질 수 있어요. 예:
module MyInstances() where
instance Show (a -> b) where
show fn = "<<function>>"
instance Show (IO a) where
show io = "<<IO action>>"
5.5 이름 충돌과 폐포 (Name Clashes and Closure)
5.5.1 한정 이름 (Qualified names)
한정 이름은 modid.name으로 써요(2.4절). 한정 이름은 다음과 같이 스코프로 들어와요:
- 최상위 선언으로. 최상위 선언은 정의되는 개체의 비한정·한정 이름 둘 다를 스코프로 가져와요. 그래서:
은 합법이에요. 정의 발생(defining occurrence)은 비한정 이름을 언급해야 해요. 따라서 다음은 불법이에요:module M where f x = ... g x = M.f x xmodule M where M.f x = ... -- ILLEGAL g x = let M.y = x+1 in ... -- ILLEGAL - import 선언으로. import 선언은 한정되든 아니든 항상 import된 개체의 한정 이름을 스코프로 가져와요(5.3절). 이 덕분에 한정 import를 비한정 import로 교체해도 import된 이름에 대한 참조는 바꿀 필요가 없어요.
5.5.2 이름 충돌 (Name clashes)
모듈이 이름 f나 A.f를 바운드 발생(bound occurrence)으로 포함하면, 그것이 가리키는 개체를 모호함 없이 해소할 수 있어야 해요. 즉 각각 f나 A.f에 대한 바인딩이 하나뿐이어야 해요.
프로그램이 그 이름을 언급하지 않는 한, 그렇게 해소될 수 없는 이름이 존재하는 것은 오류가 아니에요. 예:
module A where
import B
import C
tup = (b, c, d, x)
module B( d, b, x, y ) where
import D
x = ...
y = ...
b = ...
module C( d, c, x, y ) where
import D
x = ...
y = ...
c = ...
module D( d ) where
d = ...
tup의 정의를 고려해 보죠.
b와c에 대한 참조는 각각B에 선언된b와C에 선언된c로 모호함 없이 해소될 수 있어요.d에 대한 참조는D에 선언된d로 모호함 없이 해소돼요. 이 경우 같은 개체가 두 경로(B의 import와 C의 import)로 스코프로 들어오고,A에서 이름d,B.d,C.d로 참조될 수 있어요.x에 대한 참조는 모호해요.B에 선언된x일 수도C에 선언된x일 수도 있어요. 모호함은x참조를B.x나C.x로 바꿔서 고칠 수 있어요.y에 대한 참조는 없어요. 그래서B와C둘 다y라는 다른 개체를 export하는 것은 오류가 아니에요. 오류는y가 실제로 언급될 때만 보고돼요.
타입 시그니처나 fixity 선언에 나타나는 이름은 항상 비한정이고, 같은 선언 리스트의 다른 선언을 모호함 없이 가리켜요(클래스 메서드의 fixity 선언이 최상위에 나타날 수 있다는 점은 빼고 — 4.4.2절). 예를 들어 다음 모듈은 합법이에요:
module F where
sin :: Float -> Float
sin x = (x::Float)
f x = Prelude.sin (F.sin x)
sin에 대한 지역 선언은 합법이에요. Prelude 함수 sin이 암묵적으로 스코프 안에 있어도요. Prelude.sin과 F.sin에 대한 참조는, 어떤 sin을 의미하는지 모호하지 않게 하려면 둘 다 한정되어야 해요. 그러나 F의 첫 줄 타입 시그니처의 비한정 이름 sin은 sin에 대한 지역 선언을 모호함 없이 가리켜요.
5.5.3 폐포 (Closure)
Haskell 프로그램의 모든 모듈은 닫혀 있어야(closed) 해요. 즉 소스 코드가 명시적으로 언급하는 모든 이름은 지역적으로 정의되거나 다른 모듈에서 import돼야 해요. 그러나 컴파일러가 타입 검사나 다른 컴파일 시 분석에 요구하는 개체는, 이름으로 언급되지 않으면 import될 필요가 없어요. Haskell 컴파일 시스템은 프로그래머의 도움 없이 컴파일에 필요한 어떤 정보든 찾아낼 책임이 있어요. 즉 변수 x를 import한다고 해서 x의 시그니처에 있는 데이터타입·클래스가 x와 함께 모듈로 들어와야 하는 것은 아니에요 — 그런 개체가 사용자 프로그램에서 이름으로 참조되지 않는 한 말이에요. Haskell 시스템은 타입 검사나 다른 목적을 위해 개체에 따라와야 하는 정보를 조용히 import해요. 그런 개체는 명시적으로 export될 필요조차 없어요. 다음 프로그램은 T가 M1을 빠져나가지 않는데도 유효해요:
module M1(x) where
data T = T
x = T
module M2 where
import M1(x)
y = x
이 예에서 T가 스코프 안에 없으므로 y에 명시적 타입 시그니처를 줄 방법이 없어요. T가 명시적으로 export되든 아니든, 모듈 M2는 프로그램을 올바르게 타입 검사할 만큼 T에 대해 충분히 알아요.
export된 개체의 타입은 비-export 타입 별칭의 영향을 받지 않아요. 예를 들어:
module M(x) where
type T = Int
x :: T
x = 1
에서 x의 타입은 T이면서 동시에 Int예요. T가 스코프 안에 있지 않아도 이 둘은 상호 교환 가능해요. 즉 T의 정의는 그것을 만나는 어떤 모듈에서든, 이름 T가 스코프 안에 있든 없든 가용해요. T를 export하는 유일한 이유는 다른 모듈이 이름으로 참조할 수 있게 하기 위해서예요. 타입 검사기는 필요한 경우 T의 정의를, export됐든 아니든 찾아내요.
5.6 표준 Prelude (Standard Prelude)
Haskell의 많은 기능은 "표준 Prelude(Standard Prelude)"라 불리는 표준 데이터타입·클래스·함수의 라이브러리로서 Haskell 자체로 정의돼 있어요. Haskell에서 Prelude는 모듈 Prelude에 담겨 있어요. 덜 자주 쓰이는 함수·타입을 제공하는 미리 정의된 라이브러리 모듈도 많아요. 예를 들어 복소수·배열·대부분의 입출력은 모두 표준 라이브러리의 일부예요. 이것들은 Part II에 정의돼 있어요.
라이브러리를 Prelude에서 분리하면 Prelude의 크기와 복잡성을 줄이고, 더 쉽게 흡수되게 하며, 프로그래머가 쓸 수 있는 유용한 이름의 공간을 늘리는 이점이 있어요.
Prelude와 라이브러리 모듈은 다른 모듈과 달라요. 그 의미론(구현이 아니라)이 Haskell 언어 정의의 고정된 부분이기 때문이에요. 이는 예를 들어 컴파일러가 Prelude의 소스 코드를 참조하지 않고 Prelude의 함수 호출을 최적화할 수 있음을 의미해요.
5.6.1 Prelude 모듈 (The Prelude Module)
Prelude 모듈은, 명시적 import 선언으로 import되지 않을 때에만, 마치 'import Prelude' 문처럼 모든 모듈에 자동으로 import돼요. 이 명시적 import 조항 덕분에 Prelude에 정의된 개체를 다른 모듈의 것처럼 선택적으로 import할 수 있어요.
Prelude 개체의 의미론은 9장에 주어진, Haskell로 쓰인 Prelude의 참조 구현으로 명시돼요. 몇몇 데이터타입(Int 같은)과 함수(Int 덧셈 같은)는 Haskell로 직접 명시할 수 없어요. 그런 개체의 처리는 구현에 의존하므로 9장에서 형식적으로 정의되지 않아요. Prelude의 구현은 튜플 처리에서도 불완전해요. 튜플과 그 인스턴스 선언의 무한한 족(infinite family)이 있어야 하지만, 구현은 스킴만 줄 뿐이에요.
9장은 여러 다른 모듈(PreludeList, PreludeIO 등)을 써서 모듈 Prelude를 정의해요. 이 모듈들은 Haskell 98의 일부가 아니고 개별적으로 import될 수 없어요. 단지 Prelude 모듈의 구조를 설명하는 데 도움을 주기 위해 있는 것이에요. 구현의 일부로 간주되어야지, 언어 정의의 일부가 아니에요.
5.6.2 Prelude 이름 가리기 (Shadowing Prelude Names)
Prelude에 관한 규칙은 Prelude 이름을 비표준 목적으로 쓸 수 있도록 짜여 있어요. 다만 그렇게 하는 모든 모듈은 이 비표준 사용을 명시적으로 만드는 import 선언을 가져야 해요. 예:
module A( null, nonNull ) where
import Prelude hiding( null )
null, nonNull :: Int -> Bool
null x = x == 0
nonNull x = not (null x)
모듈 A는 null을 재정의하고, nonNull의 오른쪽 변에서 null에 대한 비한정 참조를 포함해요. 후자는 import Prelude 문의 hiding(null) 없이는 모호할 거예요. A를 비한정으로 import한 다음 null에 비한정 참조를 하는 모든 모듈도 A처럼 null의 모호한 사용을 해소해야 해요. 그래서 Prelude 이름을 실수로 가릴 위험은 거의 없어요.
Prelude 대신 다른 모듈을 만들어 쓸 수도 있어요. 암묵적으로 import된다는 점 빼고 Prelude는 일반 Haskell 모듈이에요. 특별한 것은 Prelude의 어떤 객체가 특별한 구문 구조로 참조된다는 점뿐이에요. Prelude가 쓰는 이름을 재정의해도 이런 특별한 구조의 의미는 바뀌지 않아요. 예를 들어:
module B where
import Prelude()
import MyPrelude
f x = (x,x)
g x = (,) x x
h x = [x] ++ []
에서 명시적 import Prelude() 선언은 Prelude의 자동 import를 막고, import MyPrelude 선언은 비표준 prelude를 스코프로 가져와요. 튜플((x,x)와 (,) 같은)과 리스트([x]와 [] 같은)의 특별한 문법은 계속 표준 Prelude가 정의한 튜플·리스트를 가리켜요. 예를 들어 [x]의 의미를 다른 리스트 구현으로 재정의할 방법은 없어요. 반면 ++의 사용은 특별한 문법이 아니므로 MyPrelude에서 import된 ++를 가리켜요.
그러나 Prelude의 인스턴스 선언을 숨기는 것은 불가능해요. 예를 들어 Show Char에 대한 새 인스턴스를 정의할 수 없어요.
5.7 분리 컴파일 (Separate Compilation)
쓰는 Haskell 구현에 따라, 상호 재귀 모듈의 분리 컴파일은 import된 모듈이, 컴파일되기 전에 참조될 수 있도록 추가 정보를 포함할 것을 요구할 수 있어요. 상호 재귀를 다루려면 모든 export된 값에 대한 명시적 타입 시그니처가 필요할 수 있어요. 분리 컴파일의 정확한 세부 사항은 이 리포트로 정의되지 않아요.
5.8 추상 데이터타입 (Abstract Datatypes)
생성자 없이 데이터타입을 export하는 능력 덕분에 **추상 데이터타입(ADT)**을 만들 수 있어요. 예를 들어 스택용 ADT는 이렇게 정의할 수 있어요:
module Stack( StkType, push, pop, empty ) where
data StkType a = EmptyStk | Stk a (StkType a)
push x s = Stk x s
pop (Stk _ s) = s
empty = EmptyStk
Stack을 import하는 모듈은 그 타입의 생성자에 접근할 수 없으므로 StkType 타입의 값을 구성할 수 없어요. 대신 push, pop, empty를 써서 그런 값을 구성해야 해요.
newtype 선언을 써서 기존 타입 위에 ADT를 세우는 것도 가능해요. 예를 들어 스택을 리스트로 정의할 수 있어요:
module Stack( StkType, push, pop, empty ) where
newtype StkType a = Stk [a]
push x (Stk s) = Stk (x:s)
pop (Stk (_:s)) = Stk s
empty = Stk []
더 알아보기 (Learn more)
- 4장 선언과 바인딩 (Declarations and Bindings) — 모듈이 내보내는 값·타입·클래스의 정의를 봐요.
- 9장 표준 Prelude (Standard Prelude) — 기본으로 import되는
Prelude모듈의 참조 구현이에요. - 2장 어휘 구조 (Lexical Structure) — 한정 이름(
modid.name)의 어휘 규칙을 봐요.