@jaror We’re aware that inlining and changing some definitions in dunai should give speedups (`dunai`: `morphGS` is probably inefficient · Issue #370 · ivanperez-keera/dunai · GitHub), but what’s really needed is a realistic benchmarking suite to make the right optimizations. That said, I agree that there is a fundamental optimization barrier for the dunai approach because of the recursion.
Another way to get around it is to use the initial encoding instead the final encoding: https://github.com/lexi-lambda/incremental/blob/master/src/Incremental/Fast.hs This way there is no recursion, and GHC can optimize the resulting functions well. I’ve experimented with it a bit and found dramatic speedups. It has the advantage that it is not tied to the IO monad. (But the reactimate
framework may be even faster than that, I don’t know.)
@simon I can understand that using IO is good for performance in your framework, and I’m really intrigued. Well done! But I also find the use of IO problematic:
- It ties you to GHC, e.g. you couldn’t compile a reactive program using Clash.
- It hides IO under the hood. When I use a
Signal a b
, how do I know whether someone usedarrIO launchMissiles
in its definition? - It’s hard to reason about determinism. When I write test cases with my signal functions, will they always be executed in the same way? Right now it seems you’re not using any threads in the implementation, but maybe you will some day for parallelisation, and I won’t notice as a user?
These concerns (primarily the last two) are part of what motivated us to develop dunai, and make the monad visible in the type signature. Would that have worked as well for you? E.g.:
newtype Signal m a b = Signal (m (a -> m b))
My impression is that you mainly need m
to be IO
in two places:
- To have effectful signals, i.e.
arrM :: (a -> m b) -> Signal m a b
. There, the framework would actually profit from generality. - To hide state. I haven’t thought about this in detail, but maybe it’s possible that other monads (
ST
?StateT
?UnliftIO
?MonadIO
?MonadConc
?) provide the API you need to create, read and write stateful variables.
In particular, you could allow more than only IO! For example, you could have your signal in RIO
and share resources/handlers between your components. You could have a transformer stack on top of IO
, or some effect system.