선언과 바인딩

선언과 바인딩 (Declarations and Bindings)

Haskell에서 "무엇을 정의한다"는 행위의 문법과 의미를 다루는 장이에요. 타입·데이터타입·타입 클래스 선언부터, 함수·패턴 바인딩, 그리고 Haskell 특유의 모노모피즘 제한(monomorphism restriction)까지, 언어의 뼈대를 세우는 모든 선언이 여기 있어요. 코드로 치면 "함께 동작하게 하는 부품들"을 명세하는 자리랄까요.

출처: Haskell 2010 언어 리포트

본문

이 장에서는 Haskell 선언의 문법과 비형식적 의미론을 설명해요.

module   → module modid [exports] where body
         | body
body     → { impdecls ; topdecls }
         | { impdecls }
         | { topdecls }

topdecls → topdecl1 ; … ; topdecln          (n ≥ 1)
topdecl  → type simpletype = type
         | data [context =>] simpletype [= constrs] [deriving]
         | newtype [context =>] simpletype = newconstr [deriving]
         | class [scontext =>] tycls tyvar [where cdecls]
         | instance [scontext =>] qtycls inst [where idecls]
         | default (type1 , … , typen)      (n ≥ 0)
         | foreign fdecl
         | decl

decls    → { decl1 ; … ; decln }            (n ≥ 0)
decl     → gendecl
         | (funlhs | pat) rhs

cdecls   → { cdecl1 ; … ; cdecln }          (n ≥ 0)
cdecl    → gendecl
         | (funlhs | var) rhs

idecls   → { idecl1 ; … ; idecln }          (n ≥ 0)
idecl    → (funlhs | var) rhs
         |     (empty)

gendecl  → vars :: [context =>] type        (type signature)
         | fixity [integer] ops             (fixity declaration)
         |     (empty declaration)

ops      → op1 , … , opn                    (n ≥ 1)
vars     → var1 , … , varn                  (n ≥ 1)
fixity   → infixl | infixr | infix

구문 범주 topdecls의 선언은 Haskell 모듈의 최상위에서만 허용돼요(5장). 반면 decls는 최상위나 중첩 스코프(let이나 where 구조 안)에서 모두 쓸 수 있어요.

설명을 위해 선언을 세 그룹으로 나눠요: 사용자 정의 데이터타입(type, newtype, data 선언, 4.2절), 타입 클래스와 오버로딩(class, instance, default 선언, 4.3절), 중첩 선언(값 바인딩·타입 시그니처·fixity 선언, 4.4절).

Haskell에는 "하드와이어(hard-wired)"된 원시 데이터타입(정수·부동소수점 등)이 몇 가지 있지만, 대부분의 "내장(built-in)" 데이터타입은 일반 Haskell 코드로, 일반 type·data 선언을 써서 정의돼요. 이 "내장" 데이터타입은 6.1절에서 자세히 설명해요.

4.1 타입과 클래스 개요 (Overview of Types and Classes)

Haskell은 정적 타입 의미론을 제공하기 위해 전통적인 Hindley-Milner 다형성 타입 시스템을 써요 [4, 6]. 다만 오버로드된 함수를 구조적으로 도입하는 방법을 제공하는 타입 클래스(type class, 또는 짧게 class)로 타입 시스템이 확장돼 있어요.

클래스 선언(4.3.1절)은 새 타입 클래스와, 그 클래스의 인스턴스인 모든 타입이 지원해야 하는 오버로드된 연산을 도입해요. 인스턴스 선언(4.3.2절)은 어떤 타입이 클래스의 인스턴스임을 선언하고, 이름 붙은 타입에 인스턴스화된 오버로드된 연산 — **클래스 메서드(class method)**라 불러요 — 의 정의를 포함해요.

예를 들어 IntFloat 타입에 연산 (+)negate를 오버로드하고 싶다고 해 볼게요. Num이라는 새 타입 클래스를 도입해요:

class Num a where -- simplified class declaration for Num
    (+)    :: a -> a -> a   -- (Num is defined in the Prelude)
    negate :: a -> a

이 선언은 "타입 a가 주어진 타입의 클래스 메서드 (+)negate가 그 위에 정의돼 있으면 Num 클래스의 인스턴스다"로 읽을 수 있어요. 그런 다음 IntFloat를 이 클래스의 인스턴스로 선언할 수 있어요:

instance Num Int where -- simplified instance of Num Int
    x + y    = addInt x y
    negate x = negateInt x
instance Num Float where -- simplified instance of Num Float
    x + y    = addFloat x y
    negate x = negateFloat x

여기서 addInt, negateInt, addFloat, negateFloat는 이 경우 원시 함수로 가정하지만, 일반적으로는 어떤 사용자 정의 함수든 될 수 있어요. 위 첫 선언은 "Int(+)negate에 대한 이 정의들(즉 클래스 메서드)이 증거(증인)로 Num 클래스의 인스턴스다"로 읽을 수 있어요.

타입 클래스의 더 많은 예는 Jones [8]나 Wadler·Blott [13]의 논문에서 찾을 수 있어요. 'type class'라는 용어는 원래 Haskell 1.0 타입 시스템을 설명하는 데 쓰였고, 'constructor class'는 원래 타입 클래스의 확장을 설명하는 데 쓰였어요. 이제 두 용어를 다르게 쓸 이유가 없어요. 이 리포트에서 'type class'는 원래 Haskell 타입 클래스와 Jones가 도입한 생성자 클래스를 모두 포함해요.

4.1.1 종류 (Kinds)

타입 식이 유효한지 보장하기 위해, 타입 식은 서로 다른 **종류(kind)**로 분류돼요. 종류는 두 형태 중 하나를 가져요:

  • 기호 는 모든 무항(nullary) 타입 생성자의 종류를 나타내요.
  • κ1κ2가 종류라면, κ1 → κ2는 종류 κ1의 타입을 받아 종류 κ2의 타입을 돌려주는 타입들의 종류예요.

종류 추론(kind inference)은 값 식의 유효성을 검사하는 타입 추론과 비슷한 방식으로 타입 식의 유효성을 검사해요. 다만 타입과 달리 종류는 전적으로 암묵적이고 언어의 눈에 보이는 부분이 아니에요. 종류 추론은 4.6절에서 다뤄요.

4.1.2 타입의 문법 (Syntax of Types)

type   → btype [-> type]              (function type)

btype  → [btype] atype                (type application)

atype  → gtycon
       | tyvar
       | ( type1 , … , typek )        (tuple type, k ≥ 2)
       | [ type ]                     (list type)
       | ( type )                     (parenthesised constructor)

gtycon → qtycon
       | ()                           (unit type)
       | []                           (list constructor)
       | (->)                         (function constructor)
       | (,{,})                       (tupling constructors)

Haskell 타입 식의 문법은 위에 주어져 있어요. 데이터 값이 데이터 생성자로 만들어지듯, 타입 값은 타입 생성자(type constructor)로 만들어져요. 데이터 생성자처럼 타입 생성자의 이름은 대문자로 시작해요. 데이터 생성자와 달리 중위 타입 생성자는 허용되지 않아요((->) 제외).

타입 식의 주요 형태는 다음과 같아요:

  • 타입 변수: 소문자로 시작하는 식별자로 써요. 변수의 종류는 그것이 나타나는 문맥으로 암묵적으로 결정돼요.
  • 타입 생성자: 대부분의 타입 생성자는 대문자로 시작하는 식별자로 써요. 예를 들어:
    • Char, Int, Integer, Float, Double, Bool은 종류 의 타입 상수예요.
    • MaybeIO는 단항 타입 생성자이고, 종류 ∗→∗의 타입으로 취급돼요.
    • 선언 data T ...newtype T ...는 타입 생성자 T를 타입 어휘에 추가해요. T의 종류는 종류 추론으로 결정돼요.
  • 특정 내장 타입 생성자를 위한 특별한 문법:
    • 사소한(trivial) 타입은 ()로 쓰고 종류 를 가져요. "무항 튜플" 타입을 나타내고 값이 정확히 하나 있는데, 그것도 ()로 써요(3.9절, 6.1.5절).
    • 함수 타입은 (->)로 쓰고 종류 ∗→∗→∗를 가져요.
    • 리스트 타입은 []로 쓰고 종류 ∗→∗를 가져요.
    • 튜플 타입은 (,), (,,) 등으로 써요. 그 종류는 ∗→∗→∗, ∗→∗→∗→∗ 등이에요.
    • (->)[] 상수의 사용은 아래에서 더 자세히 설명해요.
  • 타입 적용: t1이 종류 κ1 → κ2의 타입이고 t2가 종류 κ1의 타입이면, t1 t2는 종류 κ2의 타입 식이에요.
  • 괄호 타입: 형태 (t)는 타입 t와 동일해요.

예를 들어 타입 식 IO a는 상수 IO를 변수 a에 적용한 것으로 이해할 수 있어요. IO 타입 생성자가 종류 ∗→∗이므로, 변수 a와 전체 식 IO a 모두 종류 를 가져야 해요. 일반적으로 사용자 정의 데이터타입·타입 별칭·클래스에 적절한 종류를 정하려면 종류 추론 과정(4.6절)이 필요해요.

특정 타입 식을 더 전통적인 방식으로 쓸 수 있게 해 주는 특별 문법이 있어요:

  • 함수 타입은 형태 t1 -> t2로, (->) t1 t2 타입과 동등해요. 함수 화살표는 우결합이에요. 예를 들어 Int -> Int -> FloatInt -> (Int -> Float)을 의미해요.
  • 튜플 타입은 형태 (t1, …, tk)(k ≥ 2)로, 괄호 사이에 콤마가 k−1개 있는 (,…,) t1 … tk 타입과 동등해요. 첫 번째 성분이 타입 t1, 두 번째 성분이 t2, ...인 k-튜플의 타입을 나타내요(3.8절, 6.1.4절).
  • 리스트 타입은 형태 [t]로, [] t 타입과 동등해요. 원소 타입이 t인 리스트의 타입을 나타내요(3.7절, 6.1.3절).

이 특별 문법 형태들은 스코프에 무엇이 있든 무관하게 항상 함수·튜플·리스트의 내장 타입 생성자를 나타내요. 비슷하게 전위 타입 생성자 (->), [], (), (,) 등은 항상 내장 타입 생성자를 나타내요. 한정(qualify)할 수 없고, import·export 리스트에도 언급할 수 없어요(5장). (그래서 위에 특별 생성 규칙, "gtycon"이 있어요.)

리스트와 튜플 타입이 특별한 문법을 가진다 해도, 그 의미는 동등한 사용자 정의 대수적 데이터타입과 같아요.

식과 타입이 일관된 문법을 가진다는 점에 주목해요. ti가 식·패턴 ei의 타입이라면, 식 (\ e1 -> e2), [e1], (e1,e2)는 각각 타입 (t1 -> t2), [t1], (t1,t2)를 가져요.

한 가지 예외(클래스 선언의 구분된 타입 변수, 4.3.1절)를 빼면, Haskell 타입 식의 타입 변수는 모두 전역 양화(universally quantified)된 것으로 가정돼요. 전역 양화를 위한 명시적 문법은 없어요 [4]. 예를 들어 타입 식 a -> a는 타입 ∀ a. a → a를 나타내요. 하지만 명확성을 위해 Haskell 프로그램의 타입을 논할 때 양화를 명시적으로 쓰는 경우가 많아요. 명시적으로 양화된 타입을 쓸 때 의 스코프는 가능한 한 오른쪽으로 멀리 확장돼요. 예를 들어 ∀ a. a → a∀ a. (a → a)를 의미해요.

4.1.3 클래스 단언과 문맥의 문법 (Syntax of Class Assertions and Contexts)

context → class
        | ( class1 , … , classn )      (n ≥ 0)
class   → qtycls tyvar
        | qtycls ( tyvar atype1 … atypen )   (n ≥ 1)
qtycls  → [ modid . ] tycls
tycls   → conid
tyvar   → varid

클래스 단언(class assertion)은 형태 qtycls tyvar로, 타입 tyvar가 클래스 qtycls에 속함을 나타내요. 클래스 식별자는 대문자로 시작해요. 문맥(context)은 0개 이상의 클래스 단언으로 이뤄져 있고, 일반적인 형태는:

( C1 u1, …, Cn un )

여기서 C1, …, Cn은 클래스 식별자이고, u1, …, un 각각은 타입 변수이거나 타입 변수를 하나 이상의 타입에 적용한 것이에요. n = 1일 때 바깥 괄호는 생략할 수 있어요.

일반적으로 문맥을 cx로 나타내고, 문맥 cx로 제한된 타입 tcx => t로 써요. 문맥 cxt에서 참조되는 타입 변수만 포함해야 해요. 편의상 문맥 cx가 비어 있어도 cx => t로 쓰지만, 이 경우 구체 문법에는 =>가 없어요.

4.1.4 타입과 클래스의 의미론 (Semantics of Types and Classes)

이 절에서는 타입 시스템의 비형식적 세부 사항을 제공해요. (Wadler·Blott [13]와 Jones [8]가 각각 타입·생성자 클래스를 더 자세히 논의해요.)

Haskell 타입 시스템은 프로그램의 각 식에 타입을 부여해요. 일반적으로 타입은 ∀ u. cx ⇒ t 형태인데, u는 타입 변수 집합 u1, …, un이에요. 그런 어떤 타입에서든, cx에 자유로 나타나는 전역 양화된 타입 변수 ui는 반드시 t에도 자유로 나타나야 해요. 게다가 문맥 cx는 4.1.3절에서 주어진 형태여야 해요. 예를 들어 유효한 타입 몇 가지:

Eq a => a -> a
(Eq a, Show a, Eq b) => [a] -> [b] -> String
(Eq (f a), Functor f) => (a -> b) -> f a -> f b -> Bool

세 번째 타입에서 제약 Eq (f a)f가 전역 양화돼 있기 때문에 더 단순하게 만들 수 없어요.

e의 타입은 e의 자유 변수에 타입을 주는 타입 환경(type environment)과, 어떤 타입이 어떤 클래스의 인스턴스인지 선언하는 클래스 환경(class environment)에 의존해요(타입은 인스턴스 선언이나 deriving 절이 있을 때만 클래스의 인스턴스가 돼요).

타입들은 일반화 순서(아래 명시)로 관련돼요. 특정 식에 (주어진 환경에서) 부여할 수 있는, 일반화 순서가 유도하는 동치까지의 가장 일반적인 타입을 그 식의 **원리 타입(principal type)**이라고 불러요. Haskell의 확장된 Hindley-Milner 타입 시스템은 모든 식의 원리 타입을 추론할 수 있어요(오버로드된 클래스 메서드의 적절한 사용 포함. 다만 4.3.4절에서 설명하는 것처럼 어떤 모호한 오버로딩이 생길 수는 있어요). 따라서 명시적 타입 부여(타입 시그니처)는 대개 선택 사항이에요(3.16절, 4.4.1절).

타입 ∀ u. cx1 ⇒ t1은, 도메인이 u인 치환(substitution) S가 존재해 다음을 만족할 때에만 타입 ∀ w. cx2 ⇒ t2보다 더 일반적이에요:

  • t2S(t1)과 동일하고,
  • cx2가 클래스 환경에서 성립할 때마다 S(cx1)도 성립한다.

타입 ∀ u. cx ⇒ t의 값은 문맥 cx[s∕u]가 성립할 때에만 타입 s에서 인스턴스화될 수 있어요. 예를 들어 함수 double을 보죠:

double x = x + x

double의 가장 일반적인 타입은 ∀ a. Num a ⇒ a → a예요. IntNum 클래스의 인스턴스이므로 Num Int가 성립해서, doubleInt 타입의 값에 적용될 수 있어요(aInt로 인스턴스화). 그러나 Char는 보통 Num 클래스의 인스턴스가 아니므로, double은 보통 Char 타입의 값에 적용될 수 없어요. 사용자가 그런 인스턴스를 선언하기로 하면, 그 경우엔 double이 실제로 Char에 적용될 수 있어요.

4.2 사용자 정의 데이터타입 (User-Defined Datatypes)

이 절에서는 대수적 데이터타입(data 선언), 이름 바뀐 데이터타입(newtype 선언), 타입 별칭(type 선언)을 설명해요. 이 선언들은 모듈의 최상위에서만 나타날 수 있어요.

4.2.1 대수적 데이터타입 선언 (Algebraic Datatype Declarations)

topdecl    → data [context =>] simpletype [= constrs] [deriving]

simpletype → tycon tyvar1 … tyvark        (k ≥ 0)

constrs    → constr1 | … | constrn        (n ≥ 1)
constr     → con [!] atype1 … [!] atypek  (arity con = k, k ≥ 0)
           | (btype | ! atype) conop (btype | ! atype)  (infix conop)
           | con { fielddecl1 , … , fielddecln }         (n ≥ 0)
fielddecl  → vars :: (type | ! atype)

deriving   → deriving (dclass | (dclass1, … , dclassn))   (n ≥ 0)
dclass     → qtycls

