아래처럼 methodA, methodB를 만들다 보니, 나중에 약간 다른 Config타입을 받을 일이 생긴다고 가정해 봅시다.
-- ModuleA.hs
module ModuleA ( methodA) where
data Config = Config { pCommon :: Int, pSpecA :: Int }
methodA :: Config -> Int
= pCommon conf
methodA conf
-- ModuleB.hs
module ModuleB ( methodB ) where
data Config = Config { pCommon :: Int, pSpecB :: Int }
methodB :: Config -> Int
= pCommon conf + 1
methodB conf
-- ModuleC.hs
module ModuleC () where
import ModuleA
import ModuleB
data Config = Config { pCommon :: Int, pSpecC :: Int }
= Config 10 20
conf
run :: Config -> Int
= methodA + methodB run
컴파일 하면 여러 Config가 섞여 있어 에러가 납니다.
:14:7: error:
ModuleC.hsCouldn't match type ‘Config’
• ModuleA.Config’
with ‘NB: ‘ModuleA.Config’ is defined at ModuleA.hs:6:1-54
Config’ is defined at ModuleC.hs:9:1-54
‘Expected type: Config -> Int
Actual type: ModuleA.Config -> Int
In the expression: methodA + methodB
• In an equation for ‘run’: run = methodA + methodB
|
14 | run = methodA + methodB
|
나중에 다른 Config
타입에서 쓸 일이 있는 메소드는 methodA :: Config -> Int
처럼 구체 타입을 지정하면 안됩니다. 다른 타입에서 쓸 일이 있는 함수는 GHC의 타입 추론에 맡기는 것 말고 방법이 없습니다. methodA :: c -> Int
이런식으로 열어 놔야 합니다.
공통으로 가지고 있는 속성을 클래스로 옮기면 다음처럼 쓸 수 있습니다.
-- ModuleA.hs
module ModuleA ( methodA) where
import ConfigCommon
--data Config = Config { pSpecA :: Int }
--instance ConfigCommon Config where
-- pCommon cfg = 1
methodA :: ConfigCommon c => c -> Int
= pCommon conf -- 어떤 pCommon이 쓰일지 여기선 아직 알 수 없다.
methodA conf
-- ModuleB.hs
module ModuleB ( methodB ) where
--data Config = Config { pSpecB :: Int }
--instance ConfigCommon Config where
-- pCommon cfg = 2
methodB :: ConfigCommon c => c -> Int
= pCommon conf + 1
methodB conf
-- ConfigCommon.hs
-- Config의 구체적인 모습은 모르지만, 일단 pCommon 속성을 쓴다는 걸 알려주기 위해 필요합니다.
{-#LANGUAGE AllowAmbiguousTypes #-}
module ConfigCommon ( ConfigCommon(..)) where
class ConfigCommon c where
pCommon :: c -> Int
-- ModuleC.hs
module ModuleC () where
import ModuleA
import ModuleB
import ConfigCommon
data Config = Config { pSpecC :: Int }
instance ConfigCommon Config where
= 10
pCommon cfg
= Config 20
conf
run :: Config -> Int
= methodA conf + methodB conf + pSpecC conf
run conf
-- run :: ConfigCommon c => c -> Int
-- run conf = methodA conf + methodB conf + pSpecC conf
-- pSpecC를 쓰는 함수라면 이렇게 쓰지 못합니다.
-- ConfigCommon c => 제약만으론 pSpecC가 뭔지 알려 줄 도리가 없습니다.
-- pSpecC와 methodA, methodB 가 무엇인지 모두 알려 줄 방법은
-- 여기서 쓰이는 Config 타입 하나로 타입을 닫아야 합니다.
> run conf
41
원했던 방식으로 동작은 하지만, 매번 Config
를 설정할 때 ConfigCommon
의 인스턴스로 만들어야 합니다. 불편해서 다른 패턴이 없을까 고민 중인데, 타입을 열어두는 것 말고 딱히 찾아지거나, 떠오르는 아이디어가 없습니다.
문제를 정리하면, 각 메소드가 정의된 파일에서 Config
란 데이터를 참조하는데, 각 모듈에 있는 Config
들이 조금씩 다릅니다. 물론 Config
마다 각각인 속성은 건드리지 않고, 공통된 속성만 건드리는데, 이 메소드들을 다른 컨텍스트 안에서 모아서 쓰려면 어떻게 해야 할까요?
레코드 필드가 특별한 요소가 아니라, 그저 함수들을 모아만 놓은 것으로 본다면, 그 중 다른 Config
들과 공통된 함수(여기선 필드field)들은 클래스 메소드로 보내버리고, 공통되지 않는 필드들만 남겨 놓은 상태라 볼 수 있습니다. 레코드 문법이 하나의 객체안에 속성들을 끈끈하게 모아 놓은게 아니라, 그저 여기 저기 흩어져 있는 함수들의 이름을 모아, 대표 이름을 붙여 놓은 것입니다. 그래서 레코드안의 필드들이 다른 레코드의 필드와 이름이 겹칠 수 없습니다. 필드들도 그냥 보통의 함수와 다를 게 없습니다. - 딱 이해하기 좋은 표현은 아닌 것 같긴 합니다.