GHC 공식 문서 - 6.6.8. Deriving via
Deriving Via or, How to Turn Hand-Written Instances into an Anti-Pattern
아래는 GHC base 라이브러리에 있는 인스턴스입니다.
instance Monoid a => Monoid (IO a) where
mempty = pure mempty
mappend = liftA2 mappend
instance Monoid a => Monoid (ST s a) where
mempty = pure mempty
mappend = liftA2 mappend
완전히 똑같은 모양instance body을 가지고 있습니다. 뭔가 추상화할 수 있을 것처럼 보입니다. 이런 모양은 IO
와 ST s
뿐만 아니라, applicative f
면 다 가능합니다. 다음과 같이 특정 제약을 만족하는 경우에 대한 인스턴스를 만들면 한 번에 해결될 것처럼 보입니다. 아래는 Applicative
functor, Monoid
를 만족하는 모든 f
에 대한 인스턴스입니다.
instance (Applicative f, Monoid a) -- (가) 제약을 만족하는 모든 f a 를 위한
=> Monoid (f a) where
mempty = pure mempty
mappend = liftA2 mappend
이렇게 하면 추상화가 될 것 같은데, 문제가 있습니다. 이러면 Applicative f
, Monoid a
제약이 있는 것이 아니라 모든 (f a)
인스턴스를 덮어 씌우게 됩니다. 이렇게 되는 이유는, 인스턴스 레졸루션resolution은 제약context을 보기 전에 head와 일치하는지 먼저 보기 때문에, f
가 applicative
이든 아니든 간에 모두 매치돼버립니다. 일단, head가 매치되면 더 이상 살펴보지 않습니다. It will nerver backtrack.
예를 들면, 아래 Endo
타입은 Applicative
인스턴스가 아니고, 별도의 Monoid 인스턴스가 있음에도, 위 General 인스턴스에 걸려듭니다.
newtype Endo a = MkEndo (a -> a)
instance Monoid (Endo a) where
mempty = MkEndo id
mappend (MkEndo f) (MkEndo g) = MkEndo (f . g)
또는, Applicative f
이지만, 인스턴스가 위의 모양이 아닐 때도 어찌할 방법이 없습니다.
또는, 안에 있는 a
가 Monoid
가 아니더라도, 리스트 []
의 경우도 f a
이고 free Monoid로 별도의 인스턴스가 있지만, 위 인스턴스에 걸리게 됩니다.
사실, 리스트 Monoid 인스턴스는 Alternative 클래스 제약을 건 인스턴스에 걸려captured듭니다.
instance Alternative f -- (나)
=> Monoid (f a) where
mempty = empty
mappend = (<|>)
인스턴스 레졸루션은 한 번 매칭되면 더 이상 찾질 않습니다. 아마도 이 걸, 매칭 됐던 부분으로 돌아가 다른 선택지가 있는 지 보지 않는다 해서 backtrack이 없다고 표현하는 것 같습니다. 그래서, (가)
와 (나)
를 구별해서 만들 방법도 없습니다.
※ resolution: a breaking or reducing into parts; process of breaking up, dissolution
process of reduction things into simpler form
Online Etymology Dictionary - resolution
영단어 뜻은 이렇긴 한데, 적절한 인스턴스를 찾는 작업을 resolution이라 합니다.
어떤 펑크터가 안에 Num
클래스 인스턴스를 가지고 있다면, 이 펑크터 타입의 Num
인스턴스는 다음처럼 수작업으로 만들 수 있습니다.
instance (Applicative f, Num a) => Num (f a) where
+) = liftA2 (+)
(-) = liftA2 (-)
(= liftA2 (∗)
(∗) negate = liftA negate
abs = liftA abs
signum = liftA signum
fromInteger = pure . fromInteger
지루하게, liftA
, liftA2
등을 이용해 리프팅 작업을 해야 합니다.
제약을 이용해서 Generic하게 인스턴스를 정의하면, 필요 이상으로 덮어 씌우던 문제를, adapter로 newtype
을 정의하고, 이 newtype
으로 인스턴스 head를 감싸 해결할 수 있습니다.
인스턴스 헤드를 newtype
으로 한 번 래핑해서, 해당 타입만을 위한 인스턴스를 General하게 만들면 다음처럼 됩니다.
newtype Ap f a = Ap (f a)
instance (Applicative f, Monoid a)
=> Monoid (Ap f a) where
mempty = Ap (pure mempty)
mappend (Ap f) (Ap g) = Ap (liftA2 mappend f g)
instance (Applicative f, Semigroup a)
=> Semigroup (Ap f a) where
Ap f <> Ap g = Ap (liftA2 (<>) f g)
이제 새로운 규칙을 GHC에게 알려주기 위해 다음처럼 새로운 구문 via
를 덧 붙입니다.
data Maybe a = Nothing | Just a
deriving Monoid via (Ap Maybe a)
Maybe
의 Monoid
인스턴스를 자동으로 deriving
하는데, Ap Maybe a
의 Monoid
인스턴스를 보고via 베껴 오라는 말입니다.
기능만 설명하면, “이미 존재하는 인스턴스를 참조, 복사해서 새 인스턴스를 만들 수 있다”고, 용도까지 같이 언급하면,
derivingVia
는 General하게 인스턴스를 정의하면 제약에 관계없이 모두 다 덮어 씌워버리던 문제를,
1. Adapter로 쓸 newtype
을 만들고, 이 타입의 인스턴스를 general한 모양으로 만들고,
2. via
문법으로 이 Adapter의 인스턴스를 복사해서 만들어라
라고 GHC에게 알려주는 확장입니다.
이렇게 작동하는 이유는 간단합니다.
Ap Maybe a
와 Maybe a
와 같은 internal representation을 갖고 있습니다. 그래서, 둘 중 하나에 정의된 인스턴스는 쉽게 다른 쪽에도 적용되게 만들 수 있습니다. 문서에서는 more precise language에선, 아예 둘이 representationally 동등이라 하는데, 하스켈이 여기에 속하는 것 같은데, 정확한 언급은 아직 못찾았습니다.