constr의 우선순위는 식의 것과 같아요 — 일반 생성자 적용은 중위 생성자 적용보다 높은 우선순위를 가져요(그래서 a : Foo aa : (Foo a)로 파싱돼요).

대수적 데이터타입 선언은 형태가:

data cx => T u1 … uk = K1 t11 … t1k1 | … | Kn tn1 … tnkn

여기서 cx는 문맥이에요. 이 선언은 0개 이상의 구성 데이터 생성자 K1, …, Kn을 가진 새 타입 생성자 T를 도입해요. 이 리포트에서 한정 없는 용어 "constructor"는 항상 "data constructor"를 의미해요.

데이터 생성자의 타입은 다음과 같이 주어져요:

Ki :: ∀ u1 … uk. cxi ⇒ ti1 → … → tiki → (T u1 … uk)

여기서 cxi는 타입 ti1, …, tiki에 자유로 나타나는 타입 변수만 제약하는 cx의 가장 큰 부분집합이에요. 타입 변수 u1부터 uk까지는 구별되어야 하고 cxtij에 나타날 수 있어요. 다른 어떤 타입 변수가 cx나 오른쪽 변에 나타나는 것은 정적 오류예요. 새 타입 상수 Tκ1 →… → κk →∗ 형태의 종류를 가져요. 인자 변수 ui의 종류 κi는 4.6절에서 설명하는 종류 추론으로 결정돼요. 이는 T가 0개에서 k개 사이의 인자를 가진 타입 식에 쓰일 수 있음을 의미해요.

예를 들어 선언:

data Eq a => Set a = NilSet | ConsSet a (Set a)

은 종류 ∗→∗의 타입 생성자 Set과, 타입:

NilSet  :: ∀ a. Set a
ConsSet :: ∀ a. Eq a ⇒ a → Set a → Set a

의 생성자 NilSet·ConsSet을 도입해요. 주어진 예에서 ConsSet의 오버로드된 타입은 ConsSetEq 클래스의 인스턴스인 타입의 값에만 적용될 수 있게 보장해요. ConsSet에 대한 패턴 매칭도 Eq a 제약을 일으켜요. 예를 들어:

f (ConsSet a s) = a

에서 함수 f는 추론된 타입 Eq a => Set a -> a를 가져요. data 선언의 문맥은 그 외 다른 효과가 전혀 없어요.

데이터타입의 생성자들이 그 데이터타입이 정의된 모듈 밖에서 보이는지(즉 데이터타입의 "추상성")는, 5.8절에서 설명하듯 export 리스트에서 데이터타입 이름의 형태로 제어돼요.

data 선언의 선택적 deriving 부분은 파생 인스턴스(derived instances)와 관련되고, 4.3.3절에서 설명해요.

레이블 필드 (Labelled Fields)

항수 k의 데이터 생성자는 k개 성분의 객체를 만들어요. 이 성분들은 보통 식·패턴에서 생성자에 대한 인자로서 위치적으로 접근돼요. 큰 데이터타입에서는 데이터 객체의 성분에 **필드 레이블(field label)**을 붙이는 것이 유용해요. 이렇게 하면 특정 필드를 생성자 안의 위치와 무관하게 참조할 수 있어요.

data 선언의 생성자 정의는 레코드 문법 C { ... }을 써서 생성자의 필드에 레이블을 붙일 수 있어요. 필드 레이블을 쓰는 생성자와 그렇지 않은 생성자는 자유롭게 섞을 수 있어요. 연관된 필드 레이블을 가진 생성자는 여전히 일반 생성자로 쓸 수 있어요. 레이블을 쓰는 기능은 단지 위치 기반 생성자를 쓰는 연산의 약어(shorthand)예요. 위치 기반 생성자의 인자는 레이블 필드와 같은 순서로 온다. 예를 들어 선언:

data C = F { f1,f2 :: Int, f3 :: Bool }

은 다음이 만드는 것과 동일한 타입과 생성자를 정의해요:

data C = F Int Int Bool

필드 레이블을 쓰는 연산은 3.15절에서 설명해요. data 선언은 타입 별칭 확장 후 필드의 타입이 모든 경우에 같기만 하면 같은 필드 레이블을 여러 생성자에서 쓸 수 있어요. 한 레이블은 스코프 안에서 둘 이상의 타입이 공유할 수 없어요. 필드 이름은 일반 변수·클래스 메서드와 최상위 네임스페이스를 공유하며, 스코프 안의 다른 최상위 이름과 충돌하면 안 돼요.

패턴 F {}F가 레코드 문법으로 선언됐든 아니든, 생성자 F로 만든 어떤 값과도 매칭해요.

엄격성 플래그 (Strictness Flags)

데이터 생성자가 적용될 때마다, 생성자의 각 인자는 대수적 데이터타입 선언의 대응하는 타입에 엄격성 플래그(strictness flag)가 있을 때에만 평가돼요. 엄격성 플래그는 느낌표 !로 나타내요. 어휘적으로 !는 예약 연산자(reservedop)가 아니라 일반 varsym이에요. !data 선언의 인자 타입 문맥에서만 특별한 의미를 가져요.

번역: 형태:

data cx => T u1 … uk = … | K s1 … sn | …

의 선언(각 si!ti 또는 ti 형태)은, 식에서 K의 모든 발생을:

(\ x1 … xn -> ( ((K op1 x1) op2 x2) … ) opn xn)

으로 대체해요. 여기서 siti 형태면 opi는 비엄격 적용 함수 $이고, si! ti 형태면 opi는 엄격 적용 함수 $!예요(6.2절). K에 대한 패턴 매칭은 엄격성 플래그의 영향을 받지 않아요.

4.2.2 타입 별칭 선언 (Type Synonym Declarations)

topdecl    → type simpletype = type
simpletype → tycon tyvar1 … tyvark        (k ≥ 0)

타입 별칭(type synonym) 선언은 옛 타입과 동등한 새 타입을 도입해요. 형태는:

type T u1 … uk = t

이것은 새 타입 생성자 T를 도입해요. 타입 (T t1 …tk)은 타입 t[t1∕u1, …, tk∕uk]과 동등해요. 타입 변수 u1부터 uk까지는 구별되어야 하고 t에만 스코프돼요. 다른 어떤 타입 변수가 t에 나타나는 것은 정적 오류예요. 새 타입 생성자 T의 종류는 κ1 →… → κk → κ 형태인데, 인자 ui의 종류 κi와 오른쪽 변 t의 종류 κ는 4.6절에서 설명하는 종류 추론으로 결정돼요. 예를 들어 다음 정의는 리스트 타입 생성자를 다르게 쓰는 방법을 제공해요:

type List = []

타입 별칭 선언이 도입한 타입 생성자 기호 T는 부분 적용될 수 없어요. 전체 인자 수 없이 T를 쓰는 것은 정적 오류예요.

재귀·상호 재귀 데이터타입은 허용되지만, 대수적 데이터타입이 개입하지 않는 한 타입 별칭에서는 그렇지 않아요. 예를 들어:

type Rec a = [Circ a]
data Circ a = Tag [Rec a]

은 허용되지만:

type Rec a = [Circ a] -- invalid
type Circ a = [Rec a] -- invalid

은 허용되지 않아요. 마찬가지로 type Rec a = [Rec a]도 허용되지 않아요.

타입 별칭은 타입 시그니처를 더 읽기 쉽게 만드는 편리하지만 순전히 구문적인 메커니즘이에요. 별칭과 그 정의는, 인스턴스 선언의 인스턴스 타입(4.3.2절)을 빼면, 완전히 상호 교환 가능해요.

4.2.3 데이터타입 이름 바꾸기 (Datatype Renamings)

topdecl    → newtype [context =>] simpletype = newconstr [deriving]
newconstr  → con atype
           | con { var :: type }
simpletype → tycon tyvar1 … tyvark        (k ≥ 0)

형태:

newtype cx => T u1 … uk = N t

의 선언은 표현이 기존 타입과 같은 새 타입을 도입해요. 타입 (T u1… uk)은 데이터타입 t의 이름을 바꿔요. 타입 별칭과 다른 점은, 원래 타입으로·로부터 명시적으로 강제 변환(coerce)돼야 하는 구별되는 타입을 만든다는 것이에요. 또한 타입 별칭과 달리 newtype은 재귀 타입을 정의하는 데 쓸 수 있어요. 식에서 생성자 N은 값을 타입 t에서 타입 (T u1 … uk)으로 강제 변환해요. 패턴에서 N을 쓰면 값을 타입 (T u1 … uk)에서 t로 강제 변환해요. 이 강제 변환은 실행 시간 오버헤드 없이 구현될 수 있어요. newtype은 객체의 기저 표현을 바꾸지 않아요.

