[Haskell-beginners] FRP

Heinrich Apfelmus apfelmus at quantentunnel.de
Wed Oct 3 15:07:40 CEST 2012


Thanks for the explanation!

So, netwire is essentially based on the abstraction

     type Stoplight red green = Behavior' (Either red green)

of time-varying values that can either be a "normal" value of type 
green , or an "exceptional" ("inhibiting") value of type  red . (I have 
dropped the input dependence, so that arrow composition becomes normal 
function application.)

Here,  Behavior'  denotes a type of continues time-varying values, with 
the small twist that it can "detect some flanks", namely when the value 
jumps from  Right  to  Left . In this way, the flank can be interpreted 
as an  Event  in the reactive-banana sense.


The applicative instance is the composition of the applicative instances 
for  Behavior'  and  Either

     instance Monoid red => Applicative (Stoplight red) where
         pure x  = pure (Right x)
         f <*> x = (<$)> <$> f <*> x

if we assume that  Either  has the following applicative instance

     instance Monoid red => Applicative (Either red) where
         pure = Right
         (Right x) <*> (Right y) = Right (x y)
         (Left  x) <*> (Right y) = Left  x
         (Right x) <*> (Left  y) = Left  y
         (Left  x) <*> (Left  y) = Left  (x <*> y)


What I particularly like about this approach is the following: I like to 
think of Behavior as continuous functions, not necessarily because they 
are really continuous, but because this means that the semantics of 
Behavior is independent of any "update frequency", which is key point in 
simplifying code with FRP. Unfortunately, this means that we cannot 
"detect flanks", because a continuously changing Behavior simply does 
not change in discrete steps.

The Stoplight type solves this problem by introducing an explicit "this 
is a flank" value, i.e. by using the fact that the only way an  Either 
type can change is in discrete steps. Due to parametricity, I can't put 
this information into the general Behavior type, but the special case 
Behavior (Either red green)  can make use of this.


On the other hand, while the combination of Behaviors and Events allows 
for some slick switching combinators, I'm not entirely sure whether the 
mixture of two unrelated concepts (continuous functions vs singular 
occurrences) is too much of a conceptual burden.


Best regards,
Heinrich Apfelmus

