Open walseb opened 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]
See also #89 .
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.
I think what I need in order to create, destroy, move, etc actors in my game are hot-reloadable lists of Cell
s. 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?
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.
I suspect that the only way to do that is to make
CellStep
derive Data, which I think meansCellStep
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 cellStep
s, 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.
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.
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[]
inresampleListPar
, 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.
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.
Thank you so much for the detailed reply, I appreciate it!
I will have a look soon.
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.
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)
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?
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.
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