turion / essence-of-live-coding

Universal Live Coding & Functional Reactive Programming Framework
https://www.manuelbaerenz.de/#computerscience
65 stars 6 forks source link

Higher-order state support #117

Open walseb opened 1 year ago

walseb commented 1 year ago

Hello!

Essence-of-live-coding doesn't support higher order state, as stated in: https://github.com/turion/essence-of-live-coding/blob/279f42a4d04bbf42fe59a8dafec466d002a4ebea/essence-of-live-coding/src/LiveCoding/Coalgebra.lhs#L88

This means that higher order arrow constructs such as pSwitch aren't possible to implement with state remaining intact between reloads.

Unfortunately, for most games, one needs dynamically many entities, which I believe requires use of higher order arrows.

Using essence-of-live-coding to develop games would be amazing. Currently, my engine architecture makes use of essence-of-live-coding so that GPU initialization and windows can be kept between reloads, but the state simulation of my game has to rely on a library supporting higher order state, such as Dunai. At the moment, I record all inputs and then replay them in sequence to get a pseudo live coding environment, but the process of replaying past state quickly starts to take too long.

Is there any way around this limitation at all?

Thank you! Sebastian

miguel-negrao commented 1 year ago

Do you need to completely change the logic, or just change the number of elements ? If it is the latter you can try to use resampleListPar .

{- | Create as many cells as the input list is long and execute them in parallel
 (in the sense that each one has a separate state). At each tick the list with
 the different states grows or shrinks depending on the size of the input list.

 Similar to Yampa's [parC](https://hackage.haskell.org/package/Yampa-0.13.3/docs/FRP-Yampa-Switches.html#v:parC).
-}
resampleListPar :: Monad m => Cell m a b -> Cell m [a] [b]
miguel-negrao commented 1 year ago

See also #89 .

walseb commented 1 year ago

Thank you for the reply!

That's interesting, I remember having issues with using parC for simulating a dynamic amount of entities.

With resampleListPar, would it be possible to have every cell have a different position, for example?

Also, I see that the CellState is set to [] in resampleListPar, does that mean its entities wouldn't be carried over on hot-reloads? I would think not.

walseb commented 1 year ago

I think what I need in order to create, destroy, move, etc actors in my game are hot-reloadable lists of Cells. I believe that means I have to be able to store a Cell inside CellState, so that their CellStep can be restored on hot-load.

I suspect that the only way to do that is to make CellStep derive Data, which I think means CellStep couldn't be a normal Haskell function, as it can't derive Data I believe, but rather a serializable DSL.

Thoughts?

turion commented 1 year ago

This means that higher order arrow constructs such as pSwitch aren't possible to implement with state remaining intact between reloads.

Maybe pSwitch is not possible in all generality, but for certain col, it should work in theory. (Convincing the type checker and writing out all the generic code is more work still.) Namely, if col is a "polynomial" functor, and has no recursion. Basically, any combination of sum and product types, like Maybe, Data a => Either a, Pair, bounded lists, ..., one could maybe call such functors "finite Traversable" or so.

