에러가 읽기 어려울 땐, :type 을 써서 단계마다 타입을 추적하면 도움이 됩니다. 일부러 간단한 구문을 에러가 나도록 해서 메시지를 따라가 봤습니다.
> fmap ((+1) . fst) (Just 1, 100)
<interactive>:5:1: error:
Non type-variable argument in the constraint: Num (b1, b2)
• ...
fst
함수를 튜플 (Just 1, 100)
에 적용해서 나온 Just 1
에 fmap (+1)
을 적용해서 Just 2
를 결과로 예상했지만, 위와 같은 에러를 만났습니다.
타입 제약에 타입 변수가 아닌 인자가 있다?
> :t fmap ((+1) . fst) (Just 1, 100)
fmap ((+1) . fst) (Just 1, 100)
:: (Num b1, Num a, Num (b1, b2)) => (Maybe a, b1)
=>
왼쪽의 타입 제약constraint을 보면 Num (b1, b2)
가 보입니다. GHC는 왜 이런 제약이 있다고 추론했을까요?
fmap
은 어떤 인스턴스의 fmap
을 선택했을까요?
fmap
의 두 번째 인자 (,)
를 보고, 튜플의 fmap 을 선택했습니다.
fmap :: (a0 -> b) -> (a, a0) -> (a,b)
fmap
첫 번째 매개 변수의 타입은 a0 -> b
> :t (+1) . fst
(+1) . fst :: Num c => (c, b) -> c
a0
는 (c,b)
가 되고, b
는 c
라고 추론합니다.
fmap
두 번째 매개 변수의 타입은 (a, a0)
(Just 1, 100)
인자가 들어왔으니, a
는 Just 1
, a0
는 100
위에서 a0
는 튜플로 추론됐고, 여기서는 100
을 보고 Num
클래스 소속이라고 추론 됐습니다.
100
은 Int
나 Double
이 아니고 Num a => a
입니다.
Num (,)
이라고 추론 됐습니다. (,)
인스턴스가 없다는 에러가 날 것 같기도 한데, 그 이전에 Num a => a
에서 a
에 튜플이 들어갑니다. 제약에는 타입 클래스만 올 수 있습니다. a, b
같은 타입 변수가 아니라 생성자 (,)
가 들어왔기 때문에 non type-variable argument 란 에러가 납니다.
에러는 괄호를 잘 못 씌워서 원하는 대로 함수 적용이 이루어지지 않았는데, 에러 메시지는 “제약에 타입 변수가 아닌게 들어왔다” 입니다. 꽤 오랫동안 봐야 통찰이 생길 것 같은 연결 고리입니다.
아래처럼 바꿨더니 이 번에 다른 에러가 납니다.
> fmap (+1) . fst (Just 1, 100)
<interactive>:13:13: error:
Couldn't match expected type ‘a -> f b’
• type ‘Maybe Integer’ ---- [2]
with actual Possible cause: ‘fst’ is applied to too many arguments
• In the second argument of ‘(.)’, namely ‘fst (Just 1, 100)’
In the expression: fmap (+ 1) . fst (Just 1, 100)
In an equation for ‘it’: it = fmap (+ 1) . fst (Just 1, 100)
Relevant bindings include
• it :: a -> f b (bound at <interactive>:53:1)
:t
로 보려고 해도 같은 에러가 나서, 이 방법으로 힌트를 얻을 수 없습니다.it :: a -> f b
가 보입니다. 전체의 표현식이 a -> f b
라고 추론했다는데 왜 그렇게 했을까요?fmap (+1) (. fst (Just 1, 100)) -- 이렇게 나눠지지 않습니다.
fmap (+1)) . (fst (Just 1, 100)) -- 이렇게 나눌 수 있습니다. (.) 의 우선순위가 높습니다. ---- [1]
(
(.) :: (b -> c) -> (a -> b) -> a -> c
fmap :: Functor f => (a -> b) -> f a -> f b
-> c) 가 fmap (+1) :: (functor f, Num b) => f b -> f b (b
b
자리에 f b
, c
자리에 f b
로 추론하고
그럼 두 번째 함수인 a -> b
는 a -> f b
라 추론합니다.
그런데, 두 번째 함수는 (fst (Just 1, 100))
에서 Num a => Maybe a
로 추론됩니다.
그래서 a -> f b
와 Maybe a
가 매칭이 안된다는 에러가 납니다.
에러는 괄호를 잘 못 씌워서 원하는 대로 함수 적용이 이루어지지 않았는데, 에러 메시지는 “타입 매칭 실패” 입니다. 에러에 대한 경험을 키우는 것 말고, 별다른 이해 방법이 없어 보입니다. GHC 입장에서 생각하고, 타입 추론을 생각하며 에러를 따라 가는 훈련이 필요합니다.
[1] 괄호 없는 표현식의 실행 순서
:info 명령어로 우선 순위를 볼 수 있습니다. prefix 적용은 따로 우선 순위 표현이 없는데, 중위 연산자와 섞어 쓸 때면 우선순위가 가장 높은 10이라고 보면 되겠습니다. infix 만 우선 순위가 있습니다. 그리고 또 한가지, 중위 연산자는 함수를 넘기는 곳에 인자로 쓰일 수 없습니다. 괄호로 감싸서 prefix 로 만들어 넘겨야 합니다.
-- . 은 infixr 9
-- $ 는 infixr 0
-- 아래처럼 괄호 없이 써도 연산자 우선 순위가 있어 제대로 계산됩니다.
> add x y = x + y
> add 1 . add 2 $ 3
-- 중위 연산자 . 보다 앞에 add가 우선 실행되고, 중위 연산자 . 이 동작하기 전 뒤에 add를 먼저 실행합니다.
-- 그리고 나서 . 연산자를 실행합니다.
-- $ 의 우선 순위는 infixr 0 입니다. 가장 낮은 우선 순위입니다.
6
> 1 + add 1 1
3
> fmap (+1) . fmap (+2) $ [1,2]
4,5] [
[2] 실제 타입으로 Maybe a
가 아니라 Maybe Integer
가 나온건 Num
클래스의 디폴트 타입이 Integer
라서 그렇습니다. 숫자 리터럴은 별다른 추론 단서가 없다면 Integer
로 추론됩니다.
https://kseo.github.io/posts/2017-01-04-type-defaulting-in-haskell.html