newtype으로 정의된 타입에는 새 인스턴스(4.3.2절)를 정의할 수 있지만, 타입 별칭에는 정의할 수 없어요. newtype이 만든 타입은 대수적 데이터타입과 달라요. 대수적 데이터타입의 표현은 추가적인 간접(indirection) 수준을 갖기 때문이에요. 이 차이는 표현 접근을 덜 효율적으로 만들 수 있고, 패턴 매칭의 다른 규칙으로 반영돼요(3.17절). 대수적 데이터타입과 달리 newtype 생성자 N은 unlifted라서 N ⊥과 같아요.

다음 예시들이 data(대수적 데이터타입), type(타입 별칭), newtype(이름 바꾸는 타입)의 차이를 명확히 해 줘요. 다음 선언들을 주고:

data D1 = D1 Int
data D2 = D2 !Int
type S = Int
newtype N = N Int
d1 (D1 i) = 42
d2 (D2 i) = 42
s i = 42
n (N i) = 42

(d1 ⊥), (d2 ⊥), (d2 (D2 ⊥))는 모두 ⊥와 동등한 반면, (n ⊥), (n (N ⊥)), (d1 (D1 ⊥)), (s ⊥)은 모두 42와 동등해요. 특히 (N ⊥)은 ⊥와 동등하지만 (D1 ⊥)은 ⊥와 동등하지 않아요.

newtype 선언의 선택적 deriving 부분은 data 선언의 deriving 성분과 같은 방식으로 취급돼요. 4.3.3절을 봐요.

newtype 선언은 필드 이름 문법을 쓸 수 있어요. 물론 필드는 하나만 있을 수 있어요. 그래서:

newtype Age = Age { unAge :: Int }

은 생성자와 역생성자(de-constructor) 둘 다를 스코프에 넣어요:

Age :: Int -> Age
unAge :: Age -> Int

4.3 타입 클래스와 오버로딩 (Type Classes and Overloading)

4.3.1 클래스 선언 (Class Declarations)

topdecl    → class [scontext =>] tycls tyvar [where cdecls]
scontext   → simpleclass
           | ( simpleclass1 , … , simpleclassn )   (n ≥ 0)
simpleclass→ qtycls tyvar
cdecls     → { cdecl1 ; … ; cdecln }               (n ≥ 0)
cdecl      → gendecl
           | (funlhs | var) rhs

클래스 선언은 새 클래스와 그 연산(클래스 메서드)을 도입해요. 클래스 선언의 일반적인 형태:

class cx => C u where cdecls

이것은 새 클래스 이름 C를 도입해요. 타입 변수 u는 클래스 본문의 클래스 메서드 시그니처에만 스코프돼요. 문맥 cxC의 슈퍼클래스(superclass)를 지정해요(아래 설명). cx에서 참조될 수 있는 유일한 타입 변수는 u예요.

슈퍼클래스 관계는 순환적이면 안 돼요. 즉 방향성 비순환 그래프(directed acyclic graph)를 형성해야 해요.

클래스 선언의 cdecls 부분은 세 종류의 선언을 포함해요:

  • 클래스 선언은 새 클래스 메서드 vi를 도입하는데, 그 스코프는 클래스 선언 밖으로 확장돼요. 클래스 선언의 클래스 메서드는 정확히 cdecls에 명시적 타입 시그니처 vi :: cxi => ti가 있는 vi들이에요. 클래스 메서드는 변수 바인딩·필드 이름과 최상위 네임스페이스를 공유해요. 스코프 안의 다른 최상위 바인딩과 충돌하면 안 돼요. 즉 클래스 메서드는 최상위 정의·필드 이름·다른 클래스 메서드와 같은 이름을 가질 수 없어요.
    • 최상위 클래스 메서드 vi의 타입은:
      vi :: ∀u,w. (Cu,cxi) ⇒ ti
      
      tiu를 언급해야 해요. u가 아닌 타입 변수 w를 언급할 수도 있는데, 그 경우 vi의 타입은 uw 둘 다에 대해 다형성이에요. cxiw만 제약할 수 있어요. 특히 cxiu를 제약할 수 없어요. 예:
      class Foo a where
          op :: Num b => a -> b -> a
      
      여기서 op의 타입은 ∀ a, b. (Foo a, Num b) ⇒ a → b → a예요.
  • cdecls는 클래스 메서드들에 대한 fixity 선언을 포함할 수 있어요(그러나 다른 값에 대해서는 안 돼요). 다만 클래스 메서드는 최상위 값을 선언하므로, 클래스 메서드의 fixity 선언은 클래스 선언 밖 최상위에 대안으로 나타날 수 있어요.
  • 마지막으로 cdecls는 어떤 vi에 대한 기본 클래스 메서드(default class method)를 포함할 수 있어요. vi에 대한 기본 클래스 메서드는 특정 인스턴스 선언에서 그에 대한 바인딩이 주어지지 않을 때 쓰여요(4.3.2절). 기본 메서드 선언은 일반 값 정의인데, 왼쪽 변이 변수나 함수 정의일 수만 있다는 점이 달라요. 예를 들어:
    class Foo a where
        op1, op2 :: a -> a
        (op1, op2) = ...
    
    은 허용되지 않아요. 기본 선언의 왼쪽 변이 패턴이기 때문이에요.

이 경우들을 빼면, cdecls에는 다른 어떤 선언도 허용되지 않아요.

where 부분이 없는 클래스 선언은 여러 클래스를, 원래 것들의 모든 클래스 메서드를 상속하는 더 큰 클래스로 결합하는 데 유용할 수 있어요. 예:

class (Read a, Show a) => Textual a

이런 경우, 타입이 모든 슈퍼클래스의 인스턴스여도 자동으로 서브클래스의 인스턴스가 되지 않아요. 서브클래스에 즉시 클래스 메서드가 없어도 그래요. 인스턴스 선언은 where 부분 없이 명시적으로 주어져야 해요.

4.3.2 인스턴스 선언 (Instance Declarations)

topdecl → instance [scontext =>] qtycls inst [where idecls]
inst    → gtycon
        | ( gtycon tyvar1 … tyvark )   (k ≥ 0, tyvars distinct)
        | ( tyvar1 , … , tyvark )      (k ≥ 2, tyvars distinct)
        | [ tyvar ]
        | ( tyvar1 -> tyvar2 )         (tyvar1 and tyvar2 distinct)
idecls  → { idecl1 ; … ; idecln }      (n ≥ 0)
idecl   → (funlhs | var) rhs
        |     (empty)

인스턴스 선언은 클래스의 인스턴스를 도입해요. 다음을 클래스 선언이라 하자:

class cx => C u where { cbody }

대응하는 인스턴스 선언의 일반적인 형태는:

instance cx′ => C (T u1 … uk) where { d }

여기서 k ≥ 0이에요. 타입 (T u1 … uk)은 타입 생성자 T를 단순 타입 변수 u1, … uk에 적용한 형태여야 해요. 게다가 T는 타입 별칭이면 안 되고, ui는 모두 구별되어야 해요.

이것은 다음과 같은 인스턴스 선언을 금지해요:

instance C (a,a) where ...
instance C (Int,a) where ...
instance C [[a]] where ...

선언 dC의 클래스 메서드에 대한 바인딩만 포함할 수 있어요. 스코프 안에 없는 클래스 메서드에 대한 바인딩을 주는 것은 불법이지만, 스코프 안에 있는 이름은 무슨 이름이든 무관해요. 특히 한정 이름(qualified name)일 수 있어요. (이 규칙은 export 리스트의 하위 이름에 쓰이는 규칙과 동일해요 — 5.2절.) 예를 들어 range가 한정 이름 Data.Ix.range로만 스코프 안에 있어도 다음은 합법이에요:

module A where
import qualified Data.Ix
instance Data.Ix.Ix T where
    range = ...

선언은 타입 시그니처나 fixity 선언을 포함할 수 없어요. 그것들은 클래스 선언에서 이미 주어졌기 때문이에요. 기본 클래스 메서드(4.3.1절)의 경우처럼, 메서드 선언은 변수나 함수 정의의 형태여야 해요.

어떤 클래스 메서드에 바인딩이 주어지지 않으면 클래스 선언의 대응하는 기본 클래스 메서드가 쓰여요(있을 때). 그런 기본값이 없으면 이 인스턴스의 클래스 메서드는 undefined에 바인딩되고 컴파일 시 오류는 나지 않아요.

타입 T를 클래스 C의 인스턴스로 만드는 인스턴스 선언을 C-T 인스턴스 선언이라 부르고, 다음 정적 제약을 받아요:

  • 한 타입은 프로그램에서 한 특정 클래스의 인스턴스로 두 번 이상 선언될 수 없어요.
  • 클래스와 타입은 같은 종류를 가져야 해요. 이것은 4.6절에서 설명하는 종류 추론으로 결정할 수 있어요.

