Free Monads中的抽象结果类型 [英] Abstract result types in Free Monads
问题描述
假设我们想定义一个简单的DSL来定义UI交互,我们可以创建对象然后选择它们:
object TestCommand {
密封trait EntityType
案例对象Project extends EntityType
案例对象Site extends EntityType
密封特质TestCommand [A,E]
case class Create [A,E](entityType:EntityType,withEntity:E => A)扩展TestCommand [A,E]
case class选择[A,E](entity:E,next:A)扩展TestCommand [A,E]
}
我遇到的问题是我不想指定创建命令的返回类型应该是什么(上面的 E
)。我想把这个决定交给翻译。例如,如果我们使用异步REST调用创建对象, E
可以是字符串,或者 Future
p>
如果我尝试按照常规方式使用 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(选择[Unit,E](entity,()))
}
出现以下错误:
错误:(10,10)方法liftF没有类型参数:(value:S [A])scalaz.Free [S,A]存在以便它可以应用于参数(dsl.TestCommand.TestCommand [E,E])
---因为---
参数表达式的类型与形式参数类型不兼容;
found:dsl.TestCommand.TestCommand [E,E]
required:?S [?A]
Free.liftF(Create(entityType,identity:E => E):TestCommand [E,E])
我不明白上面的代码出了什么问题,但更多一个重要的问题是这是否是抽象出现在免费单子中的类型的正确方法。如果没有,那么什么是正确的(功能性)方法?
编辑:
在Haskell中,上面描述的方法没有问题:
$ $ p $ {code> { - #LANGUAGE DeriveFunctor# - }
- |
module TestDSL where
import Control.Monad.Free
data EntityType = Project | Site
data TestCommand e a =创建EntityType(e - > a)|选择导出Functor
- |的e
DSL
create :: EntityType - > Free(TestCommand e)e
create et = liftF $ Create et id
select :: e - >免费(TestCommand e)()
选择e = liftF $选择e()
- |示例程序:
test :: Free(TestCommand e)()
test = do
p< - 创建项目
选择p
_< - create Site
return()
- |一个简单的翻译。
interpTestCommand :: TestCommand字符串a - > IO a
interpTestCommand(使用实体创建项目)= do
putStrLn $创建项目
return(withEntityProject X)
interpTestCommand(使用实例创建网站)= do
putStrLn $创建站点
返回(withEntitySite 51)
interpTestCommand(选择下一个)= do
putStrLn $选择++ e
返回下一个
- |运行解释器
runTest :: IO()
runTest = foldFree interpTestCommand test
运行测试将产生以下输出:
λ> runTest
创建一个项目
选择Project X
创建一个站点
现在你有 test :: Free(TestCommand e)()
。这意味着实体 e
的类型可以是调用者想要的任何东西,但它在整个计算过程中是固定的。
但那不对!在现实世界中,为响应 Create
命令而创建的实体类型取决于命令本身:如果创建了 Project code>然后
e
应该是 Project
;如果您创建了 Site
,那么 e
应该是 Site
。所以 e
不应该在整个计算中修复(因为我可能想要创建 Project
s 和 Site
s),它不应该由调用者选择 e
。
下面是一个解决方案,其中实体的类型取决于命令的值。
data Site = Site {{ - ... - }}
data Project = Project {{ - ... - }}
data EntityType e其中
SiteTy :: EntityType Site
ProjectTy :: EntityType项目
这里的想法是,在 EntityType e
上的模式匹配告诉你它的 e
是什么。在 Create
命令中,我们将存在一个实体 e
以及一些GADT证据的形式 EntityType e
,您可以进行模式匹配以了解 e
是。
data CommandF r where
Create :: EntityType e - > (e - > r) - > CommandF r
Select :: EntityType e - > e - > r - > CommandF r
实例Functor CommandF其中
fmap f(Create t next)= Create t(f。next)
fmap f(Select te next)= Select te(f next )
type Command = Free CommandF
create :: EntityType e - >命令e
create t = Free(Create t Pure)
select :: EntityType e - > e - > Command()
select te = Free(Select te(Pure()))
myComputation :: Command()
myComputation = do
p< - 创建ProjectTy - p :: Project
选择ProjectTy p
s< - create SiteTy - s :: Site
return()
当解释器到达 Create
指令时,它的工作是返回一个匹配被包装的EntityType
。它必须检查 EntityType
以便知道 e
是什么并且行为恰当。
- 假设createSite :: IO Site和createProject :: IO Project
interp :: CommandF a - > IO a
interp(Create SiteTy next)= do
site< - createSite
putStrLn创建了一个站点
return(next site)
interp(Create ProjectTy下一个)= do
项目< - createProject
putStrLn创建了一个项目
返回(下一个项目)
- 选择
的加号子句
我不知道这会如何转化为Scala,但这是Haskell中的要点。
Suppose we want to define a simple DSL for defining UI interactions where we can create objects and then select them:
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). 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.
If I try to define the DSL in the usual way using liftF
as shown below:
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. If not, what is the right (functional) approach?
EDIT:
In Haskell the approach described above works without a problem:
{-# 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) ()
. This means that the type of the entity e
can be anything the caller wants, but it's fixed throughout the computation.
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
; if you created a Site
then e
should be 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
.
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. 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.
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
. It has to inspect the EntityType
in order to know what e
is and behave appropriately.
-- 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.
这篇关于Free Monads中的抽象结果类型的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!