모듈

모듈 (Modules)

Haskell 2010 Report 5장에서는 프로그램을 모듈(module) 단위로 나누고, 그 모듈 사이에서 이름을 어떻게 공유하고 감추는지를 정의해요. 자연스러운 말로 하면, "이 파일에 있는 것들 중 어떤 걸 바깥에 보여줄지, 어떤 걸 다른 모듈에서 들여올지"를 정하는 규칙이에요. 이 장에서는 모듈의 구조, 내보내기(export) 목록, 들여오기(import) 선언, 이름 충돌과 클로저(closure), 표준 프렐류드(Prelude), 그리고 추상 데이터 타입(ADT)까지 차근차근 다뤄볼게요.

출처: https://www.haskell.org/onlinereport/haskell2010/haskellch5.html

본문

모듈은 값, 데이터 타입, 타입 시노님(type synonym), 클래스 등의 모음을 정의해요. 이 모음은 일련의 import(다른 모듈에서 스코프 안으로 들여온 자원)가 만든 환경 안에 놓이죠. 모듈은 이 자원 중 일부를 export해서 다른 모듈에서 쓸 수 있게 해줘요. 여기서 **엔티티(entity)**라는 용어는 모듈에 정의되었거나, 들여왔거나, 내보내진 값·타입·클래스를 가리켜요.

Haskell 프로그램은 모듈들의 모음인데, 그중 하나는 관례상 Main이라 불러야 하고 값 main을 반드시 내보내야 해요. 프로그램의 값은 Main 모듈에 있는 식별자 main의 값이에요. main은 어떤 타입 τ에 대해 타입 IO τ인 계산이어야 해요(7장 참조). 프로그램이 실행되면 이 계산 main이 수행되고, 그 결과(τ 타입)는 버려져요.

모듈은 명시적인 import 선언을 통해 다른 모듈을 참조할 수 있는데, 각 선언은 들여올 모듈의 이름과 함께 들여올 엔티티를 지정해요. 모듈은 서로 **재귀적(mutually recursive)**일 수 있어요.

모듈은 이름 공간을 제어하기 위해 쓰이며, 일급 값(first class value)은 아니에요. 다중 모듈 Haskell 프로그램은 각 엔티티에 고유한 이름을 붙이고, 모든 등장을 해당 고유 이름을 가리키도록 바꾼 뒤, 모듈 본문을 모두 이어 붙이면 하나의 모듈로 바꿀 수 있어요¹. 예를 들어, 다음은 세 모듈로 이루어진 프로그램이에요.


  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)은 대문자로 시작하는 식별자 하나 이상을 점(dot)으로 구분하고 공백 없이 이어 붙인 형태예요. 예를 들어 Data.Bool, Main, Foreign.Marshal.Alloc은 모두 유효한 모듈 이름이에요.

| modid | → | {conid .} conid | (modules) |

모듈 이름은 계층 구조로 배열되어 있다고 생각할 수 있어요. 새 구성 요소를 덧붙이면 원래 모듈 이름의 자식이 만들어지죠. 예를 들어 Control.Monad.ST 모듈은 Control.Monad 하위 계층의 자식이에요. 다만 이건 순전한 관례일 뿐 언어 정의의 일부는 아니에요. 이 보고서에서는 modid를 평평한(flat) 이름 공간을 차지하는 단일 식별자로 취급해요.

특별하게 취급되는 모듈이 하나 있어요. Prelude가 그것인데, 기본적으로 모든 모듈에 들여와져요(5.6절 참조). 여기에 필요에 따라 들여올 수 있는 표준 라이브러리 모듈 모음이 더해져요(II부 참조).

Module Structure (모듈 구조)

