3. Structure
● The problem
● Embedded DSLs design
● Transition Graph eDSL
● Sample application
● Implementation
● Why you shouldn’t write such code
4. data FlowMethodF a s
= RunUI (Interaction (UIResult s)) (UIResult s -> a)
| ForkUI (Interaction (UIResult s)) a
| CallAPI (Interaction (APIResult s)) (APIResult s -> a)
| Get Store Key (Maybe String -> a)
| Set Store Key String a
| Fork (Flow s) (Control s -> a)
| DoAff (forall eff. AppFlow eff s) (s -> a)
| Await (Control s) (s -> a)
| Delay Milliseconds a
| OneOf (Array (Flow s)) (s -> a)
| HandleError (Flow (ErrorHandler s)) (s -> a)
| CheckPermissions (Array Permission) (PermissionStatus -> a)
| TakePermissions (Array Permission) (Array PermissionResponse -> a)
Presto
Framework
https://github.com/juspay/purescript-presto
The problem
5. Presto: flows
billPayFlow :: MobileNumber -> ScreenFlow
billPayFlow number = do
amount <- UI.askAmount
result <- Remote.payBill number amount
billPayStatus number amount result
billPayStatus :: MobileNumber -> Amount -> BillPayStatus -> ScreenFlow
billPayStatus number amount status = do
action <- runUI' (StatusScreen number amount status)
case action of
SuccessResult -> pure SuccessResult
StatusScreenAbort -> billPayFlow number -- recursion
6. BackT (MFlow)
main = runBackT $ do
lift $ log "Starting Run"
a <- backPoint (randomInt 1 10) -- point to return
lift $ log "A number"
unless (a >= 9) $ BackT (pure GoBack) -- return from here
b <- backPoint (randomInt 1 10) -- point to return
lift $ log "Another number"
unless (b >= 9) $ BackT (pure GoBack) -- return from here
lift $ logShow (a + b)
https://hackage.haskell.org/package/MFlow
7. BackT + flows: how it could possibly look
billPayFlow :: MobileNumber -> BackTScreenFlow
billPayFlow number = do
amount <- backPoint UI.askAmount -- point to return
result <- lift $ Remote.payBill number amount
billPayStatus number amount result
billPayStatus :: MobileNumber -> Amount -> BillPayStatus -> BackTScreenFlow
billPayStatus number amount status = do
action <- lift $ runUI' (StatusScreen number amount status)
case action of
SuccessResult -> pure SuccessResult
StatusScreenAbort -> BackT (pure GoBack) -- go back from here
8. BackT: not a monad, actually
-- https://gist.github.com/ninegua/97833cb4f82451f6c3db
-- Unfortunately, BackT is not a monad. It violates the associative
-- law, as the program shown below, test1 enters an infinite loop,
-- and test2 exits after printing 3 lines.
test1 = runBackT (a >> (b >> c))
test2 = runBackT ((a >> b) >> c)
a = lift (print "step 1") >> breturn ()
b = lift (print "step 2") >> return ()
c = lift (print "step 3") >> fail ""
11. 1. Make it look good
2. Make it compile
3 steps of eDSL design
12. 1. Make it look good
2. Make it compile
3. Make it work
3 steps of eDSL design
13. detailsFlowTrans =
trans detailsFlow
</> [ onCalculate (trans calculateFlow) ]
/> [ onClose (trans submitFlow ) ]
detailsFlow :: Flow Int
detailsFlow = pure 10
calculateFlow :: Int -> Flow Unit
calculateFlow _ = pure unit
submitFlow :: Int -> Flow Unit
submitFlow _ = pure unit
Inventing eDSL syntax, try #1
15. operationsFlowTrans = do
t <- with transactionFlow
backableTrans t onDetails (trans detailsFlow )
backableTrans t onSettings (trans settingsFlow)
forwardOnlyTrans t onHome (trans homeFlow )
Maybe, monads?
17. detailsFlowGraph :: Graph Unit Unit
detailsFlowGraph = graph $
with detailsFlow
<~> on "calculate" (leaf1 calculateFlow)
~> on "enter" (leaf1 submitFlow )
Final version of eDSL
19. node1 :: Graph IO () ()
node1 = graph $
with (print "node1" >> getInput)
<~> on "go 3" node3
~> on "go 2" node2
/> node1
node2 :: Graph IO () ()
node2 = graph $
with (print "node2" >> nop)
-/> node3
node3 :: Graph IO () ()
node3 = graph $
with (print "node3" >> getInput)
</> node1
~> on "exit" (leaf nop)
getInput :: IO (Event, ())
getInput = do
input <- getLine
pure (input, ())
nop :: IO (Event, ())
nop = pure ("", ())
main = runGraph' id (== "back") node1
Sample: Graph + IO
20. node1 :: Graph IO () Int
node1 = graph $
with (pure ("", 100500))
-/> node2
node2 :: Graph IO Int ()
node2 = graph $
with1 (n -> print n >> getInput)
<~> on "go 3" node3
/> node1
node3 :: Graph IO () ()
node3 = graph $
with (print "bye")
-/> (leaf nop)
getInput :: IO (Event, ())
getInput = do
input <- getLine
pure (input, ())
nop :: IO (Event, ())
nop = pure ("", ())
main = runGraph' id (== "back") node1
Typing
21. (~>) By event, forward-only
(<~>) By event, backable (uses a specified event to allow back transition)
(>~<) By event, go forward and return unconditionally
(/>) Default, forward-only
(</>) Default, backable
(-/>) Default, pass through node unconditionally (overrides other transitions)
Transitions
22. (~>) By event, forward-only
(<~>) By event, backable (uses a specified event to allow back transition)
(>~<) By event, go forward and return unconditionally
(/>) Default, forward-only
(</>) Default, backable
(-/>) Default, pass through node unconditionally (overrides other transitions)
Planned:
(?/>) Default, with a given condition
(!/>) Default, random with a given chance
(!~>) By event, random with a given chance
(?~>) By event, with a given condition
(!?~>) By event, with a given condition, random with a given chance
Transitions
24. Game eDSL
data AdventureLF next where
GetUserInput :: (String -> next) -> AdventureLF next
PrintMessage :: String -> next -> AdventureLF next
GetObj :: FromJSON a => String -> (a -> next) -> AdventureLF next
type AdventureL = Free AdventureLF
25. Game eDSL
data AdventureLF next where
GetUserInput :: (String -> next) -> AdventureLF next
PrintMessage :: String -> next -> AdventureLF next
GetObj :: FromJSON a => String -> (a -> next) -> AdventureLF next
type AdventureL = Free AdventureLF
westOfHouse' :: (Bool, Bool) -> AdventureL ()
westOfHouse' (showDescr, showMailbox) = do
mailbox :: Mailbox <- getObject "mailbox"
printMessage "West of House"
when showDescr $ printMessage "This is an open field west of a white house."
when showMailbox $ printMessage $ describeObject mailbox
26. type AGGraph a b = Graph AdventureL a b
game :: AGGraph () ()
game = graph $
with (inputOnly (True, True))
-/> westOfHouse
westOfHouse :: AGGraph (Bool, Bool) ()
westOfHouse = graph $
with1 (x -> westOfHouse' x >> getInput)
~> on "open mailbox" (openMailbox (False, False))
/> leaf nop
openMailbox :: (Bool, Bool) -> AGGraph () ()
openMailbox houseView = graph $
with (evalAction MailboxType "open" "mailbox" >> inputOnly houseView)
-/> westOfHouse
Game transition graph
27. Game output
West of House
This is an open field west of a white house, with a boarded front door.
This is a small mailbox.
A rubber mat saying 'Welcome to Zork!' lies by the door.
> open mailbox
Opening mailbox revealed leaflet
> open mailbox
Mailbox already opened.
29. newtype Graph lang i o
= Graph (Exists (GraphF lang i o))
data GraphF lang i o b
= GraphF1 (i -> lang (Event, b)) (Transitions lang b o ())
data Exists f where
Exists :: f a -> Exists f
Graph: existential data type
31. Graph lang i o
Exists (GraphF lang i o)
GraphF lang i o b
32. Graph lang i o
Exists (GraphF lang i o)
GraphF lang i o b
GraphF1 (i -> lang (Event, b))
(Transitions lang b o ())
33. Graph lang i o
Exists (GraphF lang i o)
GraphF lang i o b
GraphF1 (i -> lang (Event, b))
(Transitions lang b o ())
Transition (Graph lang b o)
Exists (GraphF lang b o)
34. Graph lang i o
Exists (GraphF lang i o)
GraphF lang i o b
GraphF1 (i -> lang (Event, b))
(Transitions lang b o ())
Transition (Graph lang b o)
Exists (GraphF lang b o)
GraphF lang b o b2
35. Graph lang i o
Exists (GraphF lang i o)
GraphF lang i o b
GraphF1 (i -> lang (Event, b))
(Transitions lang b o ())
Transition (Graph lang b o)
Exists (GraphF lang b o)
GraphF lang b o b2
GraphF1 (b -> lang (Event, b2))
(Transitions lang b2 o ())
36. module Data.Exists where
import Unsafe.Coerce (unsafeCoerce)
data Exists f where
Exists :: f a -> Exists f
mkExists :: forall f a. f a -> Exists f
mkExists = unsafeCoerce
runExists :: forall f r. (forall a. f a -> r) -> (Exists f -> r)
runExists = unsafeCoerce
Exists: hacky data type
37. with1
:: (Monad lang)
=> (i -> lang (Event, b))
-> Transitions lang b o ()
-> Graph lang i o
with1 langF1 table = Graph $ mkExists $ GraphF1 langF1 table
case matchTransition event prevGraph of
PassThrough (Graph graphEx) -> runExists (runTransition input) graphEx
Construction and deconstruction of existential type
38. with1
:: (Monad lang)
=> (i -> lang (Event, b))
-> Transitions lang b o ()
-> Graph lang i o
node0 = graph $
with something
<~> on "event 1" node1
~> on "event 2" node2
>~< on "event 3" node3
</> node4
/> node5
Transitions data type
39. type Transitions lang b o u = Free (TransitionF lang b o) u
data TransitionF lang b o next
= Transition Event (TransitionDef (Graph lang b o)) next
| PassThroughTransition (Graph lang b o) next
| PassDefaultForwardOnlyTransition (Graph lang b o) next
| PassDefaultBackableTransition (Graph lang b o) next
data TransitionDef graph
= Backable graph
| ForwardOnly graph
| AutoBack graph
| PassThrough graph
| PassDefaultForwardOnly graph
| PassDefaultBackable graph
| NoTransition
Transitions: Free monad data type
40. interpret
:: Event
-> TransitionF lang b o u
-> Interpreter (Graph lang b o) u
interpret currentEvent (PassThroughTransition g next) = do
transDef' <- get
case transDef' of
PassThrough _ -> pure next
_ -> put (PassThrough g) >> pure next
interpret currentEvent (PassDefaultBackableTransition g next) = do
transDef' <- get
case transDef' of
NoTransition -> put (PassDefaultBackable g) >> pure next
_ -> pure next
Transitions interpreter
(-/>)
(</>)
42. Running graph
runTransition'
:: (Monad m, Monad lang)
=> Runtime lang m
-> ThisBackable
-> AutoReturn
-> i
-> GraphF lang i o b
-> m TransitionResult
runTransition' runtime backable autoReturn i3 g3 = do
let f3 = getLang i3 g3
transitionResult <- runTransition runtime autoReturn f3 g3
case transitionResult of
Done
-> pure Done
AutoFallbackRerun -> pure FallbackRerun
FallbackRerun
-> runTransition' runtime backable thisNotAutoReturn i3 g3
Fallback
->
if isBackable backable
then pure FallbackRerun
else pure Done -- throw "No fallback"
GoForward e2 i3 -> case matchTransition e2 g2 of
NoTransition
-> pure Done
PassThrough g3@(Graph g3Ex) ->
runExists (runTransition' runtime thisNotBackable thisNotAutoReturn i3) g3Ex
PassDefaultForwardOnly g3@(Graph g3Ex) ->
runExists (runTransition' runtime thisNotBackable thisNotAutoReturn i3) g3Ex
PassDefaultBackable g3@(Graph g3Ex) ->
runExists (runTransition' runtime thisBackable
thisNotAutoReturn i3) g3Ex
Backable
g3@(Graph g3Ex) ->
runExists (runTransition' runtime thisBackable
thisNotAutoReturn i3)
ForwardOnly g3@(Graph g3Ex) ->
runExists (runTransition' runtime thisNotBacka
AutoBack
g3@(Graph g3Ex) ->
runExists (runTransit
runLang
:: (Monad m, Monad lang)
=> Runtime lang m
-> lang (LangOutput a)
-> m (LangResult Event a)
runLang (Runtime runLang isBackEvent)
lang = do(e, i) <- runLang lang
if isBackEvent e
then pure GoBackward
else pure $ GoForward e i
data Runtime lang m = Runtime
{ runLang_ :: Runner
lang m
, isBackEvent_ :: Event ->
Bool
}
data LangResult a b =
GoForward a b | GoBackward
data TransitionResult
= Fallback
| AutoFallbackRerun
| FallbackRerun
| Done
44. Why you shouldn’t write such code
● Exists is hack Use GADTs
● Overengineering Kmettism is out there
45. Why you shouldn’t write such code
● Exists is hack Use GADTs
● Overengineering Kmettism is out there
● Overengineering - 2 Free monad can be replaced by Map or list
46. Why you shouldn’t write such code
● Exists is hack Use GADTs
● Overengineering Kmettism is out there
● Overengineering - 2 Free monad can be replaced by Map
● Don't reinvent the wheel Use generic libraries for graphs
47. Why you shouldn’t write such code
● Exists is hack Use GADTs
● Overengineering Kmettism is out there
● Overengineering - 2 Free monad can be replaced by Map
● Don't reinvent the wheel Use generic libraries for graphs
● Don't reinvent the wheel - 2 You probably need FRP
48. Why you shouldn’t write such code
● Exists is hack Use GADTs
● Overengineering Kmettism is out there
● Overengineering - 2 Free monad can be replaced by Map
● Don't reinvent the wheel Use generic libraries for graphs
● Don't reinvent the wheel - 2 You probably need FRP
● Don't reinvent the wheel - 3 Is Graph In Out type just the Arrow In Out type?
49. Why you shouldn’t write such code
● Exists is hack Use GADTs
● Overengineering Kmettism is out there
● Overengineering - 2 Free monad can be replaced by Map
● Don't reinvent the wheel Use generic libraries for graphs
● Don't reinvent the wheel - 2 You probably need FRP
● Don't reinvent the wheel - 3 Is Graph In Out type just the Arrow In Out type?
● Don’t reinvent the wheel - 4 It’s just a State Machine, isn’t it?
50. Everyone knows that debugging is twice as hard as
writing a program in the first place. So if you're as
clever as you can be when you write it, how will you
ever debug it?
Brian Kernighan