인스턴스 타입 (T u1 … uk)의 타입 변수들이 인스턴스 문맥 cx′의 제약을 만족한다고 가정해요. 이 가정 아래 다음 두 조건도 만족해야 해요:

  • C의 슈퍼클래스 문맥 cx[(T u1 … uk)∕u]이 표현하는 제약이 만족되어야 해요. 즉 TC의 각 슈퍼클래스의 인스턴스여야 하고, 모든 슈퍼클래스 인스턴스의 문맥은 cx′가 함의해야 해요.
  • d의 클래스 메서드 선언이 잘 타입 부여되기 위해 인스턴스 타입의 타입 변수에 요구되는 제약도 만족되어야 해요.

실제로는 병리적인 경우를 빼면, 위 두 제약을 만족하는 가장 일반적인 인스턴스 문맥 cx′를 인스턴스 선언에서 추론하는 것이 가능해요. 그럼에도 명시적인 인스턴스 문맥을 쓰는 것은 의무예요.

다음 예가 슈퍼클래스 인스턴스가 부과하는 제약을 보여줘요:

class Foo a => Bar a where ...
instance (Eq a, Show a) => Foo [a] where ...
instance Num a => Bar [a] where ...

이 예는 유효한 Haskell이에요. FooBar의 슈퍼클래스이므로, 두 번째 인스턴스 선언은 [a]Num a 가정 아래 Foo의 인스턴스일 때에만 유효해요. EqShowNum의 슈퍼클래스이므로, 첫 인스턴스 선언이 실제로 이 가정 아래 [a]Foo의 인스턴스라고 말해요.

두 인스턴스 선언이 대신 이렇게 읽히면:

instance Num a => Foo [a] where ...
instance (Eq a, Show a) => Bar [a] where ...

프로그램이 무효가 돼요. 두 번째 인스턴스 선언은 [a]가 가정 (Eq a, Show a) 아래 Foo의 인스턴스일 때에만 유효한데, 이는 성립하지 않아요. [a]는 더 강한 가정 Num a 아래에서만 Foo의 인스턴스이기 때문이에요.

인스턴스 선언의 추가 예는 9장에서 찾을 수 있어요.

4.3.3 파생 인스턴스 (Derived Instances)

4.2.1절에서 언급했듯이 datanewtype 선언은 선택적 deriving 형태를 포함해요. 이 형태가 포함되면, 이름 붙은 각 클래스에 대해 그 데이터타입의 파생 인스턴스 선언이 자동으로 생성돼요. 이 인스턴스들은 사용자 정의 인스턴스와 같은 제약을 받아요. 타입 T에 대해 클래스 C를 파생할 때, C의 모든 슈퍼클래스의 인스턴스가 T에 대해 존재해야 해요. 명시적 인스턴스 선언으로든, 슈퍼클래스를 deriving 절에 포함시킴으로든요.

파생 인스턴스는 사용자 정의 데이터타입을 위한 편리한 자주 쓰이는 연산을 제공해요. 예를 들어 Eq 클래스의 데이터타입에 대한 파생 인스턴스는 ==/= 연산을 정의해, 프로그래머가 그것들을 정의할 필요를 덜어 줘요.

Prelude에서 파생 인스턴스가 허용되는 유일한 클래스는 Eq, Ord, Enum, Bounded, Show, Read이고, 모두 그림 6.1에 언급돼 있어요. 각 클래스에 대해 파생 인스턴스가 어떻게 생성되는지의 정확한 세부 사항은 11장에 있고, 언제 그런 파생 인스턴스가 가능한지의 명세도 거기 있어요. 표준 라이브러리가 정의한 클래스도 파생 가능할 수 있어요.

deriving 형태에 이름 붙은 클래스에 대해 인스턴스 선언을 파생하는 것이 불가능하면 정적 오류가 나요. 예를 들어 모든 데이터타입이 Enum의 클래스 메서드를 제대로 지원할 수 있는 건 아니에요. 또한 파생된 클래스에 대한 명시적 인스턴스 선언을 주는 것도 정적 오류예요.

datanewtype 선언에서 deriving 형태가 생략되면 그 데이터타입에 대해 어떤 인스턴스 선언도 파생되지 않아요. 즉 deriving 형태를 생략하는 것은 빈 deriving 형태 deriving ()을 포함하는 것과 동등해요.

4.3.4 모호한 타입과 오버로드된 숫자 연산의 기본값 (Ambiguous Types, and Defaults for Overloaded Numeric Operations)

topdecl → default (type1 , … , typen)     (n ≥ 0)

Haskell 스타일 오버로딩에 내재된 문제는 **모호한 타입(ambiguous type)**의 가능성이에요. 예를 들어 11장에 정의된 readshow 함수를 쓰고, IntBoolReadShow의 구성원이라고 가정하면, 식:

let x = read "..." in show x -- invalid

은 모호해요. showread의 타입:

show :: ∀ a. Show a ⇒ a → String
read :: ∀ a. Read a ⇒ String → a

이 두 경우 모두 aInt로, 또는 Bool로 인스턴스화함으로써 충족될 수 있기 때문이에요. 그런 식은 잘못 타입 부여된 것(ill-typed), 정적 오류로 간주돼요.

e가 타입 ∀ u. cx ⇒ t에서, cx에는 나타나지만 t에는 나타나지 않는 타입 변수 u가 있을 때, e모호한 타입을 가진다고 말해요. 그런 타입은 무효예요.

예를 들어 아까 showread를 포함한 식은 타입 ∀ a. Show a, Read a ⇒ String을 가지므로 모호한 타입을 가져요.

모호한 타입은 사용자의 입력으로만 우회할 수 있어요. 한 방법은 3.16절에서 설명하는 식 타입 시그니처를 쓰는 거예요. 예를 들어 앞서의 모호한 식에 대해 이렇게 쓸 수 있어요:

let x = read "..." in show (x::Bool)

이것은 타입을 명확히 해요.

가끔은 그렇지 않으면 모호한 식이 식 타입 시그니처로 고정된 타입을 받기보다, 어떤 변수와 같은 타입이 돼야 할 필요가 있어요. 이것이 함수 asTypeOf(9장)의 목적이에요: x 'asTypeOf' yx의 값을 가지지만, xy는 같은 타입을 갖도록 강제돼요. 예:

approxSqrt x = encodeFloat 1 (exponent x 'div' 2) 'asTypeOf' x

(encodeFloatexponent의 설명은 6.4.6절을 봐요.)

Num 클래스의 모호성은 가장 흔하므로, Haskell은 그것을 해소하는 또 다른 방법을 제공해요 — **기본값 선언(default declaration)**으로요:

default (t1 , … , tn)

여기서 n ≥ 0이고, 각 tiNum ti가 성립하는 타입이어야 해요. 모호한 타입이 발견되는 상황에서, 모호한 타입 변수 v는 다음일 때 기본값을 정할 수 있(defaultable)어요:

  • vC v 형태의 제약에만 나타나는데, 여기서 C는 클래스이고,
  • 그런 클래스 중 적어도 하나가 숫자 클래스(즉 Num 또는 Num의 서브클래스)이고,
  • 그런 클래스가 모두 Prelude나 표준 라이브러리에 정의돼 있다. (그림 6.2–6.3이 숫자 클래스를, 그림 6.1이 Prelude에 정의된 클래스를 보여줘요.)

각 기본값을 정할 수 있는 변수는, 모호한 변수의 모든 클래스의 인스턴스인 기본값 리스트의 첫 타입으로 대체돼요. 그런 타입을 찾지 못하면 정적 오류예요.

모듈당 기본값 선언은 하나만 허용되고, 그 효과는 그 모듈로 제한돼요. 모듈에 기본값 선언이 없으면 다음과 같이 가정돼요:

default (Integer, Double)

빈 기본값 선언 default ()은 모듈의 모든 기본값을 끄는 역할을 해요.

4.4 중첩 선언 (Nested Declarations)

다음 선언들은 어떤 선언 리스트에서든 쓸 수 있어요. 모듈의 최상위를 포함해서요.

4.4.1 타입 시그니처 (Type Signatures)

gendecl → vars :: [context =>] type
vars    → var1 , … , varn        (n ≥ 1)

타입 시그니처는 변수들의 타입을, 가능하면 문맥과 함께 지정해요. 타입 시그니처의 형태:

v1, …, vn :: cx => t

