3

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
Damian Nadales
  • 4,907
  • 1
  • 21
  • 34

1 Answers1

3

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 Projects and Sites), 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.

dfeuer
  • 48,079
  • 5
  • 63
  • 167
Benjamin Hodgson
  • 42,952
  • 15
  • 108
  • 157
  • Thanks for pointing out the design flaw, and for the nice example on the use of GADT's! The problem I see is that you are still deciding at the free monad what a `Site` and a `Project` are. What I was looking for is deferring this decision to the interpreter. On the other hand, giving Scala lack of support for GADT's and considering that the alternative is to write the DSL OO style, I wonder how serious this design flaw is (Java won't offer better type safety I think). – Damian Nadales Nov 09 '16 at 09:45
  • I guess I don't understand your use case. It seems reasonable to me that if your application layer (`CommandF`) knows that there exist two entities called `Site` and `Project` then it probably ought to know what they look like – Benjamin Hodgson Nov 09 '16 at 12:56
  • Well, it is like I stated in my question: if I implement the creation of entities via a rest call, it might be that these entities (`Project` and `Site`) are futures. If I just want to mock the behavior these entities could be random strings instead. That is why I want to defer the selection of the particular entities. If for instance I choose `data Project = Project {uuid :: String}` it seems I'm making too many assumptions (but for all practical purposes this might not be practical...). For instance the choice above means that I cannot pass `Future` or `Try` around ... – Damian Nadales Nov 09 '16 at 13:40
  • Don't let implementation details bleed into the application logic. The whole point of the free monad approach is to write business logic in the free monad, and deal with implementation details in the interpreter. So if you're calling a web service and dealing with futures, _awaiting the future is the interpreter's job_. So if you're running a `Create` instruction, the interpreter sends the request, awaits the response, turns it into a `Project`, and then continues the computation. (That's more or less what I wrote in `interp`.) – Benjamin Hodgson Nov 09 '16 at 15:14
  • But that means that you cannot add asynchronous behavior to the language right? If you maintain the types abstract, the interpreter is still dealing with futures, and the business logic still doesn't need to know about this. I think this should be possible, albeit I'm not sure it is good practice. – Damian Nadales Nov 09 '16 at 16:00
  • 1
    Do you _really_ need concurrency support in your embedded language? If you decide that you do, just add a new constructor to the base functor: `Concurrently :: r -> r -> CommandF r`. The interpreter will be responsible for deciding how to run the two sub-computations concurrently. – Benjamin Hodgson Nov 09 '16 at 18:40
  • 1
    At the end of the day, an embedded language isn't very useful if it can't actually do anything with the objects it's meant to be manipulating. Your application logic has to know about `Site`s and `Project`s because it's the job of the application to manipulate `Site`s and `Project`s! Likewise it's the job of the interpreter to deal with HTTP and concurrency and all that messy real-world stuff. – Benjamin Hodgson Nov 09 '16 at 19:29