How to work around the first-order constraint on arrows?
Asked Answered
T

3

7

What I mean by first-order constraint

First, I'll explain what I mean by first-order constraint on arrows: Due to the way arrows desugar, you cannot use a locally bound name where an arrow command is expected in the arrow do-notation.

Here is an example to illustrate:

proc x -> f -< x + 1 desugars to arr (\x -> x + 1) >>> f and similarly proc x -> g x -< () would desugar to arr (\x -> ()) >>> g x, where the second x is a free variable. The GHC user guide explains this and says that when your arrow is also a monad you may make an instance of ArrowApply and use app to get around this. Something like, proc x -> g x -<< () becomes arr (\x -> (g x, ())) >>> app.

My Question

Yampa defines the accumHold function with this type: a -> SF (Event (a -> a)) a. Due to this first-order limitation of arrows, I'm struggling to write the following function:

accumHoldNoiseR :: (RandomGen g, Random a) => (a,a) -> g -> SF (Event (a -> a)) a
accumHoldNoiseR r g = proc f -> do
  n <- noiseR r g -< ()
  accumHold n -< f

The definition above doesn't work because n is not in scope after desugaring.

Or, similarly this function, where the first part of the pair to SF is meant to be the initial value passed to accumHold

accumHold' :: SF (a,Event (a -> a)) -> a
accumHold' = ...

Is there some combinator or trick that I'm missing? Or is it not possible to write these definitions without an ArrowApply instance?

tl;dr: Is it possible to define accumHoldNoiseR :: (RandomGen g, Random a) => (a,a) -> g -> SF (Event (a -> a)) a or accumHold' :: SF (a,Event (a -> a)) -> a in yampa?

Note: There is no instance of ArrowApply for SF. My understanding is that it doesn't make sense to define one either. See "Programming with Arrows" for details.

Thermae answered 29/6, 2013 at 23:28 Comment(3)
I'm not an arrow expert, so I don't know if you need ArrowApply or not, but one way to approach the problem is to ask yourself what you think your code without the first-order limitation should desugar to.Zima
The use of 'n' in 'accumHold n -< f' is out of scope, but I suspect that '-<<' can replace '-<' here and make 'n' to be in scope.Watkins
@ChrisKuklewicz -<< requires an ArrowApply instance so it doesn't make sense here (ie., you can't make SF into a monad).Thermae
P
3

This is a theoretical answer. Look to Roman Cheplyaka's answer to this question, which deals more with the practical details of what you're trying to achieve.


The reason n is out of scope is that for it to be in scope to use there, you would have the equivalent of bind or >>= from monads. It's the use of the results of a previous computation as a functional input to the next which makes something as powerful as a monad.

Hence you can supply n as a function argument to a subsequent arrow exactly when you can make an ArrowApply instance.

Chris Kuklewicz correctly points out in his comment that -<< would bring n into scope - it also uses app, so you need an ArrowApply instance.

Summary

Not unless you use ArrowApply. This is what ArrowApply is for.

Predisposition answered 30/6, 2013 at 15:10 Comment(0)
R
2

noiseR is a signal function; it produces a stream of random numbers, not just one random number (for that, you'd just use randomR from System.Random).

On the other hand, the first argument of accumHold is just one, initial, value.

So this is not just some limitation — it actually prevents you from committing a type error.

If I understand correctly what you're trying to do, then simply using randomR should do the trick. Otherwise, please clarify why you need noiseR.

Reneta answered 30/6, 2013 at 10:2 Comment(1)
I used randomR (instead of noiseR) inside accumHoldNoiseR, it would give me a local binding and so I wouldn't be able to use it.Thermae
T
0

To help others understand how I worked around this I'll answer my own question.

I was trying to implement the game pong. I wanted the ball to start with a random velocity each round. I wanted to use accumHold to define the ball's velocity. I had some code like this:

ballPos = proc e -> mdo -- note the recursive do
  {- some clipping calculations using (x,y) -}
  ...
  vx <- accumHold 100 -< e `tag` collisionResponse paddleCollision
  vy <- accumHold 100 -< e `tag` collisionResponse ceilingFloorCollision
  (x,y) <- integral -< (vx,vy)
  returnA -< (x,y)

I wanted to replace the 100s with random values (presumably from noiseR).

How I solved this instead is to accumulate over the direction, where collisionResponse just flips the sign (eventually I'll want to use the angle of the velocity relative to wall/paddle):

ballPos = proc (initV, e) -> mdo
  {- some clipping calculations using (x,y) -}
  ...
  (iVx,iVy) <- hold (0,0) -< initV
  vx <- accumHold 1 -< e `tag` collisionResponse paddleCollision
  vy <- accumHold 1 -< e `tag` collisionResponse ceilingFloorCollision
  (x,y) <- integral -< (iVx*vx,iVy*vy)
  returnA -< (x,y)

Lesson Learned:

You can often separate the value/state you want to accumulate into a behavior describing how it changes and a "magnitude" that describes its current value taking the behavior as input. In my case, I separate out the magnitude of the initial velocity, pass that as input to the signal function, and use accumHold to compute the affect on the ball (the behavior) of having collisions. So regardless of what the initial velocity was, hitting the walls "reflects" the ball. And that's exactly what the accumHold is accumulating.

Thermae answered 30/6, 2013 at 17:6 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.