Как переосмыслить термин DSL в окончательном подходе без тегов?


Добрый день всем.

Наше приложение использует типизированный DSL для описания определенной бизнес-логики. DSL поставляется с несколькими переводчиками без тегов.

Вот как объявляются его условия:

{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE EmptyDataDecls #-}

class Ctl impl where
  -- Lift constants.
  cnst :: Show t => t -> impl t
  -- Obtain the state.
  state :: impl (Maybe Int)

  -- Test for equality.
  eq :: impl Int -> impl Int -> impl Bool
  -- If-then-else.
  ite :: impl Bool -> impl t -> impl t -> impl t

  -- Processing outcomes.
  retry :: impl Outcome
  finish :: impl Outcome

  -- Require a value.
  req :: impl (Maybe t) -> impl t

Бизнес-логика затем описывается с использованием фрагментов кода в этом DSL:

proc1 :: Ctl impl => impl Outcome
proc1 = ite (req state `eq` cnst 5) finish retry
Эти высокоуровневые определения используются с интерпретаторами. У меня есть интерпретатор текста для получения читаемого текстового описания того, как бизнес-процессы определено:
newtype TextE t = TextE { evalText :: String }

instance Ctl TextE where
  cnst v = TextE $ show v
  state = TextE "My current state"
  eq v1 v2 = TextE $ concat [evalText v1, " equals ", evalText v2]
  ite cond t e =
    TextE $
    concat ["If ", evalText cond, ", then ", evalText t, ", else ", evalText e]
  retry = TextE "Retry processing"
  finish = TextE "Finish"
  req v = TextE $ concat ["(", evalText v, ")*"]

Интерпретация DSL с помощью TextE дает строку:

λ> (evalText proc1) :: String
"If (My current state)* equals 5, then Finish, else Retry processing"

Такое описание используется в качестве справочного материала для пользователей / аналитиков.

Я также могу оценить термин DSL для метаязыка (Haskell) с помощью другой интерпретатор, который является тем, как приложение на самом деле следует правила:

newtype HaskellE t = HaskellE { evalHaskell :: HaskellType t }

-- Interface between types of DSL and Haskell.
type family HaskellType t

instance Ctl HaskellE where
  cnst v = HaskellE v
  state = HaskellE dummyState
  eq v1 v2 = HaskellE $ evalHaskell v1 == evalHaskell v2
  ite cond t e =
    HaskellE $
    if (evalHaskell cond)
    then (evalHaskell t)
    else (evalHaskell e)
  retry = HaskellE $ print "Retrying..."
  finish = HaskellE $ print "Done!"
  req term@(HaskellE v) =
    case v of
      Just v' -> HaskellE v'
      Nothing ->
        HaskellE (error $
                  "Could not obtain required value from ") -- ++ evalText term)

-- Dummy implementations so that this post may be evaluated
dummyState = Just 5
type Outcome = IO ()
type instance HaskellType t = t

Этот интерпретатор производит запускаемый код Хаскелла:

λ> (evalHaskell proc1) :: IO ()
"Done!"

Теперь к моей проблеме: я хотел бы использовать текстовый переводчик от HaskellE переводчик. Для например, я хочу определить отказывающую ветвь req способом, включающим текстовое представление вложенного термина (обычно получается с помощью evalText term) в сообщении об ошибке. То соответствующий код закомментирован в реализации req для HaskellE выше. Если комментарий отменен, код выглядит следующим образом:

    HaskellE (error $
              "Could not obtain required value from " ++ evalText term)
Однако система типов не позволяет мне сделать это:
tagless.lhs:90:71: Couldn't match expected type ‘TextE t0’ …
                with actual type ‘HaskellE (Maybe t)’
    Relevant bindings include
      v :: HaskellType (Maybe t)
        (bound at /home/dzhus/projects/hs-archive/tagless.lhs:85:22)
      term :: HaskellE (Maybe t)
        (bound at /home/dzhus/projects/hs-archive/tagless.lhs:85:7)
      req :: HaskellE (Maybe t) -> HaskellE t
        (bound at /home/dzhus/projects/hs-archive/tagless.lhs:85:3)
    In the first argument of ‘evalText’, namely ‘term’
    In the second argument of ‘(++)’, namely ‘evalText term’
Compilation failed.

Сообщение в основном говорит о том, что переводчик Хаскелл уже был выбран, когда переменная типа impl был создан экземпляр, и я нельзя использовать текстовый переводчик изнутри Хаскелля.

Чего я не могу понять, так это: как я могу переосмыслить термин из Хаскеллы в текст?

Если я здесь совершенно не прав, как мне изменить свой подход, чтобы я мог на самом деле используйте текстовый переводчик от Haskell one без повторное внедрение его в Хаскелле? Похоже, это вполне осуществимо. с первоначальным подходом вместо окончательного.

Я снял свой фактический DSL и упростил типы и интерпретаторы ради краткости.

1 7

1 ответ:

Можно отслеживать как значение, так и информацию о выражении, создавшем значение. Если вы сделаете это, вы потеряете некоторые преимущества производительности вашего окончательного представления без тегов.

data Traced t a = Traced {evalTraced :: HaskellType a, trace :: t a}

Мы ожидаем использовать его с трассировкой TextE, поэтому для удобства определим следующее

evalTextTraced :: Traced TextE a -> HaskellType a
evalTextTraced = evalTraced

Этот класс позволяет нам восстанавливать сообщения об ошибках из trace

class Show1 f where
    show1 :: f a -> String

instance Show1 TextE where
    show1 = evalText

instance (Show1 t) => Show1 (Traced t) where
    show1 = show1 . trace

Этот интерпретатор сохраняет след любого другого интерпретатора Ctl t, который мы можем восстановить ошибку сообщения от при интерпретации a Traced t.

instance (Show1 t, Ctl t) => Ctl (Traced t) where
    cnst v = Traced v (cnst v)
    state = Traced dummyState state
    eq (Traced v1 t1) (Traced v2 t2) = Traced (v1 == v2) (eq t1 t2)
    ite (Traced vc tc) (Traced vt tt) (Traced ve te) = Traced (if vc then vt else ve) (ite tc tt te)
    retry = Traced (print "Retrying...") retry
    finish = Traced (print "Done!") finish
    req (Traced v t) = 
        case v of 
            Just v' -> Traced v' rt
            Nothing -> Traced (error ("Could not obtain required value from " ++ show1 rt)) rt
        where rt = req t

Ваш пример ведет себя так, как и ожидалось

print . evalText . trace $ proc1
evalTextTraced proc1

"If (My current state)* equals 5, then Finish, else Retry processing"
"Done!"

Мы все еще можем evalText пример с неудачным требованием, но при попытке его запуска появляется информативное сообщение об ошибке

proc2 :: Ctl impl => impl Outcome
proc2 = ite (req (cnst Nothing) `eq` cnst 5) finish retry

print . evalText . trace $ proc2
evalTextTraced proc2

"If (Nothing)* equals 5, then Finish, else Retry processing"
finaltagless.hs: Could not obtain required value from (Nothing)*