[英]Abstract result types in Free Monads
Suppose we want to define a simple DSL for defining UI interactions where we can create objects and then select them: 假设我们要定义一个简单的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]
}
The problem I have is that I wouldn't want to specify what the return type of the creation command should be ( E
above). 我遇到的问题是我不想指定创建命令的返回类型应该是什么(上面的E
)。 I would like to let this decision up to the interpreter. 我想让这个决定由口译员决定。 For instance, E
could be a string, or a Future
if we are creating objects with asynchronous REST calls. 例如,如果我们使用异步REST调用创建对象,则E
可以是字符串,也可以是Future
。
If I try to define the DSL in the usual way using liftF
as shown below: 如果我尝试使用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, ()))
}
I get the following error: 我收到以下错误:
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])
I cannot understand what is going wrong in the code above, but a more important question is whether this is the right way to abstract over the types appearing in free monads. 我无法理解上面代码中出了什么问题,但更重要的一个问题是,这是否是抽象出现在免费monad中的类型的正确方法。 If not, what is the right (functional) approach? 如果没有,什么是正确的(功能)方法?
EDIT : 编辑 :
In Haskell the approach described above works without a problem: 在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
Running the test will result in the following output: 运行测试将产生以下输出:
λ> runTest
Creating a project
Selecting Project X
Creating a site
Right now you have test :: Free (TestCommand e) ()
. 现在你有test :: Free (TestCommand e) ()
。 This means that the type of the entity e
can be anything the caller wants, but it's fixed throughout the computation. 这意味着实体e
的类型可以是调用者想要的任何类型,但它在整个计算过程中是固定的。
But that's not right! 但那不对! In the real world, the type of the entity that's created in response to a Create
command depends on the command itself: if you created a Project
then e
should be Project
; 在现实世界中,为响应Create
命令而创建的实体的类型取决于命令本身:如果您创建了一个Project
那么e
应该是Project
; if you created a Site
then e
should be Site
. 如果你创建了一个Site
那么e
应该是Site
。 So e
shouldn't be fixed over the whole computation (because I might want to create Project
s and Site
s), and it shouldn't be up to the caller to pick an e
. 所以e
不应该被固定在整个计算(因为我可能需要创建Project
S 和 Site
S),并且它不应该是由调用者挑选的e
。
Here's a solution in which the type of the entity depends on the value of the command. 这是一种解决方案,其中实体的类型取决于命令的值。
data Site = Site { {- ... -} }
data Project = Project { {- ... -} }
data EntityType e where
SiteTy :: EntityType Site
ProjectTy :: EntityType Project
The idea here is that pattern-matching on an EntityType e
tells you what its e
is. 这里的想法是EntityType e
上的模式匹配告诉你它的e
是什么。 In the Create
command we'll existentially package up an entity e
along with a bit of GADT evidence of the form EntityType e
which you can pattern-match on to learn what e
was. 在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 ()
When the interpreter reaches a Create
instruction, its job is to return an entity of the type that matches the wrapped EntityType
. 当解释器到达Create
指令时,其作用是返回与包装的EntityType
匹配的类型的实体。 It has to inspect the EntityType
in order to know what e
is and behave appropriately. 它必须检查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
I don't know how this would translate into Scala exactly, but that's the gist of it in Haskell. 我不知道这会如何完全转化为Scala,但这是Haskell中的主旨。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.