※ 아래 GHCi에 실행 예시들은 :set -ddump-simple
옵션을 준 상태입니다.
:set -fprint-typechecker-elaboration
:set -ddumpt-types
타입 추론 단계를 보여 주긴 하지만, “왜” 그렇게 추론했는지까진 알려 주진 않습니다.
타입을 추론해서, 명확한 타입들을 붙여 나가는 작업을 말하며, 타입이 명시화 된 Core 언어 코드를 elabrated 됐다고 말합니다. 번역한다면, “명시화” 정도 될 것 같습니다.
f(X, 1) = f(2, Y)
일 때, 이 식이 성립하게 하려면 X = 2
, Y = 1
로 unification한다고 말합니다. 이 때, X
,Y
를 unification variable이라 부릅니다. 번역하면, 일치화 정도 될 것 같습니다.
a ~ b
a
와 b
가 같은 타입이어야 한다는 제약입니다.
※ 참고 Type equality constraint “~”
특별한 이유가 있는 것 같진 않고, 구분을 위해 아래 접두어를 붙이는 걸로 보입니다.
#f
: 타입 클래스 인스턴스 딕셔너리
#d
: 타입 클래스 제약constraint
#c
: 클래스 메소드
GHC는 클래스의 여러 인스턴스들 중에 적합한 인스턴스를 선택해서 해당 인스턴스에 있는 메소드를 사용합니다. 인스턴스를 딕셔너리 구조로 만들고, Core 언어에선 타입에 맞는 딕셔너리를 함수에 인자로 넙깁니다. 이때 딕셔너리들엔 #
을 붙입니다. 예를 들어, #fOrdInt
라 하면, Ord
클래스 인스턴스 중 Int
인스턴스를 가리킵니다.
가장 단순한 추론 과정을 보겠습니다.
{-# LANGUAGE NoImplicitPrelude #-}
-- 하스켈의 표준 라이브러리라 할 수 있는 Prelude를 쓰려면 명시하겠다는 뜻입니다.
class Ord a where
compare :: a -> a -> Bool
instance Ord Int where
compare x y = x <= y
foo :: Int -> Int -> Bool
= compare foo
> :t foo
λfoo :: GHC.Types.Int -> GHC.Types.Int -> GHC.Types.Bool
> foo 1 1
λ
==================== Simplified expression ====================
let {
it_a1wP :: GHC.Types.Bool -- 결과 저장을 위한 변수
LclId, -- it_a1wP의 속성을 [...] 안에 써줍니다. LclId는 지역 변수란 뜻
[Unf=Unf{Src=<vanilla>, TopLvl=False, -- 최적화 관련 속성.
Value=False, ConLike=False, WorkFree=False, Expandable=False,
Guidance=IF_ARGS [] 50 0}]
= Ghci5.foo (GHC.Types.I# 1#) (GHC.Types.I# 1#) } in
it_a1wP -- IO a -> IO b -> IO b 첫 번째 IO작업이 끌나고, 다음 IO작업 실행을 의미
GHC.Base.thenIO @() -- 타입을 명시하지 않거나, 타입을 무시하는 의미
@[GHC.Types.Any] -- 리스트 타입 Any
@GHC.Types.Bool GHC.Show.$fShowBool it_a1wP)
(System.IO.print
(GHC.Base.returnIO@[GHC.Types.Any]
GHC.Types.: -- 리스트의 Con을 뜻하는 (:))
(@GHC.Types.Any
case Unsafe.Coerce.unsafeEqualityProof
(@(*) @GHC.Types.Any @GHC.Types.Bool
of
Unsafe.Coerce.UnsafeRefl v2_aoH ->
{ `cast` (Sub v2_aoH :: GHC.Types.Bool ~R# GHC.Types.Any)
it_a1wP
})GHC.Types.[] @GHC.Types.Any)))
(
True
다형성이 있는 인자나 출력을 가진 함수라면, 함수가 현재 어떤 타입에서 작동해야 하는지, 어떤 인스턴스 딕셔너리를 써야 하는지등의 정보가 필요한데, Core에선 이들 정보 모두가 인자로 표시 됩니다. 클래스 제약도 마찬가지로 인자로 표시됩니다.
GHCi에서 작동한 print
는 it_a1wP
를 출력하는데, 값을 타입은 @GHC.Types.Bool
타입이고, Show
클래스의 인스턴스 GHC.Show.$fShowBool
를 쓰겠다는 뜻입니다.
@
타입 적용Type application, 타입 지정 효과가 납니다. 다형성이 있는 변수에 @
를 붙이면 하나의 타입으로 고정합니다.
GHCi에서 방금 평가한 표현식을 가리킬 때 씁니다.
λ> 42
42
λ> it + 1 43
Core 언어에서 이 it
에 바인딩하는 변수가 it_aFo
입니다. _aFo
는 유일한 이름을 붙이기 위해 붙이는 변수명입니다.
I#
, 1#
같은 것들은 프리미티브 타입을 뜻합니다. 하스켈의 일반적인 정수 Int
, Integer
는 boxed 타입이지만, #
이 붙은 타입들은 unboxed 원시 타입입니다.
1#
: Int#
타입의 리터럴 값 1
복잡하니 위 소스에서 GHC.Types.
, GHC.Base.
, GHC.Show
, Ghci5.
를 지우고 읽으면
let {
it_a1wP :: Bool
LclId,... 최적화]
[= foo (I# 1#) (I# 1#) } in
it_a1wP -- 위 구문은 여기서 끝나고, 아래는 GHCi에서 출력에 관한 동작입니다.
-- <--- 함수
thenIO @()
@[Any]
@Bool $fShowBool it_a1wP) -- <--- 함수
(System.IO.print -- <--- 함수
(returnIO @[Any]
:
(@Any
case Unsafe.Coerce.unsafeEqualityProof
(@(*) @Any @Bool
of
Unsafe.Coerce.UnsafeRefl v2_aoH ->
{ `cast` (Sub v2_aoH :: Bool ~R# Any)
it_a1wP
})@Any))) ([]
arr (const 42)
의 타입은 a b c
입니다. ->
타입이 아닙니다. 다시 말해 인자를 받을 수 있는 함수가 아닙니다. 그런데 ()
를 바로 넣어 주면, 알아서 Arrow
의 ->
인스턴스에 있는 arr
로 추론하고, const 42 ()
의 결과를 출력합니다. 위의 compare
가 Int
인자 타입을 보고, 인스턴스를 추론하는 것과 조금 다른 절차가 있는 듯이 보였습니다.
이 글은 arr
메소드를 어째서 Arrow
의 ->
인스턴스에 있는 메소드를 골라오는 것인가에서 시작했습니다.
인자를 주는 모양 때문에 Arrow
의 (->)
의 인스턴스를 찾아 온 것일까요? 그렇다면, pure ()
같은 경우도, Applicative
클래스의 (->)
인스턴스로 추론 될까요?
(※ 마치 GHCi에서 Num
클래스의 디폴트 인스턴스로 Integer
를 지정해서 쓰는 것처럼 Arrow
클래스도 디폴트 인스턴스 지정이 있는 건가 했는데, 그 건 아니었습니다.)
λ> arr (const 42) ()
42
λ> :t arr (const 42)
arr (const 42) :: (Arrow a, GHC.Num.Num c) => a b c
λ> :t arr (const 42) () arr (const 42) () :: GHC.Num.Num t => t
==================== Simplified expression ====================
let {
it_aFo :: forall {t}. GHC.Num.Num t => t
LclId,
[Arity=1, --하나의 인자, Num t => 딕셔너리를 받는다.
Unf :
Unf=Unf{Src=<vanilla>, TopLvl=False,
Value=True, ConLike=True, WorkFree=True, Expandable=True,
Guidance=IF_ARGS [30] 100 0}]
it_aFo = \ (@t_aWv) ($dNum_aWP :: GHC.Num.Num t_aWv) ->
Control.Arrow.arr@(->) -- (->) 타입 적용
Control.Arrow.$fArrowFUN -- `Arrow`클래스의 `Fun` 딕셔너리
@()
@t_aWv
(GHC.Base.const@t_aWv
@()
@t_aWv $dNum_aWP (GHC.Num.Integer.IS 42#)))
(GHC.Num.fromInteger GHC.Tuple.Prim.() } in
...
λ> pure ()
λ> :t pure
pure :: Applicative f => a -> f a
λ> :t pure ()
pure () :: Applicative f => f ()
==================== Simplified expression ====================
GHC.Base.bindIO@()
@[GHC.Types.Any]
(GHC.GHCi.ghciStepIO@GHC.Types.IO
GHC.GHCi.$fGHCiSandboxIOIO
@()
(GHC.Base.pure@GHC.Types.IO GHC.Base.$fApplicativeIO @() GHC.Tuple.Prim.()))
it_aXZ :: ()) ->
(\ (
GHC.Base.returnIO@[GHC.Types.Any]
GHC.Types.:
(@GHC.Types.Any
case Unsafe.Coerce.unsafeEqualityProof @(*) @GHC.Types.Any @() of
(Unsafe.Coerce.UnsafeRefl v2_aoH ->
{ `cast` (Sub v2_aoH :: () ~R# GHC.Types.Any)
it_aXZ
})GHC.Types.[] @GHC.Types.Any))) (
arr (const 42)
가 ()를 받으려면 arr
의 결과가 (->)
타입이어야 하니, (->)
인스턴스의 arr
이 적당한 후보다?
pure ()
는 pure
가 ()를 받으려면 pure
의 결과가 (->)
타입이어야 하니 (->)
의 인스턴스로 추론될 것 같지만, Applicative
의 IO
인스턴스의 pure
로 추론됩니다.
arr (const 42) :: Arrow a => a b Int
에 ()
를 적용하려면, arr
의 결과 타입 a b Int
가 () -> Int
가 되어야 합니다. (->)
타입을 다른 타입과 다름 없는 타이처럼 보이게 type Func = (->)
로 두면, Func () Int
타입이 되어야 한다고 말할 수 있습니다. arr의 결과 타입이 Func인 인스턴스를 고르면, Arrow의 Func
인스턴스에 있는 arr
로 추론합니다.
pure :: Applicative f => a -> f a
에 ()
를 적용하려면, a -> f a
가 () -> f a
가 되어야 하고, 아직 f
에 대한 제약은 없습니다. f
는 필요할 때가 되면 찾을 겁니다. Applicative의 모든 인스턴스에 있는 pure
는 다 (->) a (f a)
타입이니, 현재 단서로는 더 이상 추론할 수 없습니다.
arr
의 경우엔, 인자를 받을 수 있는 ->
여야 한다는 추론 단서로 인해 a
가 (->)
이 되어야 한다는 것까지 추론 되었지만, pure
의 경우엔 인자를 받을 수 있어야 된다는 것 만으론 f
를 추론하지 못했습니다. pure ()
가 쓰이는 컨텍스트를 보고 최종 f
를 결정하게 됩니다. GHCi의 인터랙션 환경은 IO
이므로, 결국 f
를 IO
로 추론합니다.
위 설명에, 타입 체크, 타입 추론, instance resolution과 type defaulting까지 다 섞어서 인포멀하게 말고 있습니다. 포멀하게 말하는 능력을 키워야 하는데 쉽지 않습니다.
※ @jhhuh님이 아래 영상을 추천해 주시고, equality constraint를 쌓아 뒀다가 추론한다고 조언해 주셨습니다. 감사합니다.
Simon Peyton Jones how GHC type inference engine actually works
위 영상에 쓰인 슬라이드