Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is a bindable functor a useful abstraction for more type safe DSLs?

Motivation

I'm currently working on a little hobby project to try and implement something like TaskJuggler in Haskell, mostly as an experiment to play with writing domain specific languages.

My current goal is to have a small DSL for building up the description of a Project, along with it's associated Tasks. There is no hierarchy yet, though that'll be my next extension. Currently, I have the following data types:

data Project = Project { projectName :: Text
                       , projectStart :: Day
                       , projectEnd :: Day
                       , projectMaxHoursPerDay :: Int
                       , projectTasks :: [Task]
                       }
  deriving (Eq, Show)

data Task = Task { taskName :: Text }
  deriving (Eq, Show)

Nothing too crazy there, I'm sure you will agree.

Now I want to create a DSL to build up projects/tasks. I can use Writer [Task] monad to build up tasks, but this won't scale well. We might be able to do the following now:

project "LambdaBook" startDate endDate $ do
  task "Web site"
  task "Marketing"

Where project :: Text -> Date -> Date -> Writer [Task] a, which runs the Writer to get a list of tasks, and choses a default value such as 8 for projectMaxHoursPerDay.

But I will later want to be able to do something like:

project "LambdaBook" $ do
  maxHoursPerDay 4
  task "Web site"
  task "Marketing"

So I'm using maxHoursPerDay to specify a (future) property about a Project. I can no longer use a Writer for this, because [Task] isn't able to capture everything I need.

I see two possibilities for solving this problem:

Separate "optional" properties into their own monoid

I could split Project into:

data Project = Project { projectName, projectStart, projectEnd, projectProperties }
data ProjectProperties = ProjectProperties { projectMaxHoursPerDay :: Maybe Int
                                           , projectTasks :: [Task]
                                           }

Now I can have an instance Monoid ProjectProperties. When I run Writer ProjectProperties I can do all the defaulting I need to build a Project. I suppose there's no reason that Project needs to embed ProjectProperties - it could even have the same definition as above.

Use the bindable functor Semigroup m => Writer m

While Project isn't a Monoid, it can certainly be made into a Semigroup. Name/start/end are First, maxHoursPerDay is Last, and projectTasks is [Task]. We can't have a Writer monad over a Semigroup, but we can have a Writer bindable functor.

The Actual Question

With the first solution - a dedicated 'properties' Monoid - we can use the full power of a monad, at a choice of costs. I could duplicate the overridable properties in Project and ProjectProperties, where the latter wraps each property in an appropriate monoid. Or I could just write the monoid once and embed it inside the Project - though I give up type safety (maxHoursPerDay must be Just when I actually produce the project plan!).

A bindable functor removes both the code duplication and retains type safety, but at the immediate cost of giving up syntax sugar, and the potentially longer term cost of being a pain to work with (due to lack of return/pure).

I have examples of both approaches at http://hpaste.org/82024 (for bindable functors), and http://hpaste.org/82025 (for the monad approach). These examples go a little beyond what's in this SO post (which was big enough already), and has Resource along with Task. Hopefully this will indicate why I need to go as far Bind (or Monad) in the DSL.

I'm excited to have even found an applicable use for bindable functors, so I'm happy to hear any thoughts or experience you might have.

like image 352
ocharles Avatar asked Feb 09 '13 13:02

ocharles


2 Answers

data Project maxHours = Project {tasks :: [Task], maxHourLimit :: maxHours}

defProject = Project [] ()

setMaxHours :: Project () -> Project Double
setMaxHours = ...

addTask :: Project a -> Project a

type CompleteProject = Project Double...

runProject :: CompleteProject -> ...

storeProject :: CompleteProject -> ...

You need function composition now, instead of actions in a writer, but this pattern lets you start with a partially populated record, and set those things that need to be set once and only once with plenty of type safety. It even lets you impose constraints on the relationship between various set and unset values in the final result.

like image 129
sclv Avatar answered Nov 01 '22 16:11

sclv


An interesting solution that was proposed on Google+ was to use a normal Writer monad, but using the Endo Project monoid. Along with lens, this yields a very nice DSL:

data Project = Project { _projectName :: String
                       , _projectStart :: Day
                       , _projectEnd :: Day
                       , _projectTasks :: [Task]
                       }
  deriving (Eq, Show)

makeLenses ''Project

Along with the operation

task :: String -> ProjectBuilder Task
task name = t <$ mapProject (projectTasks <>~ [t])
  where t = Task name []

Which can be used with the original DSL. This is probably the best solution for what I want (though maybe using a monad is just too much of an abuse of syntax anyway).

like image 21
ocharles Avatar answered Nov 01 '22 15:11

ocharles