The idea is roughly that when you have col (Cell m a b), then you have col many cells, each of which has a different state type. Only if we can enumerate all these types such that the type checker can be satisfied that the combined type will again have a Data instance, it can work. So for example, for a Pair functor, something like this can work (I didn't typecheck it):

data Pair a = Pair a a

runPair :: Pair (Cell m a b) -> Cell m a (Pair b)
runPair (Pair (Cell state1 step1) (Cell state2 step2)) = Cell
  { cellState = Pair state1 state2
  , cellStep = \(Pair s1 s2) a -> Pair <$> step1 s1 <*> step2 s2
  }

Similarly, I'm sure you can write a pSwitch function specialised to Pair, it'll just be a bit lengthier than runPair. Now you can write a pSwitch for Either e and Maybe. Given a pSwitch for two functors f and g, you can write a pSwitch for Sum f g and Product f g. You might guess where this goes. We can define a type class PSwitch:

class Functor col => PSwitch col where
  pSwitch :: (forall sf . (a -> col sf -> col (b, sf)))
        -> col (Cell m b c)
        -> Cell m (a, col c) (Event d)
        -> (col (Cell m b c) -> d -> Cell m a (col c))
        -> Cell m a (col c)

And we can write down generic instances for GHC Generics, or SoP. Then we can derive instances for many functors.

Unfortunately, for most games, one needs dynamically many entities, which I believe requires use of higher order arrows.

True, but you might be able to get away with a "finite traversable". If you know at compile time that you only need 1000 entities, you could use Template Haskell to create a 1000-tuple and derive PSwitch for it. I haven't tried this though.

turion commented 1 year ago

I suspect that the only way to do that is to make CellStep derive Data, which I think means CellStep couldn't be a normal Haskell function, as it can't derive Data I believe, but rather a serializable DSL.

Yes, that is also a possibility. You will not quite be able to write down pSwitch in all its generality, but if you can defunctionalise all your cellSteps, then you can do a lot more. So you could go down this route:

data DefunCell m step a b = forall s . (Data s, Data (step m a b)) => DefunCell
  { dcState :: s
  , dcStep :: step m (s, a) (s, b)
  }

class Fun step where
  run :: step m a b -> a -> m b

runDefun :: Fun step => DefunCell m step a b -> Cell m a b

You would need to build up a lot of infrastructure like instance (Monad m, Arrow step) => Arrow (DefunCell m step) first.

turion commented 1 year ago

Yet another idea might apply to your special case of many similar entities. If you know the state types of them, and they are all the same, then the situation is in principle much easier. I haven't thought about this in detail, but if you make the state type explicit, you might be able to define pSwitch in general:

data ExplicitCell s m a b = ExplicitCell
  { ecState :: s
  , ecStep :: s -> a -> m (b, s)
  }

implicit :: Data s => ExplicitCell s m a b -> Cell m a b

For your ExplicitCell, you could be able to define pSwitch, and then later cast into Cell. But the downside is that ExplicitCell is not Arrow, not even Applicative (only Functor), and it can frequently be a hassle to write down the state type completely.

miguel-negrao commented 1 year ago

With resampleListPar, would it be possible to have every cell have a different position, for example?

I believe so, the state is separate for each element in the array.

Also, I see that the CellState is set to [] in resampleListPar, does that mean its entities wouldn't be carried over on hot-reloads? I would think not.

I belive the state will be carried over with hot-reload. [] is only used when initializing, on reloads the previous state is saved and reloaded.

miguel-negrao commented 1 year ago

I was able to make a GUI with dynamic number of buttons, where the number of buttons could be increased or decreased by another set of buttons, using forever, throw and resampleListPar . The code is not particularly beautiful but it works... This is indeed a limitation of eolc.

walseb commented 1 year ago

Thank you so much for the detailed reply, I appreciate it!

I will have a look soon.

miguel-negrao commented 1 year ago

So looking at my approach again, it was essentialy the following.

I used as switching function this one:

-- | foreverE uses ReaderT e which in most cases requires lifting cells into the reader monad.
--  If the e value doesn't need to be sent deeply down into other cells, then it is easier to
--  get the e value as an input instead of via ask.
foreverE' ::
  (Monad m, Data e) =>
  e ->
  Cell (ExceptT e m) (e, a) b ->
  Cell m a b
foreverE' e cell = foreverE e (readerToInput cell)
  where
    readerToInput cell = proc a -> do
      e <- constM ask -< ()
      liftCell cell -< (e, a)

It is just a convenience to not have to do call ask explicitely. The value e that is passed to the function is the inicial state which should be an array, in my case it was [Int] and it represents the internal state of each line of buttons. Then cell will receive this value together with a ((e,a)). Then I feed the e value (which is a list) to resampleListPar and it creates as many widgets as needed with the internal state given by the values in e. Then I respond to events which can change the number of elements in e by calling throwEC or dthrowEC inside cell. This will make foreverE' "switch", i.e., destroy the previous objects and create new ones using the new state, in particular I might add one more element to the list, or remove one element from the list, which will cause one more line of buttons to appear or one existing line to disappear. I place the definition of these functions below. The key element is that the switching in this case is binary, i.e. on each tick decide to switch or not to switch, but there is no decision being made as to what cell to run next, it is always running the same cell next, it's just that this cell will have access to the last value of the exception which is thrown. This is at least enough to build dynamic GUIs where the number of event emiters can change, and therefore the shape of the FRP graph changes, but it doesn't change in an arbitrary fashion, it changes in a constrained way, in this case just the number of elements in the graph changes, but they are always of the same type.

-- | Throw an exception on an event.
throwEC :: (Monad m, Traversable f) => Cell (ExceptT e m) (f e) ()
throwEC = traverse' throwC >>> constantly ()

-- | Throw an exception on an event, but only on the next tick.
-- Usefull when used with 'foreverE'.
dthrowEC :: (Monad m, Data e, Data (f e), Traversable f, Monoid (f e)) => Cell (ExceptT e m) (f e) ()
dthrowEC = delay mempty >>> traverse' throwC >>> constantly ()

The functions above require the stuff that is not in eolc yet, but in this PR which adds a Traversing instance. The functions above are also in this branch which has a bunch of useful FRP combinators.

Also, I can confirm the hot-reloading works with this approach. I can change the code, reload and keep the current state of the rows of buttons.

miguel-negrao commented 1 year ago

Actually, after studying my code again, and looking at it with fresh eyes, I refactored it and realized that I don't event need any exceptions at all. So I don't need forever or throw, all I need is feedback and resampleListPar. Then constructing the signal which represents the list of internal counter values, is done with classical FRP approach, where there is an initial state, which is changed via accumHold and a bunch of event sources which are all merged with lMerge. The feedback is needed to let the delete button of each row influence how many rows there are via feeding into resampleListPar.

demoDyn3 :: Cell (HandlingStateT IO) () ()
demoDyn3 = loopPrintExceptions $
  withMainLoopC $ feedback
        (([],[]) :: ([Maybe ()],[Maybe ()]))
        ( proc ((), (prevIncEvents, previousRemoveEvents)) -> do
    -- widgets
    buttonAdd <- newC Button -< [#label := "Add item"]
    labelNum <- newC Label -< []
    -- logic
    addItemE <- onEC #clicked -< buttonAdd
    entryModelLabel <- newC Label -< [#label := "This simulates a change of the model from outside the GUI"]
    entryModel <- newC Entry -< [#text := tshow ([1, 2, 3]::[Int])]
    activateE <- onEC #activate -< entryModel
    textB <- getC #text -< entryModel
    simulateChangeModel <- arr (\x -> x >>= (readMaybe . T.unpack :: Text -> Maybe [Int])) -< textB <$ activateE
    -- resampleListPar creates as many 'singleLine' cells as needed according to the list 'counters'
    let
      deleteAt :: [Int] -> [Int] -> [Int]
      deleteAt (idx : idxs) xs = case splitAt idx xs of
          (lft, _ : rgt) -> deleteAt idxs (lft ++ rgt)
          _ -> error "splitAt demoDyn1"
      deleteAt [] xs = xs
      getList :: [a] -> Maybe [a]
      getList []     = Nothing
      getList xs     = Just xs
    counters <- accumHold [1,2,3] -<
      ((\counters -> counters ++ [0])  <$ addItemE) `lMerge`
      (deleteAt <$> getList (catMaybes (zipWith (<$) [(0 :: Int) ..] previousRemoveEvents))) `lMerge`
      ((\counters -> zipWith (\n e -> if isJust e then n + 1 else n) counters prevIncEvents) <$ getList (catMaybes prevIncEvents)) `lMerge`
      (const <$> simulateChangeModel)
    (boxes, incEvents, removeEvents) <- arr unzip3 <<< resampleListPar singleLine -< counters
    labelList <- newC Label -< [#label := ("Current model value: " <> tshow counters)]
    -- layout
    box <-
      boxCreatePackC
        -<
          ( [#orientation := OrientationVertical, #margin := 10],
            (,False,False,0) <$> [toW buttonAdd, toW labelNum, toW entryModelLabel, toW entryModel] ++ (toW <$> boxes) ++ [toW labelList]
          )
    newWindowC -< ([#title := "Dynamic GUI"], box)
    returnA -< ((), (incEvents, removeEvents))
    )
  where
    singleLine :: Cell (ExceptT EOLCGtkError (HandlingStateT IO)) Int (Box, Maybe (), Maybe ())
    singleLine = proc num -> do
      buttonRemove <- newC Button -< [#label := "Remove"]
      buttonCount <- newC Button -< [#label := "Increment "]
      countE <- onEC #clicked -< buttonCount
      removeE <- onEC #clicked -< buttonRemove
      entry <- newC Entry -< [#halign := AlignFill, #expand := True, #text := tshow num]
      box <-
        boxCreatePackC
          -<
            ( [#orientation := OrientationHorizontal],
              (,False,False,0) <$> [toW buttonRemove, toW buttonCount, toW entry]
            )
      returnA -< (box, countE, removeE)

imagem

walseb commented 1 year ago

Wow thank you! That looks very interesting! I have yet to read everything here, but do you have a runnable project using that snippet that's publicly available?

miguel-negrao commented 1 year ago

Wow thank you! That looks very interesting! I have yet to read everything here, but do you have a runnable project using that snippet that's publicly available?

The essence-of-live-coding-gi-gtk library is not public yet, I'm still cleaning it up, but hope to make it available soon.