은 각 i(1부터 n까지)에 대해 vi :: cx => t를 단언하는 것과 동등해요. 각 vi는 타입 시그니처를 포함하는 같은 선언 리스트에 값 바인딩이 있어야 해요. 즉 바깥 스코프에 바인딩된 변수에 타입 시그니처를 주는 것은 무효예요. 게다가 한 변수에 하나보다 많은 타입 시그니처를 주는 것도 무효예요. 시그니처가 동일하더라도요.

4.1.2절에서 언급했듯이, 시그니처에 나타나는 모든 타입 변수는 그 시그니처에 대해 전역 양화돼요. 따라서 타입 변수의 스코프는 그것을 포함하는 타입 시그니처로 제한돼요. 예를 들어 다음 선언들에서:

f :: a -> a
f x = x :: a -- invalid

두 타입 시그니처의 a들은 꽤 구별돼요. 실제로 이 선언들은 정적 오류를 포함해요. x가 타입 ∀ a. a를 갖지 않기 때문이에요. (x의 타입은 f의 타입에 의존해요. Haskell에서 의존적 타입을 가진 변수에 시그니처를 지정할 방법은 현재 없어요. 이것은 4.5.4절에서 설명해요.)

주어진 프로그램이 변수 f에 대한 시그니처를 포함하면, f의 각 사용은 선언된 타입을 가진 것으로 취급돼요. 같은 타입을 f의 정의 발생에 대해서도 추론할 수 없다면 정적 오류예요.

변수 f가 대응하는 타입 시그니처 선언 없이 정의된다면, 자신의 선언 그룹(4.5절) 밖에서 f의 각 사용은 대응하는 추론된 타입, 즉 **원리 타입(principal type)**을 가진 것으로 취급돼요. 다만 타입 추론이 여전히 가능하도록, 정의 발생과 f의 선언 그룹 안의 모든 사용은 같은 단형성(monomorphic) 타입을 가져야 해요(거기서 원리 타입이 일반화로 얻어짐 — 4.5.2절).

예를 들어 우리가 정의하면:

sqr x = x*x

원리 타입은 sqr :: ∀ a. Num a ⇒ a → a인데, 이는 sqr 5sqr 0.1 같은 적용을 허용해요. 더 구체적인 타입을 선언하는 것도 유효해요. 예:

sqr :: Int -> Int

그렇지만 이제 sqr 0.1 같은 적용은 무효예요. 다음과 같은 타입 시그니처:

sqr :: (Num a, Num b) => a -> b -- invalid
sqr :: a -> a -- invalid

sqr의 원리 타입보다 더 일반적이므로 무효예요.

타입 시그니처는 다형성 재귀(polymorphic recursion)를 지원하는 데도 쓸 수 있어요. 다음 정의는 병리적이지만, 타입 시그니처가 추론될 타입보다 더 일반적인 타입을 지정하는 데 어떻게 쓰이는지 보여줘요:

data T a = K (T Int) (T a)
f :: T a -> a
f (K x y) = if f x == 1 then f y else undefined

만약 시그니처 선언을 제거하면, 첫 재귀 호출에서 f의 인자가 T Int이므로 f의 타입은 T Int -> Int로 추론돼요. 다형성 재귀는 사용자가 더 일반적인 타입 시그니처 T a -> a를 제공할 수 있게 해 줘요.

4.4.2 fixity 선언 (Fixity Declarations)

gendecl → fixity [integer] ops
fixity  → infixl | infixr | infix
ops     → op1 , … , opn       (n ≥ 1)
op      → varop | conop

fixity 선언은 하나 이상의 연산자의 fixity와 바인딩 우선순위를 줘요. fixity 선언의 정수는 0에서 9 범위여야 해요. fixity 선언은 타입 시그니처가 나타날 수 있는 어디든 나타날 수 있고, 타입 시그니처처럼 특정 연산자의 속성을 선언해요. 또한 타입 시그니처처럼 fixity 선언은 연산자 자신의 선언과 같은 선언 시퀀스에서만 나타날 수 있고, 어떤 연산자에 대해 최대 하나의 fixity 선언이 주어질 수 있어요. (클래스 메서드는 사소한 예외인데, 그 fixity 선언은 클래스 선언 자체나 최상위에 나타날 수 있어요.)

세 종류의 fixity, 비결합·좌결합·우결합(infix, infixl, infixr)과 열 개의 우선순위 수준, 0에서 9까지 포함(수준 0이 가장 덜 단단히, 수준 9가 가장 단단히 결합)이 있어요. 숫자가 생략되면 수준 9로 가정돼요. fixity 선언이 없는 어떤 연산자든 infixl 9로 가정돼요(3절에서 fixity 사용에 대해 더 봐요). 표 4.1은 Prelude에 정의된 연산자의 fixity와 우선순위를 나열해요.

표 4.1: Prelude 연산자의 우선순위와 fixity

Prec-  Left associative     Non-associative   Right associative
edence operators            operators         operators
9      !!                   .                 8  ^ , ^^ , **
7      *, /, 'div',                           
       'mod', 'rem', 'quot'                   
6      +, -                                    
5      :, ++                                   
4      ==, /=, <, <=, >, >=,                  
       'elem', 'notElem'                       
3      &&                                      
2      ||                                      
1      >>, >>=                                 
0      $, $!, 'seq'                            

fixity는 타입처럼 특정 개체(생성자·변수)의 속성이지, 그 개체 이름의 속성이 아니에요. 예를 들어:

module Bar( op ) where
    infixr 7 'op'
    op = ...
module Foo where
    import qualified Bar
    infix 3 'op'
    a 'op' b = (a 'Bar.op' b) + 1
    f x = let
              p 'op' q = (p 'Foo.op' q) * 2
          in ...

여기서 'Bar.op'infixr 7, 'Foo.op'infix 3, 그리고 f의 오른쪽 변에서 op의 중첩 정의는 기본 fixity인 infixl 9를 가져요. (중첩 fixity 선언으로 'op'의 중첩 정의에 fixity를 주는 것도 가능해요.)

4.4.3 함수·패턴 바인딩 (Function and Pattern Bindings)

decl   → (funlhs | pat) rhs

funlhs → var apat { apat }
       | pat varop pat
       | ( funlhs ) apat { apat }

rhs    → = exp [where decls]
       | gdrhs [where decls]

gdrhs  → guards = exp [gdrhs]

guards → | guard1, …, guardn        (n ≥ 1)

guard  → pat <- infixexp            (pattern guard)
       | let decls                  (local declaration)
       | infixexp                   (boolean guard)

이 문법에서 두 경우를 구분해요: 왼쪽 변이 pat일 때 패턴 바인딩(pattern binding), 그 외에는 **함수 바인딩(function binding)**이라 불러요. 두 바인딩 모두 모듈 최상위나 where·let 구조 안에 나타날 수 있어요.

4.4.3.1 함수 바인딩 (Function bindings)

함수 바인딩은 변수를 함수 값에 바인딩해요. 변수 x에 대한 함수 바인딩의 일반적인 형태:

x p11 … p1k match1
…
x pn1 … pnk matchn

여기서 각 pij는 패턴이고, 각 matchi는 일반적인 형태:

= ei where { declsi }

또는:

| gsi1 = ei1
…
| gsimi = eimi
where { declsi }

이고 n ≥ 1, 1 ≤ i ≤ n, mi ≥ 1이에요. 전자는 후자의 특정 경우의 약어로 취급돼요. 바로:

| True = ei where { declsi }

함수를 정의하는 모든 절(clause)은 연속되어야 하고, 각 절의 패턴 수는 같아야 해요. 각 매치에 대응하는 패턴 집합은 선형이어야 해요 — 어떤 변수도 전체 집합에 두 번 이상 나타나면 안 돼요.

함수 값을 중위 연산자에 바인딩하는 대안 문법이 제공돼요. 예를 들어 이 세 함수 정의는 모두 동등해요:

plus x y z = x+y+z
x 'plus' y = \ z -> x+y+z
(x 'plus' y) z = x+y+z

fixity 해석이 식(10.6절)과 같은 방식으로 함수 바인딩의 중위 변형에 적용된다는 점에 주의해요. 함수 바인딩의 등호 왼쪽에 fixity 해석을 적용한 결과는, 정의되는 varop이 최상위에 남아 있어야 해요. 예를 들어 우선순위 6의 새 연산자 ##을 정의한다면, 이 정의는 불법이에요:

a ## b : xs = exp

