2016-11-08 119 views
3

假設我們要定義一個簡單的DSL定義UI交互,我們可以創建對象,然後選擇它們:摘要結果類型

object TestCommand { 

    sealed trait EntityType 
    case object Project extends EntityType 
    case object Site extends EntityType 

    sealed trait TestCommand[A, E] 
    case class Create[A, E](entityType: EntityType, withEntity: E => A) extends TestCommand[A, E] 
    case class Select[A, E](entity: E, next: A) extends TestCommand[A, E] 

} 

我的問題是,我不希望指定創建命令的返回類型應該是什麼(上面的E)。我想把這個決定交給翻譯。例如,如果我們使用異步REST調用創建對象,則E可以是字符串,也可以是Future

如果我嘗試定義使用liftF通常的方式DSL如下圖所示:

object TestDSL { 

    def create[E](entityType: EntityType): Free[TestCommand[?, E], E] = 
    Free.liftF(Create(entityType, identity: E => E): TestCommand[E, E]) 

    def select[E](entity: E): Free[TestCommand[?, E], Unit] = 
    Free.liftF(Select[Unit, E](entity,())) 

} 

我收到以下錯誤:

Error:(10, 10) no type parameters for method liftF: (value: S[A])scalaz.Free[S,A] exist so that it can be applied to arguments (dsl.TestCommand.TestCommand[E,E]) 
--- because --- 
argument expression's type is not compatible with formal parameter type; 
found : dsl.TestCommand.TestCommand[E,E] 
required: ?S[?A] 
    Free.liftF(Create(entityType, identity: E => E): TestCommand[E, E]) 

我不明白什麼是在腳麻上面的代碼,但更重要的問題是這是否是抽象出現在免費單子中的類型的正確方法。如果不是,什麼是正確的(功能)方法?

編輯

在Haskell上述方法效果不會有問題:

{-# LANGUAGE DeriveFunctor #-} 
-- | 

module TestDSL where 

import   Control.Monad.Free 

data EntityType = Project | Site 

data TestCommand e a = Create EntityType (e -> a) | Select e a 
    deriving Functor 

-- | The DSL 
create :: EntityType -> Free (TestCommand e) e 
create et = liftF $ Create et id 

select :: e -> Free (TestCommand e)() 
select e = liftF $ Select e() 


-- | A sample program: 
test :: Free (TestCommand e)() 
test = do 
    p <- create Project 
    select p 
    _ <- create Site 
    return() 

-- | A trivial interpreter. 
interpTestCommand :: TestCommand String a -> IO a 
interpTestCommand (Create Project withEntity) = do 
    putStrLn $ "Creating a project" 
    return (withEntity "Project X") 
interpTestCommand (Create Site withEntity) = do 
    putStrLn $ "Creating a site" 
    return (withEntity "Site 51") 
interpTestCommand (Select e next) = do 
    putStrLn $ "Selecting " ++ e 
    return next 

-- | Running the interpreter 
runTest :: IO() 
runTest = foldFree interpTestCommand test 

運行測試將導致以下的輸出:

λ> runTest 
Creating a project 
Selecting Project X 
Creating a site 

回答

3

現在你有test :: Free (TestCommand e)()。這意味着實體e的類型可以是調用者想要的任何東西,但它在整個計算過程中都是固定的。

但是,這是不對的!在現實世界中,這是響應Create命令創建實體的類型取決於命令本身:如果您創建一個Project然後eProject;如果您創建了Site那麼e應該是Site。所以e不應該在整個計算過程中被修復(因爲我可能想要創建Project的s Site s),並且它不應該由呼叫者選擇e

下面是一個解決方案,其中實體的類型取決於命令的值。

data Site = Site { {- ... -} } 
data Project = Project { {- ... -} } 

data EntityType e where 
    SiteTy :: EntityType Site 
    ProjectTy :: EntityType Project 

這裏的想法是對的EntityType e該模式匹配告訴你它的e是什麼。在Create命令,我們會與存在性一點的形式EntityType e你可以在模式匹配,以瞭解哪些東西e是的GADT證據一起打包的實體e

data CommandF r where 
    Create :: EntityType e -> (e -> r) -> CommandF r 
    Select :: EntityType e -> e -> r -> CommandF r 

instance Functor CommandF where 
    fmap f (Create t next) = Create t (f . next) 
    fmap f (Select t e next) = Select t e (f next) 

type Command = Free CommandF 

create :: EntityType e -> Command e 
create t = Free (Create t Pure) 

select :: EntityType e -> e -> Command() 
select t e = Free (Select t e (Pure())) 

myComputation :: Command() 
myComputation = do 
    p <- create ProjectTy -- p :: Project 
    select ProjectTy p 
    s <- create SiteTy -- s :: Site 
    return() 

當解釋器達到Create指令,它的工作是返回匹配的包裹EntityType類型的實體。它必須檢查EntityType以便知道e是什麼並且行爲適當。

-- assuming createSite :: IO Site and createProject :: IO Project 

interp :: CommandF a -> IO a 
interp (Create SiteTy next) = do 
    site <- createSite 
    putStrLn "created a site" 
    return (next site) 
interp (Create ProjectTy next) = do 
    project <- createProject 
    putStrLn "created a project" 
    return (next project) 
-- plus clauses for Select 

我不知道如何將其轉化爲斯卡拉準確,但是這是它在Haskell的要點。

+0

感謝您指出的設計缺陷,並在使用GADT的好例子!我看到的問題是,你仍然在免費的monad中決定什麼是「Site」和「Project」。我正在尋找的是將這個決定推遲給翻譯。另一方面,給Scala缺乏對GADT的支持,並且考慮替代方法是編寫DSL OO風格,我不知道這種設計缺陷有多嚴重(Java不會提供我認爲更好的類型安全性)。 –

+0

我想我不明白你的用例。我認爲如果你的應用層('CommandF')知道存在兩個名爲'Site'和'Project'的實體,那麼它可能應該知道它們看起來像什麼 –

+0

嗯,就像我在我的問題中所說的那樣:如果我通過休息調用來實現實體的創建,可能是這些實體('Project'和'Site')是未來的。如果我只是想嘲笑這些行爲,那麼這些實體可能是隨機字符串。這就是爲什麼我想推遲選擇特定的實體。例如,如果我選擇'data Project = Project {uuid :: String}',似乎我做了太多假設(但對於所有實際目的而言,這可能並不實際......)。例如,上面的選擇意味着我無法繞過'Future'或'Try' ... –