HeinrichApfelmus / reactive-banana

Library for functional reactive programming in Haskell.
https://wiki.haskell.org/Reactive-banana
522 stars 71 forks source link

Combinator for applying a function in an event to a value in a behavior and making a new Event #89

Open Kritzefitz opened 9 years ago

Kritzefitz commented 9 years ago

I would like to propose an operator similiar to <@>. Instead of applying the function from the behavior to the value in the event, the function from the event is applied to the value of the behavior. The operator would thus have the following type:

foo :: Event t (a -> b) -> Behavior t a -> Event t b

Currently this can be accomplished by doing something like this:

myFunctionEvent :: Event t (a -> B)
myFunctionEvent = <...>

myValueBehavior :: Behavior t a
myValueBehavior = <...>

myAppliedEvent :: Event t b
myAppliedEvent = fmap (flip ($)) myValueBehavior <@> myFunctionEvent

The fmap (flip ($)) myValueBehavior may seem a bit cryptic (that's why I'm proposing this new operator). Its purpose is to create a new behavior that contains the function ($ myValue), which will the be applied to the function in the event, ending up as myFunction $ myValue. With the proposed operator myAppliedEvent could be shortened to this:

myAppliedEvent = myFunctionEvent `foo` myValueBehavior
HeinrichApfelmus commented 9 years ago

To be honest, I'm not too keen on creating a new operator for situations that are not very common. Do you have any code examples where this operator is useful?

In your particular example, you can use <$> to save a few parentheses.

flip ($) <$> myValueBehavior <@> myFunctionEvent

I think this is reasonably clear. If anything, it may be more useful to give a better name to flip ($), because this combination appears in other contexts as well.

Kritzefitz commented 9 years ago

The version you provided does indeed look alright. I think it would be ok to go with it. But because you asked for a use case, here a simplified version of my use case:

I have a behavior in the following form:

someBehavior :: Behavior t a
someBehavior = accumB [] appliedEvent -- I will get to the definition of appliedEvent later

Now I want to modify my behavior through the following event:

someEvent :: Event t (b -> a -> a)
someEvent = <...> -- Some input, probably something fromAddHandler

That b is a value from the data graph that influences how the value in someBehavior is changed. So the current way to define appliedEvent would be this (assuming that someOtherBehavior provides the required b):

appliedEvent :: Event t (a -> a)
appliedEvent = flip ($) <$> someOtherBehavior <@> someEvent

I don't know how common this use case is. So it's up to you to decide whether to make a new operator for this.

HeinrichApfelmus commented 9 years ago

I currently think that

flip ($) <$> myValueBehavior <@> myFunctionEvent

is the best way to go about it. But thanks again for the suggestion!

ocharles commented 9 years ago

I can see how it could be problematic if you had a function where one argument from an Event was "in the middle". For example, consider some contrived function...

foo :: A -> B -> C -> D

If you have

a :: Behavior A
b :: Event B
c :: Behavior C

it is quite hard to form Event D:

ev = (\a' c' b' -> foo a' b' c') <$> a <*> c <@> b

which is pretty weird!

My preference would be that <@> is actually a type-classed operator, but I'd have to give this a bit more thought.

That said, it's all hypothetical - I've been nervous about this for a while, but it hasn't actually come up in practice.

HeinrichApfelmus commented 9 years ago

Usually, I tend to rewrite the definition of foo, or just use the seemingly complicated form. The Animation.hs example has some expressions where I'm not 100% happy either, but I think it's ok once you get used to the "dollar, star, at" pattern.

The trouble with polymorphic <@> is that it may lead to ambiguities — though I'm not sure if that happens here. It's probably not worth the cost, but I'll leave the issue open in case someone finds a solution.

archaephyrryx commented 8 years ago

This might not be entirely practical with the specific implementations of Event and Behavior, but I have a mockup of a <@> operator and a complementary <#> operator that work with dummy Behavior and Event types (which are just value wrappers in my example). The exact instance implementation of <@> and <#> should not be much harder than the current <@> implementation as far as I can guess, but this may not be practical for whatever reason.

{-# LANGUAGE TypeFamilies #-}

import Control.Applicative ((<$>))

class Functor k => FRDual k where
  type Dual k :: * -> *
  (<@>) :: (Dual k) (a -> b) -> k a -> k b
  (<#>) :: k (a -> b) -> (Dual k) a -> k b

data Behavior x = B { b :: x }
data Event x    = E { e :: x }

instance Functor Behavior where
  fmap f (B x) = B (f x)

instance Functor Event where
  fmap f (E x) = E (f x)

instance FRDual Behavior where
  type Dual Behavior = Event
  (E f) <@> bx = f <$> bx
  bf <#> (E x) = ($x) <$> bf

instance FRDual Event where
  type Dual Event = Behavior
  (B f) <@> ex = f <$> ex
  ef <#> (B x) = ($x) <$> ef
HeinrichApfelmus commented 8 years ago

@archaephyrryx So, essentially, you are proposing to give a name to this operator.

(<#>) :: Event (a -> b) -> Behavior a -> Event b
ef <#> bx = flip ($) <$> bx <*> ef

I'm not sure if this particular naming scheme is a good idea. The operator (#) is used in the diagrams library as a reverse function application, so if we take that as a "standard" (which we don't have to), then I would expect the type

(<#>) :: Behavior a -> Behavior (a -> b) -> Behavior b

instead. In particular, I would expect that the function argument comes second.

I don't think that the idea with overloading (<@>) works. It's not possible to write combinators with types

bar :: Event (a -> b) -> Behavior a -> Behavior b
bar2 :: Behavior (a -> b) -> Event a -> Behavior b

that do the right thing (= are total).

archaephyrryx commented 8 years ago

@HeinrichApfelmus

I agree with you that <#> is a rather unfortunate name, though I chose it just for the purposes of the mock-up. I also figured that for Behavior, an FRDual instance might not be possible (or, even if it technically could be done, it wouldn't necessarily conform to the underlying models). In any case, this was more of a proof of concept than a true solution, but I think I might have a revision that addresses some of the issues:

{-# LANGUAGE TypeFamilies #-}

import Control.Applicative ((<$>))
infixl 4 <@>

class Functor k => FRDual k where
  type Dual k :: * -> *
  (<@>) :: k (a -> b) -> (Dual k) a -> Event b

data Behavior x = B { b :: x }
data Event x    = E { e :: x }

instance Functor Behavior where
  fmap f (B x) = B (f x)

instance Functor Event where
  fmap f (E x) = E (f x)

instance FRDual Behavior where
  type Dual Behavior = Event
  (B f) <@> ex = f <$> ex

instance FRDual Event where
  type Dual Event = Behavior
  ef <@> bx = flip ($) <$> bx <@> ef