모듈은 값 바인딩, 데이터 타입, 타입 시노님, 클래스 등에 대한 선언을 담는 **서로 재귀적인 스코프(mutually recursive scope)**를 정의해요(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, 모듈 이름, 그리고 내보낼 엔티티 목록(소괄호로 감쌈)이 순서대로 오죠. 그 뒤에는 들여올 모듈을 지정하고 선택적으로 들여오는 바인딩을 제한하는, import 선언 목록(impdecls, 5.3절)이 올 수 있어요(비어 있어도 됨). 이어서 최상위 선언 목록(topdecls, 4장)이 옵니다(비어 있어도 됨).

모듈 본문만으로 이루어진 축약 형태의 모듈도 허용돼요. 이 형태를 쓰면 헤더는 module Main(main) where로 간주돼요. 축약 모듈의 첫 lexeme이 {가 아니라면, 모듈 최상위에 layout 규칙이 적용돼요.

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 목록이 생략되면 모듈에 정의된 값·타입·클래스는 전부 내보내지지만, 들여온 것들은 내보내지지 않아요.

export 목록의 엔티티는 다음과 같이 이름을 붙일 수 있어요.

  • 값, 필드 이름, 클래스 메서드는 모듈 본문에서 선언됐든 들여왔든 관계없이, qvarid로 이름을 주어 이름 붙일 수 있어요. 이 qvarid는 스코프 안에 있어야 해요. 연산자(operator)는 소괄호로 감싸서 qvarid로 만들어야 해요.
  • data 또는 newtype 선언으로 선언된 대수 데이터 타입(algebraic datatype) T는 다음 세 가지 방식 중 하나로 이름 붙일 수 있어요.
    • T 형태는 타입만 이름 붙이고 생성자(constructor)나 필드 이름은 이름 붙이지 않아요. 타입을 생성자 없이 내보낼 수 있다는 점이 추상 데이터 타입을 만들 수 있게 해주는 핵심이에요(5.8절 참조).
    • T(c1,…,cn) 형태는 타입과 생성자·필드 이름의 일부 또는 전부를 이름 붙여요.
    • 축약 형태 T(..)는 타입과, 현재 스코프 안에 있는(한정(qualified)이든 아니든) 모든 생성자·필드 이름을 이름 붙여요.
    • 어떤 경우든 (한정될 수 있는) 타입 생성자 T가 스코프 안에 있어야 해요. 두 번째 형태의 생성자·필드 이름 ci는 한정되지 않아요. 이 보조 이름들 중 하나가 유효하려면, (a) T의 생성자 또는 필드를 이름 붙이고, (b) 그 생성자·필드가 한정이든 비한정이든 스코프 안에 있을 때뿐이에요. 예를 들어 다음은 유효해요.
 
  module A( Mb.Maybe( Nothing, Just ) ) where
 
    import qualified Data.Maybe as Mb

데이터 생성자는 보조 이름으로만 export 목록에 이름 붙일 수 있어요. 그 외의 방식으로는 타입 생성자와 구분할 수 없기 때문이에요.

  • type 선언으로 선언된 타입 시노님 TT로 이름 붙일 수 있으며, T는 스코프 안에 있어야 해요.
  • class 선언으로 선언된, 연산 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에서 들여온 모든 엔티티를 축약해요.

모듈은 자체 로컬 정의를 export 목록에서 "module M" 문법으로 자기 이름을 써서 이름 붙일 수 있어요. 로컬 선언은 한정 이름과 비한정 이름을 모두 스코프 안으로 끌어들이기 때문이에요(5.5.1절 참조). 예:

 
  module Mod1( module Mod1, module Mod2 ) where
 
  import Mod2
 
  import Mod3

여기서 Mod1 모듈은 모든 로컬 정의와 함께 Mod2에서 들여온 것들은 내보내지만 Mod3에서 들여온 것들은 내보내지 않아요.

export 목록에서 module M을 쓰는데 M이 export 목록을 가진 모듈 자체가 아니거나, 최소 하나의 import 선언(한정이든 비한정이든)으로 들여와지지 않았다면 오류예요.

export 목록은 **누적적(cumulative)**이에요. export 목록이 내보내는 엔티티 집합은 목록의 개별 항목이 내보내는 엔티티의 합집합이에요.

들여오는 모듈 입장에서는 엔티티가 어떻게 내보내졌는지는 전혀 상관없어요. 예를 들어 데이터 타입 T의 필드 이름 f는 단독으로(f, 위 항목 (1)), 데이터 타입의 명시적 이름이 붙은 멤버로(T(f), 항목 (2)), 암시적 멤버로(T(..), 항목 (2)), 또는 모듈 전체를 내보내는 방식으로(module M, 항목 (5)) 내보낼 수 있어요.

모듈이 내보내는 엔티티의 비한정 이름은 (각각의 이름 공간 안에서) 모두 서로 달라야 해요. 예를 들어

 
  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.gg 사이에 이름 충돌이 있어요(C.gg가 서로 다른 엔티티라고 가정 — 기억하세요, 모듈은 서로 재귀적으로 들여올 수 있어요). module BC.f 사이에도 충돌이 있어요(B.fC.f가 서로 다른 엔티티라고 가정).

Import Declarations (import 선언)

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

모듈이 내보낸 엔티티는 모듈 시작 부분의 import 선언으로 다른 모듈의 스코프 안으로 끌어들일 수 있어요. import 선언은 들여올 모듈의 이름과, 선택적으로 들여올 엔티티를 지정해요. 하나의 모듈을 둘 이상의 import 선언으로 들여올 수도 있어요. 들여온 이름은 최상위 선언 역할을 해요. 즉 모듈 본문 전체에 걸쳐 스코프를 갖지만 로컬 비최상위 바인딩에 가려질(shadowed) 수 있어요.

여러 import 선언의 효과는 엄격히 누적적이에요. 어떤 엔티티가 모듈의 어느 import 선언이든 하나로 들여와지기만 하면 스코프 안에 있어요. import 선언의 순서는 무관해요.

어휘적으로, 터미널 심볼 "as", "qualified", "hiding"은 각각 reservedid가 아니라 varid예요. 이들은 import 선언이라는 맥락에서만 특별한 의미를 가지며, 변수로도 쓸 수 있어요.

What is imported (무엇이 들여오지나)

정확히 어떤 엔티티를 들여올지는 다음 세 가지 방식 중 하나로 지정할 수 있어요.

  • 들여올 엔티티를 괄호 안에 나열해서 명시적으로 지정할 수 있어요. 목록의 항목은 export 목록과 같은 형태인데, 한정자(qualifier)는 허용되지 않고 "module modid" 엔티티도 허용되지 않아요. 타입이나 클래스에 (..) 형태의 import를 쓰면, 그 (..)는 모듈에서 내보낸 모든 생성자·메서드·필드 이름을 가리켜요.
    • 목록은 들여올 모듈이 내보낸 엔티티만 이름 붙여야 해요. 목록은 비어 있을 수 있는데, 그 경우 인스턴스(instance)를 제외한 아무것도 들여오지 않아요.
  • hiding(import1,…,importn) 형태를 써서 엔티티를 제외할 수 있어요. 이 형태는 이름 붙인 모듈이 내보낸 모든 엔티티 중 목록에 있는 것들을 제외하고 전부 들여오라는 뜻이에요. 데이터 생성자는 관련 타입을 앞에 붙이지 않고 hiding 목록에 직접 이름 붙일 수 있어요. 따라서
    
    import M hiding (C)
    
    
    에서 C라고 이름 붙은 생성자·클래스·타입은 모두 제외돼요. 반면 import 목록에서 C를 쓰는 것은 클래스나 타입만을 이름 붙여요.
    • 실은 내보내지 않는 엔티티를 hiding하는 것은 오류예요.
  • 마지막으로, impspec이 생략되면 지정한 모듈이 내보낸 모든 엔티티를 들여와요.

Qualified import (한정 import)

5.3.1절 규칙으로 들여온 각 엔티티에 대해 최상위 환경이 확장돼요. import 선언이 qualified 키워드를 썼다면, 엔티티의 한정 이름만 스코프 안으로 들어와요. qualified 키워드가 생략되면 엔티티의 한정 이름과 비한정 이름이 모두 들어와요. 5.5.1절에서 한정 이름을 더 자세히 설명해요.

들여온 이름의 한정자(qualifier)는 들여온 모듈의 이름이거나, 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)

Local aliases (로컬 별칭)

들여온 모듈은 as 절을 써서 들여오는 모듈 안에서 로컬 별칭을 부여받을 수 있어요. 예를 들어

 
  import qualified VeryLongModuleName as C

에서 엔티티는 "VeryLongModuleName." 대신 "C."를 한정자로 써서 참조해야 해요. 이 방식은 또한 들여온 모듈이 바뀌어도 그 모듈에 쓴 한정자를 바꾸지 않고 다른 모듈로 대체할 수 있게 해줘요. 스코프 안의 여러 모듈이 같은 한정자를 쓰는 것도, 모든 이름을 여전히 모호함 없이 해석할 수 있다면 유효해요. 예:

 
  module M where
 
    import qualified Foo as A
 
    import qualified Baz as A
 
    x = A.f

이 모듈은 FooBaz가 둘 다 f를 내보내지만 않는다면 유효해요.

as 절은 비한정 import 문에도 쓸 수 있어요.

 
  import Foo as A(f)

이 선언은 fA.f를 스코프 안으로 끌어들여요.

Examples (예시)

위 import 규칙을 명확히 하기 위해, 모듈 Axy를 내보낸다고 가정해볼게요. 그러면 아래 표는 지정한 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에서 스코프 안에 있는 모든 인스턴스 선언은 들여와져요(5.4절).

Importing and Exporting Instance Declarations (인스턴스 선언의 들여오기와 내보내기)

인스턴스 선언은 import 목록이나 export 목록에 명시적으로 이름 붙일 수 없어요. 모듈 안에서 스코프 안에 있는 모든 인스턴스는 항상 내보내지고, 어떤 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>>"

Name Clashes and Closure (이름 충돌과 클로저)

Qualified names (한정 이름)

한정 이름은 modid.name 형태로 씁니다(2.4절). 한정 이름은 스코프 안으로 끌어들여져요.

  • 최상위 선언으로. 최상위 선언은 정의하는 엔티티의 비한정 이름과 한정 이름을 모두 스코프 안으로 끌어들여요. 따라서
    
    module M where
    
      f x = ...
    
      g x = M.f x x
    
    
    는 유효해요. 정의하는 등장(defining occurrence)은 비한정 이름을 언급해야 해요. 따라서 다음과 같이 쓰는 건 불법이에요.
    
    module M where
    
      M.f x = ...                 -- ILLEGAL
    
      g x = let M.y = x+1 in ...  -- ILLEGAL
    
    
  • import 선언으로. import 선언은 한정이든 아니든 항상 들여온 엔티티의 한정 이름을 스코프 안으로 끌어들여요(5.3절). 이 덕분에 들여온 이름을 참조하는 코드를 바꾸지 않고도 한정 import를 비한정 import로 교체할 수 있어요.

Name clashes (이름 충돌)

모듈에 이름(fA.f 같은)의 바인딩된 등장(bound occurrence)이 있으면, 그 이름이 가리키는 엔티티를 모호함 없이 해석할 수 있어야 해요. 즉 fA.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의 정의를 생각해볼게요.

  • bc에 대한 참조는 각각 B에서 선언된 b, C에서 선언된 c로 모호함 없이 해석돼요.
  • d에 대한 참조는 D에서 선언된 d로 모호함 없이 해석돼요. 이 경우 같은 엔티티가 두 경로(B의 import와 C의 import)로 스코프 안으로 끌려 들어와요. A에서는 d, B.d, C.d라는 이름으로 그것을 참조할 수 있어요.
  • x에 대한 참조는 모호해요. B에서 선언된 x일 수도, C에서 선언된 x일 수도 있죠. 이 모호함은 x에 대한 참조를 B.xC.x로 바꾸면 해결돼요.
  • y에 대한 참조는 없어요. 그래서 BC가 둘 다 y라는 서로 다른 엔티티를 내보내는 건 잘못이 아니에요. 오류는 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.sinF.sin에 대한 참조는 어느 sin을 뜻하는지 모호함 없이 만들려면 둘 다 한정해야 해요. 하지만 F 첫 줄의 타입 시그니처에 있는 비한정 이름 sin은 로컬 sin 선언을 모호함 없이 가리켜요.

Closure (클로저)

Haskell 프로그램의 모든 모듈은 닫혀 있어야(closed) 해요. 즉 소스 코드가 명시적으로 언급하는 모든 이름은 로컬에서 정의되거나 다른 모듈에서 들여와야 해요. 다만 타입 검사나 다른 컴파일 시점 분석을 위해 컴파일러가 필요로 하는 엔티티는, 이름으로 언급되지 않는다면 들여올 필요가 없어요. Haskell 컴파일 시스템은 프로그래머의 도움 없이 컴파일에 필요한 정보를 찾아낼 책임이 있어요. 즉 변수 x를 들여오는 것은, x의 시그니처에 있는 데이터 타입과 클래스를 사용자 프로그램에서 이름으로 참조하지 않는 한, 그것들을 x와 함께 모듈 안으로 끌어들일 것을 요구하지 않아요. Haskell 시스템은 타입 검사나 다른 목적을 위해 어떤 엔티티에 동반되어야 하는 정보를 조용히 들여와요. 그런 엔티티는 명시적으로 내보낼 필요조차 없어요. TM1을 빠져나가지 않는데도 다음 프로그램은 유효해요.

 
  module M1(x) where
 
    data T = T
 
    x = T
 
 
 
  module M2 where
 
    import M1(x)
 
    y = x

이 예에서 T가 스코프 안에 없으므로 y에 명시적 타입 시그니처를 붙일 방법이 없어요. T가 명시적으로 내보내지든 아니든, 모듈 M2는 프로그램을 올바르게 타입 검사할 만큼 T에 대해 알고 있어요.

내보낸 엔티티의 타입은 내보내지 않은 타입 시노님의 영향을 받지 않아요. 예를 들어

 
  module M(x) where
 
    type T = Int
 
    x :: T
 
    x = 1

에서 x의 타입은 T이면서 동시에 Int예요. T가 스코프 안에 없어도 이 둘은 서로 바꿔 쓸 수 있어요. 즉 T의 정의는 T라는 이름이 스코프 안에 있든 없든 그 정의를 만나는 어떤 모듈이든 사용할 수 있어요. T를 내보내는 유일한 이유는 다른 모듈이 그것을 이름으로 참조하게 하기 위해서예요. 타입 검사기는 내보내지지 않아도 필요하면 T의 정의를 찾아내요.

Standard Prelude (표준 프렐류드)

Haskell의 기능 상당수가 Haskell 자체에서 표준 데이터 타입·클래스·함수 라이브러리로 정의되어 있는데, 이것을 "Standard Prelude"라고 불러요. Haskell에서 Prelude는 Prelude 모듈 안에 들어 있어요. 덜 자주 쓰는 함수와 타입을 제공하는 미리 정의된 라이브러리 모듈도 많이 있어요. 예를 들어 복소수, 배열, 그리고 대부분의 입출력은 모두 표준 라이브러리 일부예요. 이것들은 II부에서 정의돼요. 라이브러리를 Prelude에서 분리하는 것은 Prelude의 크기와 복잡성을 줄여 더 쉽게 흡수되게 하고, 프로그래머가 쓸 수 있는 유용한 이름의 공간을 넓히는 장점이 있어요.

Prelude와 라이브러리 모듈은 다른 모듈과 달라요. 다른 모듈과 달리, 그들의 의미론(구현이 아니라)은 Haskell 언어 정의의 고정된 부분이에요. 예를 들어 컴파일러는 Prelude의 소스 코드를 보지 않고도 Prelude 함수에 대한 호출을 최적화할 수 있어요.

The Prelude Module (Prelude 모듈)

Prelude 모듈은 명시적 import 선언으로 들여오지 않는 경우에만, "import Prelude" 문을 쓴 것처럼 모든 모듈에 자동으로 들여와져요. 이렇게 명시적 import를 허용하는 조항 덕분에 Prelude에 정의된 엔티티를 다른 모듈에서 온 것처럼 선택적으로 들여올 수 있어요.

Prelude의 엔티티 의미론은 9장에 주어진, Haskell로 쓰인 Prelude의 참조 구현으로 명시돼요. 어떤 데이터 타입(Int 같은)과 함수(Int 덧셈 같은)는 Haskell로 직접 명시할 수 없어요. 그런 엔티티의 취급은 구현에 의존하므로 9장에서 형식적으로 정의되지 않아요. Prelude의 구현은 튜플 처리에서도 불완전해요. 튜플과 그 인스턴스 선언은 무한한 가족이어야 하지만, 구현은 스킴(scheme)만 제공해요.

9장에서는 Prelude 모듈을 PreludeList, PreludeIO 등 여러 다른 모듈을 써서 정의해요. 이 모듈들은 Haskell 98의 일부가 아니며 따로 들여올 수 없어요. 이들은 단지 Prelude 모듈의 구조를 설명하는 데 도움을 주기 위해 있을 뿐이에요. 구현의 일부로 여겨야지 언어 정의의 일부로 여기면 안 돼요.

Shadowing Prelude Names (Prelude 이름 가리기)

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)