:는 우선순위 5이므로, 왼쪽 변이 (a ## x) : xs로 해석되고, (a ## x)는 유효한 패턴이 아니므로 패턴 바인딩이 될 수 없기 때문이에요.

번역: 함수의 일반 바인딩 형태는 의미론적으로 다음 등식(즉 단순 패턴 바인딩)과 동등해요:

x = \ x1 … xk -> case (x1, …, xk) of (p11, …, p1k) match1
                                          …
                                          (pn1, …, pnk) matchn

여기서 xi는 새 식별자예요.

4.4.3.2 패턴 바인딩 (Pattern bindings)

패턴 바인딩은 변수들을 값에 바인딩해요. 단순 패턴 바인딩은 형태 p = e를 가져요. 패턴 p는 마치 앞에 암묵적 ~가 있는 것처럼 불반박(irrefutable) 패턴으로 "느슨하게(lazily)" 매칭돼요. 3.12절의 번역을 봐요.

패턴 바인딩의 일반적인 형태는 p match인데, match는 위 함수 바인딩과 같은 구조예요. 즉 패턴 바인딩은:

p | gs1 = e1
  | gs2 = e2
  …
  | gsm = em
  where { decls }

번역: 위 패턴 바인딩은 의미론적으로 이 단순 패턴 바인딩과 동등해요:

p = let decls in
    case () of
      () | gs1 -> e1
         | gs2 -> e2
           …
         | gsm -> em
      _ -> error "Unmatched pattern"

4.5 함수·패턴 바인딩의 정적 의미론 (Static Semantics of Function and Pattern Bindings)

let 식이나 where 절의 함수·패턴 바인딩의 정적 의미론을 이 절에서 논의해요.

4.5.1 의존성 분석 (Dependency Analysis)

일반적으로 정적 의미론은 일반 Hindley-Milner 추론 규칙을 적용해 주어져요. 다형성을 높이기 위해, 이 규칙들은 의존성 분석(dependency analysis)으로 식별된 바인딩 그룹들에 적용돼요.

바인딩 b1은 같은 선언 리스트에서, 다음 중 하나일 때 바인딩 b2에 의존해요:

  • b1이 타입 시그니처가 없고 b2가 바인딩한 자유 식별자를 포함하거나,
  • b1b2에 의존하는 바인딩에 의존한다.

**선언 그룹(declaration group)**은 상호 의존 바인딩의 최소 집합이에요. Hindley-Milner 타입 추론은 의존성 순서로 각 선언 그룹에 적용돼요. where/let 구조의 선언 순서는 무관해요.

4.5.2 일반화 (Generalization)

Hindley-Milner 타입 시스템은 let-식에 두 단계로 타입을 부여해요:

  • 선언 그룹들이 의존성 순서로 고려돼요. 각 그룹에 대해, 그룹에 바인딩된 각 변수에 전역 양화 없는 타입이 추론돼요. 그런 다음 그런 타입들에 나타나는 모든 타입 변수는, 타입 환경의 바인딩된 변수와 연관되지 않는 한 전역 양화돼요. 이것을 **일반화(generalization)**라 불러요.
  • 마지막으로 let-식의 본문이 타입 부여돼요.

예를 들어 선언:

f x = let g y = (y,y)
      in ...

을 고려해 보죠. g의 정의 타입은 a → (a,a)예요. 일반화 단계가 g에 다형성 타입 ∀ a. a → (a,a)를 부여하고, 그 후 "..." 부분의 타입 부여가 진행될 수 있어요.

오버로드된 정의를 타입 부여할 때, 한 선언 그룹의 모든 오버로딩 제약이 함께 모여 그룹에 선언된 각 변수의 타입에 대한 문맥을 형성해요. 예를 들어 정의:

f x = let g1 x y = if x>y then show x else g2 y x
          g2 p q = g1 q p
      in ...

g1g2의 정의 타입은 모두 a → a → String이고, 누적된 제약은 Ord a(>의 사용에서)와 Show a(show의 사용에서)예요. 이 제약 집합에 나타나는 타입 변수를 **제약된 타입 변수(constrained type variables)**라 불러요.

일반화 단계는 g1g2 둘 다에 타입 ∀ a. (Ord a, Show a) ⇒ a → a → String을 부여해요. >show의 발생이 g1의 정의에 있어도 g2g1과 같은 방식으로 오버로드된다는 점에 주목해요.

프로그래머가 선언 그룹의 둘 이상의 변수에 명시적 타입 시그니처를 제공하면, 그 시그니처들의 문맥은 타입 변수 이름 바꾸기까지 동일해야 해요.

4.5.3 문맥 축소 오류 (Context Reduction Errors)

4.1.4절에서 언급했듯이 타입의 문맥은 타입 변수, 또는 타입 변수를 하나 이상의 타입에 적용한 것만 제약할 수 있어요. 따라서 일반화가 만든 타입은 모든 문맥 제약이 이 "head normal form"으로 축소된 형태로 표현돼야 해요. 예를 들어 정의:

f xs y = xs == [y]

를 고려해 보죠. 그 타입은:

f :: Eq a => [a] -> a -> Bool

로 주어지고:

f :: Eq [a] => [a] -> a -> Bool

이 아니에요. 동등성이 리스트 타입에서 취해지더라도, 문맥은 일반화 전에 리스트에 대한 Eq의 인스턴스 선언을 써서 단순화돼야 해요. 그런 인스턴스가 스코프에 없으면 정적 오류가 나요.

여기 C (m t) 형태의 제약이 필요한 예가 있어요. 여기서 m은 일반화되는 타입 변수 중 하나이고, 즉 클래스 C가 타입 변수나 타입 생성자가 아닌 타입 식에 적용돼요. 고려해 보죠:

f :: (Monad m, Eq (m a)) => a -> m a -> Bool
f x y = return x == y

return의 타입은 Monad m => a -> m a, (==)의 타입은 Eq a => a -> a -> Bool이에요. 따라서 f의 타입은 (Monad m, Eq (m a)) => a -> m a -> Bool이어야 하고, 문맥은 더 이상 단순화할 수 없어요.

data 타입 deriving 절에서 파생된 인스턴스 선언(4.3.3절)은, 다른 인스턴스 선언처럼 단순 문맥(simple context)을 가져야 해요. 즉 모든 제약은 C a 형태여야 하는데, 여기서 a는 타입 변수예요. 예를 들어 타입:

data Apply a b = App (a b) deriving Show

에서 파생된 Show 인스턴스는 문맥 Show (a b)를 만들 텐데, 이는 축소될 수 없고 단순하지 않아요. 따라서 정적 오류가 나요.

4.5.4 모노모피즘 (Monomorphism)

때로는 정의 타입에 쓰인 모든 타입 변수에 대해 일반화하는 것이 불가능해요. 예를 들어 선언:

f x = let g y z = ([x,y], z)
      in ...

을 고려해 보죠. x가 타입 a를 가지는 환경에서, g의 정의 타입은 a → b → ([a],b)예요. 일반화 단계는 g에 타입 ∀ b. a → b → ([a],b)를 부여해요. a가 타입 환경에 나타나므로 b만 전역 양화될 수 있어요. 우리는 g의 타입이 타입 변수 a에 대해 **단형성(monomorphic)**이라고 말해요.

이런 모노모피즘의 효과는 g의 모든 적용의 첫 인자가 하나의 단일 타입이어야 한다는 거예요. 예를 들어 "..."이:

(g True, g False)

이면 유효하지만(부수적으로 xBool 타입으로 강제),:

(g True, g 'c')

이면 무효해요.

일반적으로 타입 ∀ u. cx ⇒ ta∀ u. cx ⇒ t에 자유로 나타날 때 타입 변수 a에 대해 단형성이라고 말해요.

Haskell이 제공하는 명시적 타입 시그니처는 단형성 타입 변수를 포함하는 타입을 표현할 만큼 강력하지 않다는 점을 알아 둘 가치가 있어요. 예를 들어 우리는 쓸 수 없어요:

f x = let
          g :: a -> b -> ([a],b)
          g y z = ([x,y], z)
      in ...

왜냐하면 그것은 gab 둘 다에 대해 다형성이라고 주장하는 셈이기 때문이에요(4.4.1절). 이 프로그램에서 g는 그 첫 인자가 타입 변수를 포함하지 않는 타입으로 제한될 때에만 타입 시그니처를 받을 수 있어요. 예:

g :: Int -> b -> ([Int],b)

이 시그니처는 또한 x가 타입 Int를 갖게 해요.

4.5.5 모노모피즘 제한 (The Monomorphism Restriction)

Haskell은 위에서 설명한 표준 Hindley-Milner 제한 너머로 일반화 단계에 특정 추가 제한을 두는데, 이는 특정 경우에 다형성을 더 줄여요.

모노모피즘 제한은 변수의 바인딩 문법에 의존해요. 변수는 함수 바인딩이나 패턴 바인딩으로 바인딩되고, 단순 패턴 바인딩은 패턴이 단일 변수만으로 이뤄진 패턴 바인딩이라는 걸 기억해요(4.4.3절).

다음 두 규칙이 모노모피즘 제한을 정의해요:

모노모피즘 제한

  • 규칙 1. 주어진 선언 그룹이 다음일 때 그 그룹은 **무제한(unrestricted)**이라고 말해요:

    • (a) 그룹의 모든 변수가 함수 바인딩이나 단순 패턴 바인딩(4.4.3.2절)으로 바인딩되고,
    • (b) 단순 패턴 바인딩으로 바인딩된 그룹의 모든 변수에 명시적 타입 시그니처가 주어진다.

    다형성에 대한 평소의 Hindley-Milner 제한은 환경에 자유로 나타나지 않는 타입 변수만 일반화될 수 있다는 거예요. 추가로, 제한된(restricted) 선언 그룹의 제약된 타입 변수는 그 그룹의 일반화 단계에서 일반화될 수 없어요. (타입 변수가 어떤 타입 클래스에 속해야 하면 제약된 것임을 기억해요 — 4.5.2절.)

  • 규칙 2. 전체 모듈의 타입 추론이 완료됐을 때 남는 단형성 타입 변수들은 모호한 것으로 간주되고, 기본값 규칙(4.3.4절)으로 특정 타입에 해소돼요.

동기 (Motivation)

규칙 1은 두 가지 이유로 필요한데, 둘 다 꽤 미묘해요.

규칙 1은 계산이 예기치 않게 반복되는 것을 막아요. 예를 들어 genericLength는 (Data.List 라이브러리의) 표준 함수이고 타입은:

genericLength :: Num a => [b] -> a

이제 다음 식을 고려해 보죠:

let { len = genericLength xs } in (len, len)

len이 한 번만 계산되어야 할 것 같지만, 규칙 1이 없으면 서로 다른 두 오버로딩에서 각각 한 번씩, 두 번 계산될 수 있어요. 프로그래머가 실제로 계산 반복을 원한다면 명시적 타입 시그니처를 추가할 수 있어요:

let { len :: Num a => a; len = genericLength xs } in (len, len)

규칙 1은 모호성을 방지해요. 예를 들어 선언 그룹:

[(n,s)] = reads t

을 고려해 보죠. reads는 시그니처:

reads :: (Read a) => String -> [(a,String)]

로 타입이 주어진 표준 함수임을 기억해요. 규칙 1이 없으면 n은 타입 ∀ a. Read a ⇒ a, s는 타입 ∀ a. Read a ⇒ String을 받았을 거예요. 후자는 본질적으로 모호하기 때문에 무효한 타입이에요. s를 어떤 오버로딩으로 쓸지 결정하는 것이 불가능하고, s에 타입 시그니처를 추가해도 해결되지 않아요. 따라서 비단순 패턴 바인딩(4.4.3.2절)이 쓰일 때, 추론된 타입은 타입 시그니처가 제공되는지와 무관하게 항상 그 제약된 타입 변수들에서 단형성이에요. 이 경우 ns 모두 a에서 단형성이에요.

같은 제약이 패턴으로 바인딩된 함수에도 적용돼요. 예를 들어:

(f,g) = ((+),(-))

에서 fg 모두 fg에 제공된 타입 시그니처와 무관하게 단형성이에요.

규칙 2는 현재 모듈 바깥의 모듈들에 타입 추론을 수행하지 않고서는 export된 바인딩의 단형성 사용을 강제할 방법이 없기 때문에 필요해요. 규칙 2는 모듈에 바인딩된 모든 변수의 정확한 타입이, 그것을 import하는 모듈이 아니라 그 모듈 자체만으로 결정되어야 한다고 명시해요.

module M1(len1) where
  default( Int, Double )
  len1 = genericLength "Hello"
module M2 where
  import M1(len1)
  len2 = (2*len1) :: Rational

모듈 M1에 대한 타입 추론이 완료되면 len1은 단형성 타입 Num a => a를 가져요(규칙 1로). 규칙 2는 이제 단형성 타입 변수 a가 모호하므로 4.3.4절의 기본값 규칙으로 해소되어야 한다고 명시해요. 따라서 len1은 타입 Int를 얻고, len2에서의 사용은 타입이 맞지 않아요. (만약 위 코드가 실제로 원하는 것이라면, len1에 타입 시그니처를 주면 문제가 풀려요.)

이 문제는 중첩 바인딩에는 생기지 않아요. 그 전체 스코프가 컴파일러에게 보이기 때문이에요.

결과 (Consequences)

모노모피즘 규칙은 프로그래머에게 여러 결과를 주어요. 함수 문법으로 정의된 것은 대개 함수가 기대하는 대로 일반화돼요. 그래서:

f x y = x+y

에서 함수 fNum 클래스의 어떤 오버로딩에서든 쓸 수 있어요. 여기서 재계산의 위험은 없어요. 그러나 같은 함수를 패턴 문법으로 정의하면:

f = \x -> \y -> x+y

f가 완전히 오버로드되려면 타입 시그니처가 필요해요. 많은 함수가 단순 패턴 바인딩으로 가장 자연스럽게 정의되는데, 사용자는 완전한 오버로딩을 유지하려면 이것에 타입 시그니처를 붙이는 데 조심해야 해요. 표준 prelude에는 이것의 예가 많아요:

sum :: (Num a) => [a] -> a
sum = foldl (+) 0

규칙 1은 최상위·중첩 정의 모두에 적용돼요. 고려해 보죠:

module M where
  len1 = genericLength "Hello"
  len2 = (2*len1) :: Rational

여기서 타입 추론은 len1이 단형성 타입 (Num a => a)를 갖는다는 걸 찾아내고, len2에 대해 타입 추론을 수행할 때 타입 변수 aRational로 해소돼요.

4.6 종류 추론 (Kind Inference)

이 절은 종류 추론을 수행하는 데 쓰이는 규칙, 즉 주어진 프로그램에 나타나는 각 타입 생성자·클래스에 적절한 종류를 계산하는 규칙을 설명해요.

종류 추론 과정의 첫 단계는 데이터타입·별칭·클래스 정의 집합을 의존성 그룹으로 배열하는 것이에요. 이는 4.5절에서 설명한 값 선언에 대한 의존성 분석과 거의 같은 방식으로 이룰 수 있어요. 예를 들어 다음 프로그램 조각은 데이터타입 생성자 D, 별칭 S, 클래스 C의 정의를 포함하는데, 모두 같은 의존성 그룹에 포함될 거예요:

data C a => D a = Foo (S a)
type S a = [D a]
class C a where
    bar :: a -> D a -> Bool

각 그룹 안의 변수·생성자·클래스의 종류는 타입 추론과 종류 보존 유니피케이션(kind-preserving unification)의 표준 기법으로 결정돼요 [8]. 예를 들어 위 정의에서 매개변수 abar의 타입에서 함수 생성자 (->)의 인자로 나타나므로 종류 를 가져야 해요. 따라서 DS 모두 종류 ∗→∗를 가져야 하고, 클래스 C의 모든 인스턴스는 종류 를 가져야 해요.

추론된 종류의 일부는 대응하는 정의로 완전히 결정되지 않을 수 있어요. 그런 경우 의 기본값이 가정돼요. 예를 들어 다음 각 예에서 a 매개변수에 임의의 종류 κ를 가정할 수 있어요:

data App f a = A (f a)
data Tree a = Leaf | Fork (Tree a) (Tree a)

이것은 AppTree에 각각 어떤 종류 κ에 대해 종류 (κ→∗) → κ →∗κ →∗를 주고, 다형성 종류(polymorphic kinds)를 허용하는 확장을 요구할 거예요. 대신 기본 바인딩 κ = ∗을 쓰면, 이 두 생성자의 실제 종류는 각각 (∗→∗) →∗→∗∗→∗예요.

기본값은 나중에 의존성 그룹이나 프로그램의 다른 곳에서 특정 타입 생성자 상수·클래스가 쓰이는 방식과 무관하게 각 의존성 그룹에 적용돼요. 예를 들어 다음 정의를 위의 것들에 추가해도 Tree에 대해 추론된 종류에는 영향을 주지 않고(예를 들어 (∗→∗) →∗로 바꾸지 않고), 대신 []의 종류 ∗→∗Tree의 인자에 기대되는 종류 와 맞지 않으므로 정적 오류를 만든다:

type FunnyTree = Tree [] -- invalid

이것은 각 생성자·클래스가 스코프에 있을 때마다 같은 종류로 일관되게 쓰이는 것을 보장하므로 중요해요.

더 알아보기 (Learn more)