Free Monads 中的抽象结果类型
Abstract result types in Free Monads
假设我们想要定义一个简单的 DSL 来定义 UI 交互,我们可以在其中创建对象然后 select 它们:
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]
}
我遇到的问题是我不想指定创建命令的 return 类型(上面的 E
)。我想把这个决定权交给口译员。例如,E
可以是字符串,或者如果我们使用异步 REST 调用创建对象,则 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])
我不明白上面的代码出了什么问题,但一个更重要的问题是,这是否是对自由 monad 中出现的类型进行抽象的正确方法。如果不是,什么是正确的(功能性)方法?
编辑:
在 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
现在你有 test :: Free (TestCommand e) ()
。这意味着实体的类型 e
可以是调用者想要的任何类型,但它在整个计算过程中是固定的。
但那是不对的!在现实世界中,为响应 Create
命令而创建的实体类型取决于命令本身:如果您创建了 Project
,那么 e
应该是 Project
;如果您创建了 Site
,那么 e
应该是 Site
。所以 e
不应该在整个计算过程中固定(因为我可能想创建 Project
s and Site
s),它应该由来电者选择 e
.
这是一个解决方案,其中实体的类型取决于命令的值。
data Site = Site { {- ... -} }
data Project = Project { {- ... -} }
data EntityType e where
SiteTy :: EntityType Site
ProjectTy :: EntityType Project
这里的想法是 EntityType e
上的模式匹配告诉您它的 e
是什么。在 Create
命令中,我们将存在性地打包一个实体 e
以及一些形式为 EntityType 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
指令时,它的工作是 return 一个与包装的 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
我不知道这将如何准确地转化为 Scala,但这就是 Haskell 中的要点。
假设我们想要定义一个简单的 DSL 来定义 UI 交互,我们可以在其中创建对象然后 select 它们:
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]
}
我遇到的问题是我不想指定创建命令的 return 类型(上面的 E
)。我想把这个决定权交给口译员。例如,E
可以是字符串,或者如果我们使用异步 REST 调用创建对象,则 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])
我不明白上面的代码出了什么问题,但一个更重要的问题是,这是否是对自由 monad 中出现的类型进行抽象的正确方法。如果不是,什么是正确的(功能性)方法?
编辑:
在 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
现在你有 test :: Free (TestCommand e) ()
。这意味着实体的类型 e
可以是调用者想要的任何类型,但它在整个计算过程中是固定的。
但那是不对的!在现实世界中,为响应 Create
命令而创建的实体类型取决于命令本身:如果您创建了 Project
,那么 e
应该是 Project
;如果您创建了 Site
,那么 e
应该是 Site
。所以 e
不应该在整个计算过程中固定(因为我可能想创建 Project
s and Site
s),它应该由来电者选择 e
.
这是一个解决方案,其中实体的类型取决于命令的值。
data Site = Site { {- ... -} }
data Project = Project { {- ... -} }
data EntityType e where
SiteTy :: EntityType Site
ProjectTy :: EntityType Project
这里的想法是 EntityType e
上的模式匹配告诉您它的 e
是什么。在 Create
命令中,我们将存在性地打包一个实体 e
以及一些形式为 EntityType 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
指令时,它的工作是 return 一个与包装的 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
我不知道这将如何准确地转化为 Scala,但这就是 Haskell 中的要点。