확장 MonomorphismRestriction

Posted on July 10, 2020

검증 필요

타입 서명을 써주지 않으면, 가끔은 GHC가 type defaulting 규칙으로 타입을 결정합니다. 추론 단서가 없으면 클래스에 정의되어 있는 default 타입으로 추론합니다.

컴파일된 모듈은 디폴트로 monomorphism restriction이 켜져 있고, GHCi 프롬프트에서는 디폴트로 꺼져 있습니다.

> :set -XMonomorphismRestriction
> let plus = (+)
> plus 1.1 1.1

<interactive>:30:6: error:
No instance for (Fractional Integer)
    arising from the literal ‘1.1
In the first argument of ‘plus’, namely ‘1.1
    In the expression: plus 1.1 1.1
    In an equation for ‘it’: it = plus 1.1 1.1

> :set -XNoMonomorphismRestriction
> let plus = (+) -- Restriction을 바꾸면, 재정의 후 테스트해야 합니다.
> plus 1.1 1.1
2.2

확장이 켜져 있는 상태에서 plus 타입이 추론되는 걸 보면, 1.1 리터럴을 보고 Fractional 클래스라고 판단하는데, 타입 서명 없이 plus를 정의하면 GHC가 알아서 type defaulting 규칙에1 따라 Num 클래스의 디폴트 타입 Integer를 적용해 Integer -> Integer -> Integer 타입으로 추론합니다. Fractional 클래스의 Integer 인스턴스가 없기 때문에 오류가 납니다.

monomorphism restriction을 켜면 아래 함수 정의들의 해석이 달라집니다.

Monomorphism restriction - Haskell Wiki

-- GHC는 f1 :: (Show x) => x -> String 로 추론한다. (1)
f1 x = show x

-- 아래는 위와 다르게 GHC는 f2 :: () -> String 로 추론한다. (2)
f2 = \x -> show x

-- 두 번째에 명시적으로 타입 서명을 주면 결과가 같아진다. (3)
f3 :: (Show a) => a -> String
f3 = \x -> show x

-- 포인트 프리로 적은 건 () -> String 로 추론된다. (4)
f4 = show

-- 역시 타입 서명을 추가하면 포인트 프리로 쓸 수 있다. (5)
f5 :: (Show a) => a -> String
f5 = show

검증 필요 왜 (1)과 (2)가 다르게 추론될까요?
monomorphism restriction 때문에 다르게 추론됩니다.
(1)번은 f1show와 함수 바인딩2해서 x를 추론합니다. f1show x 함수에 이름을 붙여 놓은겁니다. show가 그렇듯이 f1Show 인스턴스를 인자로 받습니다. 하지만 (2)번은 show가 아닌 \x -> ... 람다 함수에 이름을 붙여 놓은 겁니다. 여기서도 역시 하스켈은 lazy합니다. show가 실행이 필요해지기 전까지는 x가 뭔지 모르는 상태입니다. monomorphism 제한이 켜져 있는 상태에서 추론 단서가 전혀 없으면 ()로 추론합니다.
그럼 (4)번은 어떻게 해석하면 될까요? (1)번에서 x는 함수 바인딩 하면서 show에 있는 매개 변수의 정보를 가져오지만, (4)번은 매개 변수는 상관없이 show 함수와 바인딩합니다.

함수가 폴리모픽 매개 변수를 가지고 있을 때, 폴리모픽 변수가 한가지로 추론되지 않으면 이 함수는 한가지mono 모양morphic이 아닙니다. 여러 코드 덩어리가 준비되어 있고, 나중에 구체 타입이 결정 되면 한가지를 고르게 됩니다.

monomorphic을 켜면, GHC가 함수 정의를 보고 타입을 추론할 단서(타입 서명)를 찾아 보고, 구체 타입까지 도달 못하고 클래스까지만 추론 가능하면 디폴트 규칙에 따라 함수의 타입을 결정합니다.

monomorphic을 끄면, GHC는 클래스까지만 추론되면 그대로 두고, 구체 타입은 나중에 코드 조합에 따라 결정합니다.


  1. Type defaulting in Haskell - Kwang’s Haskell Blog
    Prelude numeric 클래스들만 디폴트값을 갖고 있고, 사용자 정의 클래스들에 디폴트를 지정할 수 없습니다.

    default Num Integer
    default Real Integer
    default Enum Integer
    default Integral Integer
    default Fractional Double
    default RealFrac Double
    default Floating Double
    default RealFloat Double
    ↩︎
  2. Part I - The Haskell 2010 Language - Chapter4 Declarations and Bindings
    코드에 임시로 이름을 붙여 놓는 걸 바인딩이라 합니다. 모듈(함수)의 최상위 레벨이나, let절, where절등에서 바인딩할 수 있습니다.

    함수 바인딩

    someFunc (Just x) = ...작업 코드
    someFunc Nothing = ...작업 코드

    function 바인딩은 someFunc란 이름을 작업 코드의 결과값에 붙이는 작업입니다. 매개 변수 자리에 들어오는 값과 비교할 패턴이 있을 경우 패턴 바인딩이 일어납니다.

    패턴 바인딩

    매개 변수를 통해 들어온 값에 이름을 붙이는 작업입니다. 예를 들어 매개 변수를 통해 Just 5가 들어오면 Just x란 패턴과 매칭해서 5x와 바인딩합니다.

    ※ 위 링크의 문서를 보면

    -- (##) 이란 연산을 정의하고 아래같이 써주면?
    a ## b : xs = ...

    이렇게 선언할 수 없다고 나옵니다. 패턴 매칭은 값 생성자와 하지 일반 함수와 하지 않습니다. 문서는 정교하게 정의를 해야하니 좀 복잡하게 쓰여 있습니다.↩︎

Github 계정이 없는 분은 메일로 보내주세요. lionhairdino at gmail.com