확장 FlexibleInstances, FlexibleContexts

Posted on July 13, 2020

FlexibleInstances

instance SomeClass (Maybe a) where ... -- o
instance SomeClass (Maybe [a]) where ... -- x
instance SomeClass (Maybe Int) where ... -- x

Maybe a는 가능하지만, Maybe Int는 안됩니다.
Haskell98에서 이렇게 제약을 해두었다고 하는데 왜 그럴까요?

딱히 왜 그런지에 대한 근거 자료는 아직 찾지 못해서 추측만 해봅니다. 인스턴스를 찾는 과정을 추측해보면 Maybe까지만 매칭해서 결정하면 되는지, Maybe를 벗겨내고 안 쪽도 매칭을 확인해야 되는지 과정의 차이가 있습니다. 제일 바깥의 생성자만 매칭하도록 만들었다면 Maybe Int라고 쓰지 못하도록 하는게 의미가 있지 않을까요. 어디까지나 추측입니다. 다른 의견이 있는 분들은 댓글 부탁드립니다.

이 제약을 풀려면 FlexibleInstances 확장을 켜주면 됩니다. 실제 ad-hoc하게 메소드를 만들려면 당연히 가능해야 할 것도 같은데 Haskell98에서는 왜 막아 두었을까요. 이게 가능하지 않다면, Maybe IntMaybe Double을 다루는 메소드가 다를 경우 인스턴스를 각 각 만들 방법이 없습니다.

인스턴스 표현식의 부분 부분을 다음처럼 부릅니다.

instance (Num t) => SomeC [t] where
      constraint      head

haskell.org의 설명에 따르면 인스턴스 헤드 부분에 여러 개의 생성자로 쌓인 타입(abitrary nested types in the instance head)이 와도 인스턴스를 만들 수 있게 해주는 확장이라고 되어 있습니다.

이 확장은 TypeSynonymInstances 확장을 켜면 따라서 켜지기도 합니다.

아래 코드를 확장을 켜고, 끄고 컴파일 해보세요.

{-# LANGUAGE FlexibleInstances #-}
data Wrapper a = Wrapper a
data Inner1 = Inner1
data Inner2 = Inner2

instance Eq (Wrapper Inner1) where
  (==) x y = True

instance Eq (Wrapper Inner2) where
  (==) x y = True

FilexibleContexts

위와 마찬가지로, context에 타입 변수만 올 수 있는데, 이 확장을 켜면 구체 타입도 써줄 수 있습니다.

http://dev.stephendiehl.com/hask/#flexibleinstances 2023.11.30 깨진 링크입니다.

{-# LANGUAGE FlexibleContexts #-}

class MyClass a

-- 확장 없이는 구체 타입이 아닌 타입 변수만 올 수 있습니다.
instance (MyClass a) => MyClass (Either a b)

-- 확장을 켜면, 여러겹 쌓여 있는 구체적인 타입도 OK
instance (MyClass (Maybe a)) => MyClass (Either a b)

컨텍스트는 나중에 타입을 추론할 때, 범위를 좁히는 역할을 합니다. 그런데, 여기에 구체 타입을 써준다면, 타입 추론을 할 필요가 없는데 컨텍스트로 써줄 의미가 없지 않을까요? 그래서 구체 타입이 아닌 클래스와 타입 변수만 올 수 있게 해놨을 것 같습니다.(추측) 하지만, MultiParam 타입 클래스가 가능할 경우는, 여러 개의 타입 중 하나는 고정되고, 나머지 것들은 타입 추론한테 맡기는 경우도 있을 수 있습니다. 그래서 보통 MultiParamTypeClasses와 같이 사용합니다.

somefunc :: (MonadReader c m) => ...
somefunc ... = ... -- 여기 코드에는 c를 SomeConfig로 추론할만한(달리 말하면, SomeConfig여야만 하는) 단서가 되는 코드가 있습니다.

이럴 경우 somefunc 안에서 SomeConfig가 들어올거라 예상하고 작업을 하기 때문에, 나중에 코드 조립할 때 SomeConfig만 받겠다 안전 장치가 필요합니다. 하지만, 해당 정보가 어디에도 없습니다. 그래서 MonadReader c m 대신 MonadReader SomeConfig m라고 제약 사항을 두려하면 아래같은 오류가 납니다.

Couldn't match type ‘c’ with ‘SomeConfig
        arising from a functional dependency between constraints:
MonadReader SomeConfig m

이럴 때도, MonadReader c a가 아닌 MonadReader SomeConfig a로 일부 타입 변수를 구체 타입으로 써주려면 이 확장을 켜야 합니다.

이게 처음에는 좀 혼란스러운 부분입니다. SomeConfig로 추론했다면 GHC가 알아서 나중에 타입 매칭을 해서 SoneConfig가 아닌게 들어오면 에러를 내면 좋은데, “이 함수는 SomeConfig 타입을 쓰는데, 나중에 다른게 들어오면 안되잖아, 그러니 네가 확실하게 통과 절차를 만들어 둬.”라고 미리 강요합니다. 아직 관련 설명은 보지 못했지만, 간단하게 다음과 같은 경우를 생각해 보면 이유를 추측할 수 있습니다. somefunc를 다른 부품(함수)과 조립할 때, 타입 서명만 보면서 하려면 타입 서명에 필요한 조건은 모두 적어놔야만 합니다.

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