Tagless Final style

Posted on July 7, 2020
어떤 인터프리터를 붙이냐에 따라 다르지

기본 아이디어는 “똑같은 문장을 써놓고 때에 따라 다르게 번역하고 싶다” 입니다. 하스켈에서 이렇게 할 수 있는 문법이 바로 클래스와 인스턴스입니다. 구체 타입을 지정하지 않고, 클래스 제약을 걸어두고 메소드를 쓰면, 실제 실행되는 메소드 구현체는 나중에 GHC가 추론 단서들을 보고 고르게 됩니다. 예를 들면 getData라 메소드를 선언해서 사용하고, 이 메소드가 때로는 database에서 값을 가져오고, 때로는 file에서 값을 가져오게 하고 싶을 때 같은 경우를 말합니다.

eDSL - embedded Domain Specific Language 1을 모두 클래스의 메소드로 정의해 놓고, 나중에 인스턴스를 바꿔서 적용 가능하게 해 놓는 걸 tagless final style 이라 부릅니다. Free 모나드를 같은 목적으로 쓰는 경우가 많은데, Free 모나드에 비해 장점은 런타임에 인터프리팅되는게 아니라, GHC가 컴파일 타임에 코드 조합에 따라 인스턴스를 골라서 컴파일 하니 퍼포먼스측면에서 장점이 있습니다.
mtl 스타일에 익숙하다면, mtl과 아이디어는 같다고 보면 됩니다. 실용 코드에서는 application monad2 안에서 mtl에 얹혀 tagless final을 사용합니다.

class Monad m => EDSL m where
  command1 :: ... -> m ()
  command2 :: ... -> m ()
  command3 :: ... -> m ()

embeddedProgram :: EDSL m => m ()
embeddedProgram = do
  command1
  commadn2
  command1
  command2
  command3
  ...
data Config = Config { c1::Integer, c2::String }

-- 첫 번째 인터프리터
newtype AppM a = AppM { runAppM :: ReaderT Config IO a }
  deriving (Functor, Applicative, Monad, MonadIO, MonadReader Config)

instance EDSL AppM where
  command1 = do c1 <- asks c1; c2 <- asks c2; ...
  command2 = do c1 <- asks c1; c2 <- asks c2; ...
  command3 = ...

-- 두 번째 인터프리터
newtype TestAppM a = AppM { runTestAppM :: ReaderT Config IO a }
  deriving (Functor, Applicative, Monad, MonadIO, MonadReader Config)

instance EDSL TestAppM where
  command1 = ... 
  command2 = ...
  command3 = ...

main = do
  runReaderT (runAppM embeddedProgram) (Config 100 "some")
  runReaderT (runTestM embeddedProgram) (Config 100 "some")

embeddedProgram에는 구현체는 없이 작업명만 나열해 놓은 상태입니다. 구체 동작은 EDSL 인스턴스 정의해서 구현됩니다.

기존 라이브러리가 tagless final 스타일이라면 다음 처럼 기존 라이브러리 수정 없이 eDSL을 확장 할수 있습니다.

class Monad m => ExtEDSL m where
  command4 :: ... -> m ()
  command5 :: ... -> m ()

-- 원래 EDSL 코드를 건드리지 않고, 명령어를 추가했습니다.
embeddedProgram2 :: (EDSL m, ExtEDSL m) => m () 
embeddedProgram2 = do
  command1
  commadn2
  command3
  ...
  command4
  command5

-- 인터프리터에도 추가된 명령어 해석기를 추가합니다.
instance ExtEDSL AppM where
  command4 = ...
  command5 = ...

instance ExtEDSL TestAppM where
  command4 = ...
  command5 = ...

DSL의 해석을 할 때, 명령어들의 타입이 의미가 있는 경우, 명령어와 타입을 튜플로 싸서 해석기에 넘기는 스타일을 썼는데, 이 때 튜플 안에 들어있는 타입을 tag라고 불렀다는 것 같습니다. 그래서 여기서는 tagless라 부르는 것 같은데, tagless final encoding 용어의 어원을 알려면 히스토리를 좀 더 봐야 될 것 같습니다. 어원에 대해 명확히 아는 분은 댓글 부탁드립니다.

좀 더 심오한 의미가 있는 걸로 보이는데, 너무 단순하게 이해한게 아닌가 싶습니다. 핵심은 위 아이디어가 맞는 것 같은데, 응용 부분을 더 봐야할 것 같습니다.

참고
https://serokell.io/blog/tagless-final
https://jproyo.github.io/posts/2019-03-17-tagless-final-haskell.html


  1. eDSL (embedded Domain Specific Language)
    요리 레시피처럼 할 일들을 순서대로 주욱 기록해 두는 것과 비슷합니다. 실제 실행 코드로 조합하는게 아닌, 작업에 붙여 놓은 작업명을 DSL이라 보면 됩니다. 어디선가는 작업명에 맞는 실행 코드를 가져와서 실행하게 될텐데 Free 모나드는 런타임에, tagless final은 컴파일 타임에 인터프리팅이 일어납니다.

    크게 보면 프로그래밍 언어 자체가 DSL이긴 하지만, 보통 언어내embedded에서 사용자가 정의한 태그들로, 나중에 인터프리팅 단계를 거쳐 실행 코드로 바뀌는 것들을 뜻하는 좁은 의미로 쓰입니다.

    하스켈에선, 어떤 사람들은 do 표현이 가능한 정도로만 DSL이라 부르기도 하고, 어떤 사람들은 메소드들로 AST(Abstraction Syntax Tree)를 정의해야만 DSL이라 부르기도 합니다.↩︎

  2. Application Monad
    특정 모나드를 가리키는게 아니라 Tagless final 스타일에서, 한 모나드(예시 코드의 AppM, TestAppM 모나드)안에서 DSL을 조합해서 쓰게되는데, 이 때의 모나드를 application monad라 부릅니다.↩︎

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