SciFlow-0.7.0: src/Control/Workflow/Language.hs
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE OverloadedStrings #-}
module Control.Workflow.Language
( -- * Defining workflows
node
, nodePar
, (~>)
, path
, namespace
, Workflow(..)
, Builder
-- * Lens for Attributes
, doc
, nCore
, memory
, queue
, Node(..)
, NodeAttributes
, THExp(..)
) where
import Control.Arrow
import qualified Data.Text as T
import Control.Monad.State.Lazy (State)
import qualified Data.HashMap.Strict as M
import Control.Monad.State.Lazy (modify, execState)
import Data.Maybe (isNothing)
import Language.Haskell.TH (ExpQ, Name, varE)
import Control.Workflow.Types (Resource(..))
-- | A computation node.
data Node = Node
{ _node_function :: ExpQ -- ^ a function with type: a -> ReaderT env IO b
, _node_job_resource :: Maybe Resource -- ^ Computational resource config
, _node_parallel :: Bool -- ^ Should the job be run in parallel
, _node_doc :: T.Text -- ^ Documentation
}
data NodeAttributes = NodeAttributes
{ _doc :: T.Text -- ^ documentation
, _nCore :: Int
, _memory :: Int
, _queue :: Maybe String }
-- | Node description.
doc :: Functor f => (T.Text -> f T.Text) -> NodeAttributes -> f NodeAttributes
doc x y = fmap (\newX -> y { _doc = newX }) (x (_doc y))
{-# INLINE doc #-}
-- | Number of cores.
nCore :: Functor f => (Int -> f Int) -> NodeAttributes -> f NodeAttributes
nCore x y = fmap (\newX -> y { _nCore = newX }) (x (_nCore y))
{-# INLINE nCore #-}
-- | Total memory.
memory :: Functor f => (Int -> f Int) -> NodeAttributes -> f NodeAttributes
memory x y = fmap (\newX -> y { _memory = newX }) (x (_memory y))
{-# INLINE memory #-}
-- | Job queue.
queue :: Functor f
=> (Maybe String -> f (Maybe String))
-> NodeAttributes -> f NodeAttributes
queue x y = fmap (\newX -> y { _queue = newX }) (x (_queue y))
{-# INLINE queue #-}
mkNode :: THExp q
=> q -- ^ Template Haskell expression representing
-- functions with type @a -> IO b@.
-> State NodeAttributes ()
-> Node
mkNode fun attrSetter = Node (mkExp fun) res False $ _doc attr
where
res | isNothing core && isNothing mem && isNothing (_queue attr) = Nothing
| otherwise = Just $ Resource core mem $ _queue attr
core = if _nCore attr > 1 then Just $ _nCore attr else Nothing
mem = if _memory attr > 0 then Just $ _memory attr else Nothing
attr = execState attrSetter $ NodeAttributes
{ _doc = ""
, _nCore = 1
, _memory = -1
, _queue = Nothing }
{-# INLINE mkNode #-}
-- | Workflow declaration, containing a map of nodes and their parental processes.
data Workflow = Workflow
{ _nodes :: M.HashMap T.Text Node
, _parents :: M.HashMap T.Text [T.Text] }
instance Semigroup Workflow where
x <> y = Workflow (_nodes x <> _nodes y) (_parents x <> _parents y)
type Builder = State Workflow
-- | Define a step.
node :: THExp q
=> T.Text -- ^ Node id
-> q -- ^ Template Haskell expression representing
-- functions with type @a -> ReaderT env IO b@.
-> State NodeAttributes () -- ^ Option setter
-> Builder ()
node i f attrSetter = modify $ \wf ->
wf{ _nodes = M.insertWith undefined i nd $ _nodes wf }
where
nd = mkNode f attrSetter
{-# INLINE node #-}
-- | Define a step that will be executed in parallel, i.e.,
-- @a -> m b@ becomes @[a] -> m [b]@.
nodePar :: THExp q
=> T.Text -- ^ Node id
-> q -- ^ Template Haskell expression representing
-- functions with type @a -> ReaderT env IO b@.
-> State NodeAttributes ()
-> Builder ()
nodePar i f attrSetter = modify $ \wf ->
wf{ _nodes = M.insertWith undefined i nd{_node_parallel=True} $ _nodes wf }
where
nd = mkNode f attrSetter
{-# INLINE nodePar #-}
linkFromTo :: [T.Text] -> T.Text -> Builder ()
linkFromTo ps to = modify $ \wf ->
wf{ _parents = M.insertWith undefined to ps $ _parents wf }
{-# INLINE linkFromTo #-}
-- | Connect nodes.
-- Example:
--
-- > node "step1" [| \() -> return 1 |] $ return ()
-- > node "step2" [| \() -> return 2 |] $ return ()
-- > node "step3" [| \(x, y) -> x * y |] $ return ()
-- > ["step1", "step2"] ~> "step3"
(~>) :: [T.Text] -> T.Text -> Builder ()
(~>) = linkFromTo
{-# INLINE (~>) #-}
-- | @'path' [a, b, c]@ is equivalent to @[a] ~> b >> [b] ~> c@
path :: [T.Text] -> Builder ()
path ns = sequence_ $ zipWith linkFromTo (map return $ init ns) $ tail ns
{-# INLINE path #-}
-- | Add a prefix to IDs of nodes for a given builder, i.e.,
-- @id@ becomes @prefix_id@.
namespace :: T.Text -> Builder () -> Builder ()
namespace prefix builder = modify (st <>)
where
st = execState (builder >> addPrefix) $ Workflow M.empty M.empty
addPrefix = modify $ \Workflow{..} ->
let nodes = M.fromList $ map (first add) $ M.toList _nodes
parents = M.fromList $ map (add *** map add) $ M.toList _parents
in Workflow nodes parents
add x = prefix <> "_" <> x
{-# INLINE namespace #-}
class THExp q where
mkExp :: q -> ExpQ
instance THExp Name where
mkExp = varE
instance THExp ExpQ where
mkExp = id