purescript-run

repository·master·Indexed 41 days ago

https://github.com/natefaubion/purescript-run

An implementation of the extensible-effects pattern for PureScript, providing a way to build and compose modular, effectful programs using algebraic effects. It allows developers to define DSLs using data types and interpret them into other effects, Aff, or Effect, with specific support for stack-safety via *Rec variations and continuation passing via runCont.

Tokens
1.9K
Snippets
7
Records
9
Agent score
1%

What's inside purescript-run

  1. What is purescript-run?

    master

    purescript-run is an implementation of extensible, algebraic effects for PureScript. It allows you to write composable programs using normal PureScript data types and then provide interpreters to run them. This approach avoids the need for large encompassing data types or explicit lifting through transformer stacks.

    Before using Run, it is recommended to familiarize yourself with purescript-variant.

  2. How to define a DSL using Run

    master

    To define a Domain Specific Language (DSL) with Run, you define a data type for your effects, a Variant type for the effect row, and a Proxy to identify the effect. You then use Run.lift to lift your constructors into the Run monad.

    Example pattern:

    1. Define the effect functor (e.g., TalkF).
    2. Define the row type (e.g., type TALK r = (talk :: TalkF | r)).
    3. Create a Proxy for the effect (e.g., _talk = Proxy :: Proxy "talk").
    4. Define lifting functions using Run.lift _talk.
    -- 1. Effect Functor
    data TalkF a = Speak String a | Listen (String -> a)
    derive instance functorTalkF :: Functor TalkF
    
    -- 2. Row Type
    type TALK r = (talk :: TalkF | r)
    
    -- 3. Proxy
    _talk = Proxy :: Proxy "talk"
    
    -- 4. Lifting functions
    speak :: forall r. String -> Run (TALK + r) Unit
    speak str = Run.lift _talk (Speak str unit)
    
    listen :: forall r. Run (TALK + r) String
    listen = Run.lift _talk (Listen identity)
  3. Ensure stack-safety in PureScript

    master

    Stack safety is critical when targeting JavaScript.

    • When interpreting Run into Run or Aff: You can safely use interpret, run, and runAccum because Run and Aff are designed to be stack-safe.
    • When interpreting into Effect: Effect is not stack-safe. You must use the *Rec variations: interpretRec, runRec, and runAccumRec to guarantee stack safety.
    • When using Continuation Passing (*Cont): It is not possible to guarantee stack safety.
    • Purely interpreting: When using runPure or runAccumPure, you must use the Step data type (Loop and Done) to preserve stack safety.
  4. Write an exhaustive interpreter with interpret

    master

    To run a program where you handle every possible effect in the Run stack, use the interpret combinator. You must provide a handler for every effect using exhaustive pattern matching (e.g., case_).

    Note: interpret is stack-safe when interpreting a Run effect into another Run effect or into Aff. However, if interpreting into Effect, use the *Rec variations to guarantee stack safety.

    -- Assuming 'program' is of type Run (TALK + r) Unit
    main = program # interpret (case_ # on _talk handleTalk)
    
    where
      handleTalk :: TalkF ~> Effect
      handleTalk = case _ of
        Speak str next -> do
          Console.log str
          pure next
        Listen reply -> do
          pure (reply "I am Groot")
  5. Interpret a single effect using send

    master

    If you want to interpret only a subset of effects in a Run stack (leaving the others untouched), use interpret combined with on and send. The send function handles the cases that do not match your specific handler, passing them through to the rest of the stack.

    This allows you to transform a program from Run (EFFECT + TALK + r) to Run (EFFECT + r) by interpreting TALK into EFFECT.

    -- handleTalk interprets TalkF into Run (EFFECT + r)
    handleTalk :: forall r. TalkF ~> Run (EFFECT + r)
    handleTalk = case _ of
      Speak str next -> do
        liftEffect $ Console.log str
        pure next
      Listen reply -> do
        pure (reply "I am Groot")
    
    -- runTalk uses 'send' to pass unmatched effects through
    runTalk :: forall r. Run (EFFECT + TALK + r) ~> Run (EFFECT + r)
    runTalk = interpret (on _talk handleTalk send)
  6. Run an interpreter with state using runAccumPure

    master

    To interpret an effect purely while maintaining an internal state (an accumulator), use runAccumPure.

    Because this bypasses the standard monadic loop to run purely, you must use the Step data type (Loop and Done) from Control.Monad.Rec.Class to maintain stack safety during the interpretation process.

    -- handleDinner uses the Step pattern (Loop/Done) for stack safety
    handleDinner :: forall a. Tally -> DinnerF a -> Tuple Tally a
    handleDinner tally = case _ of
      Eat _ reply
        | tally.stock > 0 -> 
            let tally' = { stock: tally.stock - 1, bill: tally.bill + 1 }
            in Tuple tally' (reply true)
        | otherwise -> Tuple tally (reply false)
      CheckPlease reply -> Tuple tally (reply tally.bill)
    
    -- runDinnerPure uses runAccumPure to eliminate the DINNER effect
    runDinnerPure :: forall r a. Tally -> Run (DINNER + r) a -> Run r (Tuple Bill a)
    runDinnerPure = runAccumPure
      (\tally -> on _dinner (Loop <<< handleDinner tally) Done)
      (\tally a -> Tuple tally.bill a)
  7. Write interpreters using continuation passing (runCont)

    master

    If your effect functors embed an Effect callback (continuation) instead of a next step a, you can use runCont or runAccumCont. This is useful when your base runtime is Effect and you want to use standard callbacks (like setTimeout).

    -- Example using runCont with setTimeout
    program2 :: Effect Unit
    program2 = program # runCont go done
      where
        go = match
          { log: \(Log str cb) -> Console.log str *> cb
          , sleep: \(Sleep ms cb) -> void $ setTimeout ms cb
          }
        done _ = do
          Console.log "Done!"