모듈 Anull을 재정의하고, nonNull의 오른쪽 변에 있는 null에 대한 비한정 참조를 포함해요. 후자는 import Prelude 문의 hiding(null)이 없으면 모호해져요. A를 비한정으로 들여온 뒤 null에 대한 비한정 참조를 하는 모든 모듈은, A가 그랬듯 null의 모호한 사용도 해결해야 해요. 따라서 Prelude 이름을 실수로 가릴(shadow) 위험은 거의 없어요.

Prelude를 대신할 다른 모듈을 만들어 쓰는 것도 가능해요. 암시적으로 들여와진다는 점을 빼면 Prelude는 평범한 Haskell 모듈이에요. 특별한 것은 오직 Prelude의 일부 객체가 특별한 구문 구조(syntactic construct)로 참조된다는 점뿐이에요. 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 선언은 비표준 프렐류드를 스코프 안으로 끌어들여요. 튜플((x,x)(,) 같은)과 리스트([x][] 같은)에 대한 특별한 구문은 계속 표준 Prelude가 정의한 튜플과 리스트를 가리켜요. 예를 들어 [x]의 의미를 다른 리스트 구현으로 재정의할 방법은 없어요. 반면 ++의 사용은 특별한 구문이 아니므로 MyPrelude에서 들여온 ++를 가리켜요.

그렇지만 Prelude의 인스턴스 선언을 hiding할 수는 없어요. 예를 들어 Show Char에 대한 새 인스턴스를 정의할 수 없어요.

Separate Compilation (분리 컴파일)

사용하는 Haskell 구현에 따라, 서로 재귀적인 모듈의 분리 컴파일은 들여온 모듈이 컴파일되기 전에 참조될 수 있도록 들어온 모듈이 추가 정보를 포함하도록 요구할 수 있어요. 내보낸 모든 값에 대한 명시적 타입 시그니처는 상호 재귀를 다루는 데 필요할 수 있어요. 분리 컴파일의 정확한 세부 사항은 이 보고서에서 정의하지 않아요.

Abstract Datatypes (추상 데이터 타입)

데이터 타입을 생성자 없이 내보낼 수 있다는 능력 덕분에 추상 데이터 타입(ADT)을 만들 수 있어요. 예를 들어 스택(stack)용 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을 들여오는 모듈은 타입의 생성자에 접근할 수 없으므로 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)