Ertugrul Söylemez wrote:
> Heinrich Apfelmus  wrote:
> 
>>> Wire is also an Alternative, which allows concise and efficient
>>> switching with very little cruft.  The following wire renders "yes"
>>> when the "keyDown Space" event happens and "no" otherwise:
>>>
>>>     pure "yes" . keyDown Space <|> pure "no"
>>>
>>> Or with the OverloadedStrings extension:
>>>
>>>     "yes" . keyDown Space <|> "no"
>>>
>>> All classic (non-wire) FRP implementations need switching or another
>>> ad-hoc combinator for this.  If you happen to need switching it's
>>> also a lot simpler using wires:
>>>
>>>     "please press space" . notE (keyDown Space) --> "thanks"
>>>
>>> This one waits for the Space key and then outputs "thanks" forever.
>>> So far Netwire has the fastest and most elegant way of dealing with
>>> events compared to all other libraries I have tried.
>> These examples look neat!
>>
>> I'm a bit confused about the model you are using, though. If I
>> understand that correctly, you don't distinguish between events and
>> behaviors; rather, you are working with data streams in discrete time
>> steps. Still, I don't quite understand.
> 
> First let me try to put reactive-banana's model into a data type of my
> own, which you might call TimedZipStream.  The name Behavior is easier,
> so let me pick that one instead (Time is the type for time deltas):
> 
>     newtype Behavior a =
>         Behavior {
>           stepBehavior :: Time -> Network (a, Behavior a)
>         }
> 
> This is not anywhere near how reactive-banana represents its behaviors,
> but just a simplified and less powerful model.  The argument is the time
> delta to the last instant.  Communication happens through the Network
> monad and is opaque to the user.  The type is an applicative functor
> that represents values that can behave differently at each instant.
> 
> My model is similar to Yampas model, where instead of time-varying
> values you have time-varying functions, so-called signal functions:
> 
>     newtype SF a b =
>         SF {
>           stepSF :: Time -> a -> (b, SF a b)
>         }
> 
> This is a function from an 'a' to a 'b' that mutates over time.  There
> is a counterpart for classic behaviors, which is when the input type is
> fully polymorphic:
> 
>     time :: SF a Time
> 
> SF forms a family of applicative functors, but now there is a direct
> path from one signal function to the next, because SF is itself a
> category.  No graph, no monad, just plain categorical composition.
> Unfortunately to this day Yampa does not provide an Applicative
> instance, so you have to use the Arrow interface, which is usually very
> ugly.
> 
> The weak spot of both models is events.  They need to be handled using
> switchers and other combinators.  Causality, simultaneity and choice all
> need to be encoded explicitly.  Event modifiers work outside of the
> behavior level.
> 
> What makes Netwire different?  Wire categories are encoded by the
> following (simplified) type:
> 
>     newtype Wire e a b =
>         Wire {
>           stepWire :: Time -> a -> (Either e b, Wire e a b)
>         }
> 
> Wires can choose not to output anything, but instead inhibit with a
> value of type 'e'.  Where i is an inhibiting wire the following
> identities hold:
> 
>     x . i = i
>     i . x = i
> 
> Furthermore now when 'e' is a Monoid Wire is a family of Alternative
> functors with the following identities, where x and y produce and i, j
> and ij' and inhibit:
> 
>     x <|> y = x
>     i <|> y = y
> 
>     i <|> j = ij'
> 
> The ij' wire also inhibits, but mappend-combines the inhibition values.
> The empty wire always inhibits with mempty.  The monoid is necessary for
> the Category, Applicative and Alternative laws to hold.
> 
> 
>> What is
>>
>>      pure "yes" . keyDown Space <|> pure "no"
>>
>> supposed to mean? If it's a function Time -> String , how long does it
>> have the "yes" value? 439.7 milliseconds? If it's an event, how often
>> does the "no" event fire?
> 
> An event wire is a wire that acts like the identity wire when it
> produces, but may choose to inhibit instead:
> 
>     pure "yes" . keyDown Space
> 
> The 'keyDown Space' wire acts like the identity wire when the space key
> is pressed, otherwise it inhibits.  As a consequence of the above laws
> the composition also inhibits.  This is where (<|>) comes in:
> 
>     pure "yes" . keyDown Space <|> pure "no"
> 
> When the left wire inhibits, the right wire takes over.  By definition a
> 'pure' wire never inhibits.  Notice that in this example I'm assuming
> that 'keyDown' is a 'continuous event'.  That's where behaviors are
> mixed with events.  An event can actually have a duration.
> 
> If 'keyDown' would be instantaneous without a duration you could use the
> 'holdFor' combinator:
> 
>     pure "yes" . holdFor 1 (keyDown Space) <|> pure "no"
> 
> This would also work for a continuous 'keyDown' event wire.  Then if you
> press the space key for one second, "yes" is displayed for two seconds.
> 
> 
>> Concerning the other example, I don't understand what the expression
>>
>>     "please press space" . notE (keyDown Space)
>>
>> means. If it's a function, what value does it have when the key was
>> pressed? If it's an event, how often does it "fire" the string value?
> 
> In this case it doesn't really matter if 'keyDown Space' has a duration
> or not.  As soon as it produces once, the switch happens and the next
> wire takes over.  There is another name for the (-->) combinator called
> 'andThen'.
> 
>     x --> y
> 
> As soon as x inhibits, this combination switches to y.
> 
> Side note:  Unlike classic FRP a wire category is not time-bound.  In
> fact in the current official release of Netwire (3.1.0) time is actually
> an extension.  In Netwire time is back as a primitive, because it makes
> time-related wires (the analog to behaviors) much easier to write.  I
> have retained the flexibility that your wire can have a time frame
> different from real time, though.

--
http://apfelmus.nfshost.com




More information about the Beginners mailing list