Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

essence-of-live-coding-frp: Continuous time FRP with eolc #102

Open
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

miguel-negrao
Copy link
Contributor

This PR contains two packages:

  • essence-of-live-coding-frp
  • essence-of-live-coding-frp-test

essence-of-live-coding-frp is a port of Bearriver for essence-of-live-coding.

Notable changes from Bearriver:

  • functions which don't use time are of type Cell m a b, cells which do are of type Cell (ClockInfoT m) a b.
  • Event type has been substituted by Maybe.
  • Not all Yampa combinators have been ported, notably many switch variants are missing.
  • takeWhile and dropWhile implementation was changed, I believe the original ones are buggy. Added tests that check their behaviour.
  • Some utility functions which are not present in Bearriver were added. I think of essence-of-live-coding-frp as a package which should have almost everything you need to do FRP programming. Also added a module to automatically uncurry functions and lift them to arrows. Currently the functions added are:
holdM :: (Monad m, Data a) => Cell m (m a, Maybe a) a`
arrEM ::
  Monad m =>
  (a -> m b) ->
  Cell m (Maybe a) (Maybe b)
catchC :: (Monad m, Data e, Finite e) => Cell (ExceptT e m) a b -> (e -> Cell m a b) -> Cell m a b
throwEC :: Monad m => Cell (ExceptT e m) (Maybe e) ()
dthrowEC :: (Monad m, Data e) => Cell (ExceptT e m) (Maybe e) ()
traverseHold :: (Monad m, Data c) => Cell m a c -> c -> Cell m (Maybe a) c
  • I changed integral to use the trapezoidal rule instead of the rectangle rule, I think it is more accurate with no drawback. Integral is tested using sine and polynomials.
  • I added a function to run ClockInfoT cells which gets the current time using functions from the clock package: runClockInfoC :: MonadIO m => Cell (ClockInfoT m) a b -> Cell m a b.
  • I did not touch any code in essence-of-live-coding, but I think that if this is merged then the time related stuff in essence-of-live-coding should be removed. Possibly the function edge might not be needed in eolc, and there is a nameclash with the function once.

Additionally I added a package essence-of-live-coding-frp-test which implements a Linear Temporal Logic approach to testing FRP, porting the code from the yampa-test package. Some of the tests are ported from that package, but many were created by myself. This is a separate package so that other packages can test their own code using this package. This is due to my assumption that you cannot import library code from a test suite of another package, and it also follows the fact that yampa-test is a separate package from Yampa.

  • This package also has a demo app with a ball bouncing.
  • The LTL implementation is different from yampa-tests. Yampa and MSFs are not exactly compatible, they differ in one important aspect. Yampa streams have a value at time 0, which is separate, while MSFs do not. The way evalT was implemented in yampa-test and the way some functions are defined in Bearriver break the test suite. I reimplemented evalT directly in terms of functions from eolc. I hope/believe that my re-implementation is correct. I tested with some tautologies from LTL and they hold. Obviously a subtle bug might have been missed.
  • I find LTL very useful for testing cells. You can easily test some quite complex conditions.
  • tests will fail if passed an empty list. Cells must be evaluated using a non-empty list.
  • Technically many cells don't need to be tested with a stream with time values (e.g. all the initialization cells). In order to simplify code and not have additional code, I just test them with time values anyway.

Additional info:

  • All code in these packages is formatted using ormolu.
  • Tests are passing in CI.
  • Since almost all code is ported from Bearriver and yampa-test, the copyright is attributed to the original authors of the code (Ivan and you). I hope I did that in the correct way.
  • This PR needs the Traversing instance for Cell therefore it goes on top of my PR which introduces that instance.

Add instances for Cell of Profunctor, Choice and Strong via WrappedArrow
Add instance for Cell of Traversing
liveCell,
liveMain,
returnA,
(>>>),
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hah, I didn't know this is valid syntax

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I generate these lists by accepting hlint suggestions is vscode and then formatted with ormolu. I read somewhere that, if possible, explicit import lists are better because if a package in a new version adds a new function that clashes with another existing function that will not cause a problem.

-- Functions which are lifted into arrows have to be in completely curried form.
-- This module provides utility functions to automatically uncurry functions and
-- lift them to 'Arrow's using the tuple package's 'Curry' typeclass.
module LiveCoding.FRP.Curry where
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be part of either https://github.com/miguel-negrao/arrow-utils or directly of essence-of-live-coding.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It has some functions which depend on Cell. Should I put the functions that depend only on Arrow in arrow-utils, and the ones that depend on Cell directly in essence-of-live-coding ?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I see. Yes, that sounds like the right way to go forward.

deriving via (WrappedArrow (Cell m)) instance Monad m => Strong (Cell m)
deriving via (WrappedArrow (Cell m)) instance Monad m => Data.Profunctor.Choice.Choice (Cell m)

instance Monad m => Traversing (Cell m) where
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, we have to merge #95 first.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I use traverse' a lot. Very handy function. :-)

@@ -0,0 +1,30 @@
Copyright (c) 2022, Ivan Perez, Manuel Bärenz, and Miguel Negrão
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you ask Ivan, or are you adding him because of his authorship of yampa-test?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't ask, I'm adding due to his authorship of yampa-test and bearriver.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But this is a separate package, written by you, right? Or is it a verbatim copy of yampa-test?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Either way I believe you should not add him as a copyright owner, but rather state where you got the code or inspiration from. (I'm not a legal expert though.) @ivanperez-keera what do you think?

@@ -20,7 +20,9 @@ jobs:
enable-stack: true
stack-version: '2.7'
- name: Install dependencies
run: sudo apt-get install -y libxml2-utils libgl1-mesa-dev libglu1-mesa-dev freeglut3-dev libpulse-dev libblas-dev liblapack-dev libasound2-dev
run: |
sudo apt-get update
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good idea independently of this PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is needed independently of this PR. Without it it can't install the linux packages because the server addresses have changed or something like that.

@turion
Copy link
Owner

turion commented Apr 1, 2022

Thanks a lot! This is a huge contribution, I'll have to come back to it later. If you want to speed it up, maybe we can split it into only the FRP part and the test part?

@miguel-negrao
Copy link
Contributor Author

Thanks a lot! This is a huge contribution, I'll have to come back to it later. If you want to speed it up, maybe we can split it into only the FRP part and the test part?

My pleasure ! :-) Sure, I can do that. So I would do one PR just with the FRP part and another PR (on top of the previous one) with the test stuff ?

Today I was working on my GUI code, and I started wondering if

accum :: (Monad m, Data a) => a -> Cell m (Maybe (a -> a)) (Maybe a)

and similar functions should be generalized to

accum :: (Monad m, Data a, Traversable f) => a -> Cell m (f (a -> a)) (f a)

Such that it can either deal with events in the usual sense (Maybe e), or with lists of events ( [e]). This is relevant for instant when merging events from different sources to be fed into accum. If we want to take just one event then we would use Maybe e, but if want to group possible simultaneous events then we can use [e], and the function accum can deal with both just fine since it is defined as traverse' . accumB. To me it makes more sense to use the general type signature, although for newcomers it will be harder to understand what it does...

@miguel-negrao
Copy link
Contributor Author

I've split this into two PRs. This one only has the frp library, the test part will go into another PR. Also I've generalized the functions that use traverse' and fixed the documentation issues you mentioned.

repeatedly :: (Monad m, Data b) => Time -> b -> Cell (ClockInfoT m) a (Maybe b)
repeatedly q x
| q > 0 = afterEach qxs
| otherwise = error "bearriver: repeatedly: Non-positive period."
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
| otherwise = error "bearriver: repeatedly: Non-positive period."
| otherwise = error "essence-of-live-coding-frp: repeatedly: Non-positive period."

ev = if null qxsNow then Nothing else Just (map snd qxsNow)
return (ev, (qxsLater, t'))

-- Events
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you make all these captions haddock captions?

Suggested change
-- Events
-- * Events

Comment on lines +503 to +505
------------------------------------------------------------------------------
-- Maybe filtering
------------------------------------------------------------------------------
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
------------------------------------------------------------------------------
-- Maybe filtering
------------------------------------------------------------------------------
-- * Maybe filtering


-- Parallel composition and switching over collections with broadcasting

-- #if ( (4) < 4 || (4) == 4 && (8) < 14 || (4) == 4 && (8) == 14 && (0) <= 3)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Say what now 🤣

Comment on lines +586 to +600
-- dpSwitchB ::
-- (Functor m, Monad m, Traversable col) =>
-- col (Cell m a b) ->
-- Cell m (a, col b) (Maybe c) ->
-- (col (Cell m a b) -> c -> Cell m a (col b)) ->
-- Cell m a (col b)
-- dpSwitchB sfs sfF sfCs = Cell $ \a -> do
-- res <- T.mapM (`unMSF` a) sfs
-- let bs = fmap fst res
-- sfs' = fmap snd res
-- (e,sfF') <- unMSF sfF (a, bs)
-- ct <- case e of
-- Maybe c -> snd <$> unMSF (sfCs sfs c) a
-- Nothing -> return (dpSwitchB sfs' sfF' sfCs)
-- return (bs, ct)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do these not work out of the box in eolc?

Copy link
Owner

@turion turion left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A general question about direction of this library. Do you want:

  1. a mostly complete drop-in replacement for Yampa
  2. eolc, but time-aware, with signal processing

I'm personally only interested in 2., especially in light of turion/rhine#143 (rhine - eolc). Which would mean deleting a lot of special switching functions, possibly replacing them by more general concepts like resampling and exception handling. I'd also want to get rid of Yampa-specific operators, I think they aren't popular enough and don't make code more readable. I'd also want to add some ClSF-like code included (so we can later delete ClSF from rhine in favour of this library here).

Is that the direction you want to go as well? If yes we can start to prune, but if no you'll probably be happier with your own self-maintained library.

@miguel-negrao
Copy link
Contributor Author

I'm not particularly interested in using or porting Yampa-based code, so I don't feel the need keep the same API. I'm fine with nº2 as long as we have all the required combinators to deal with events and continuous time. Do you have some examples of operators you would want to remove ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants