확장 MultiParamTypeClasses, FunctionalDependencies

Posted on July 29, 2020

MultiParamTypeClasses

{-# LANGUAGE MultiParamTypeClasses #-}
class SomeC a b where
  method :: a -> b -> IO ()

instance SomeC Integer Double where
  method _ _ = putStrLn "Double ok"

instance SomeC Integer Char where
  method _ _ = putStrLn "Char ok"

> method 1 1.0
Double ok
> method 1 'a'
Char ok

타입 클래스를 정의할 때, 타입 매개 변수가 두 개 이상인 것도, 또는 하나도 없는 것도 가능하게 해주는 확장입니다.
확장을 끄고 위 클래스를 정의하면 에러가 납니다.

Too many parameters for classSomeC
      (Enable MultiParamTypeClasses to allow multi-parameter classes)
-- 클래스 매개 변수는 하나만 가능합니다. 

Illegal instance declaration for ‘SomeC Integer Double
        (Only one type can be given in an instance head.
         Use MultiParamTypeClasses if you want to allow more, or zero.)
-- 인스턴스 헤드에는 하나의 타입만 있어야 합니다.

아마도 초창기 하스켈은, 인스턴스 헤드에 있는 타입의 가장 바깥을 싸고 있는 생성자하고만 매칭을 시도 했던 것 같습니다. 디폴트로 이 기능이 들어가 있지 않은 이유를 아는 분은 댓글 부탁드립니다.

FunctionalDependencies

{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FunctionalDependencies #-}

class SomeC a b c where
  method :: a -> b -> c

instance SomeC Int Int Int where 
  method a b = a + b

main :: IO ()
main = putStrLn $ show $ method 1 2  

위 코드는 method 의 결과 타입이 모호하다는 에러를 내뱉습니다. 왜 GHC가 추론하지 못할까요?

instance SomeC Int Int Double where
  method a b = (a + b) :: Double

이런 인스턴스가 추가로 있다고 생각하면, method 1 2 를 보고, SomeC Int Int Int를 골라야 할지, SomeC Int Int Double을 골라야 할지 알 수 없습니다. 인스턴스가 하나만 있다고 GHC가 무조건 있는 인스턴스를 고르는 건 아닙니다.

이럴 때, 매개 변수로 들어오는 타입들 중 하나가 다른 타입에 의존한다고 알려주면 GHC가 추론을 마칠 수 있는 단서가 됩니다.

class SomeC a b c | a b -> c where
  method :: a -> b -> c

instance SomeC Int Int Int where 
  method a b = a + b

| a b -> c 구문으로 c 타입은 a, b 타입에 의존한다고 해 놓으면, a :: Int, b :: Int를 만나면 c :: Int로 추론합니다.

물론 이 상태에서

instance SomeC Int Int Double where
  method a b = a - b

을 추가하면

Functional dependencies conflict between instance declarations:
  instance SomeC Int Int Int -- ... 
  instance SomeC Int Int Double -- ...

의존성 충돌이 난다고 에러가 납니다.

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