[racket] Re-inventing dataflow languages

17 views
Skip to first unread message

John Clements

unread,
Oct 11, 2012, 7:26:58 PM10/11/12
to Shriram Krishnamurthi, Gregory Cooper, users@racket-lang.org list
As part of the rsound package, I find myself reinventing data flow languages; or, at least, the parts that I need. I've come to a design question that I don't know how to answer, and I'm hoping that those with more experience can make suggestions.

First: I'm re-inventing dataflow languages because:
- the natural way to specify audio signals is as dataflow networks (e.g., I have a low-frequency oscillator controlling the frequency of this other oscillator…), and
- FrTime is sadly not fast enough to use, here.

Also, I'm fine with dataflow, here, I don't need full FRP.

The other connection here is to World-style programming; in a big-bang, you specify an initial world and then a transition function (or functions), that map one world to the next. I can't use this framework as-is because of parallel composition issues; specifically, if I have the two oscillators I describe above, then the natural way to represent their state would be as a structure containing the state of each individual component. Allocating a new one of these 44K times per second uses up a lot of memory really quickly. So, I've developed my own framework, that uses mutation but hides it from the user.

Using my "network" syntax, I might specify the above as:

(network ()
[lfo (sine-wave 2)]
[out (sine-wave (+ 200 (* 10 lfo)))])

This creates a sine wave at 2 Hz, controlling another sine wave whose frequency varies between 210 and 190 Hz twice per second.

Everything fine so far.

In order to make this work, we need signals that refer to old values of themselves, as in the world model. To allow this, I have a "prev" primitive that allows you to refer to the prior value of some signal node. So, for instance, here's a simple counter that goes up one each tick:

(define simple-ctr
(network ()
[out (add1 (prev out)) #:init 0]))

So, this counter rises by one on each tick. Note that we need to specify an initial value, as with big-bang.

BUT, HERE COMES THE PROBLEM.

What should the first value of this signal be? Should it be zero, or should it be one? Put differently: does this node's updater get called on the first tick, producing 1, or should we just use the initial value, zero?

Zero is the one I really want, but then I have a problem: how do I distinguish between clauses, like this one, that should not be evaluated the first time through, from others, something like this:

[sum (+ sig-a sig-b)]

… that *should* be evaluated the first time through?

My current solution is to treat them uniformly, and to evaluate all of them each time. This means that I wind up with dreadful hacks like this one:

[out (add1 (prev out)) #:init -1]

… so that the -1 gets bumped up to a zero on the first output. It turns out that this trick is fragile; if you don't know what the increment will be, you can't accurately "pre-decrement" it.


I can imagine doing something more complicated, but what I really want to ask is this: for those of you with experience in other dataflow languages, how do they solve this?


Sorry for the long e-mail; I'm hoping that someone can point to a nice solution that exists in another language!


John



Gregory Cooper

unread,
Oct 11, 2012, 7:56:08 PM10/11/12
to John Clements, users@racket-lang.org list, Shriram Krishnamurthi
On Thu, Oct 11, 2012 at 4:26 PM, John Clements
<clem...@brinckerhoff.org> wrote:
> As part of the rsound package, I find myself reinventing data flow languages; or, at least, the parts that I need. I've come to a design question that I don't know how to answer, and I'm hoping that those with more experience can make suggestions.
>
> First: I'm re-inventing dataflow languages because:
> - the natural way to specify audio signals is as dataflow networks (e.g., I have a low-frequency oscillator controlling the frequency of this other oscillator…), and
> - FrTime is sadly not fast enough to use, here.

Yes. There are significant opportunities for performance improvement
in FrTime, but for this sort of scenario, I think a "pull" evaluator
will be hard to beat.

> Also, I'm fine with dataflow, here, I don't need full FRP.
>
> The other connection here is to World-style programming; in a big-bang, you specify an initial world and then a transition function (or functions), that map one world to the next. I can't use this framework as-is because of parallel composition issues; specifically, if I have the two oscillators I describe above, then the natural way to represent their state would be as a structure containing the state of each individual component. Allocating a new one of these 44K times per second uses up a lot of memory really quickly. So, I've developed my own framework, that uses mutation but hides it from the user.
>
> Using my "network" syntax, I might specify the above as:
>
> (network ()
> [lfo (sine-wave 2)]
> [out (sine-wave (+ 200 (* 10 lfo)))])
>
> This creates a sine wave at 2 Hz, controlling another sine wave whose frequency varies between 210 and 190 Hz twice per second.
>
> Everything fine so far.
>
> In order to make this work, we need signals that refer to old values of themselves, as in the world model. To allow this, I have a "prev" primitive that allows you to refer to the prior value of some signal node. So, for instance, here's a simple counter that goes up one each tick:
>
> (define simple-ctr
> (network ()
> [out (add1 (prev out)) #:init 0]))
>
> So, this counter rises by one on each tick. Note that we need to specify an initial value, as with big-bang.
>
> BUT, HERE COMES THE PROBLEM.
>
> What should the first value of this signal be? Should it be zero, or should it be one? Put differently: does this node's updater get called on the first tick, producing 1, or should we just use the initial value, zero?
>
> Zero is the one I really want, but then I have a problem: how do I distinguish between clauses, like this one, that should not be evaluated the first time through, from others, something like this:

Can you not just use the presence of the #:init keyword to disable
evaluation for the first step (or the first n steps in general)?
That's probably what I'd do, though I may be missing something that
makes this problematic...

--Greg

> [sum (+ sig-a sig-b)]
>
> … that *should* be evaluated the first time through?
>
> My current solution is to treat them uniformly, and to evaluate all of them each time. This means that I wind up with dreadful hacks like this one:
>
> [out (add1 (prev out)) #:init -1]
>
> … so that the -1 gets bumped up to a zero on the first output. It turns out that this trick is fragile; if you don't know what the increment will be, you can't accurately "pre-decrement" it.
>
>
> I can imagine doing something more complicated, but what I really want to ask is this: for those of you with experience in other dataflow languages, how do they solve this?
>
>
> Sorry for the long e-mail; I'm hoping that someone can point to a nice solution that exists in another language!
>
>
> John
>
>
>

____________________
Racket Users list:
http://lists.racket-lang.org/users

Tony Garnock-Jones

unread,
Oct 12, 2012, 2:22:48 PM10/12/12
to John Clements, users@racket-lang.org list
On 10/11/2012 07:26 PM, John Clements wrote:
> I can imagine doing something more complicated, but what I really want to ask is this: for those of you with experience in other dataflow languages, how do they solve this?

The systems I've worked with have used a delay node in the graph rather
than your previous-value idea. If the delay is nonzero (positive), it
can be placed into a feedback cycle. You still have the problem of
specifying what value to use for the delay until the first signals make
it through to the other end, but it could be a less roundabout way of
thinking about the problem?

(define simple-ctr
(network ()
[out (delay (add1 out) 1 #:init 0)]))

vs

(define simple-ctr
(network ()
[out (add1 (delay out 1 #:init 0))]))

Regards,
Tony

Matthias Felleisen

unread,
Oct 12, 2012, 6:18:54 PM10/12/12
to Tony Garnock-Jones, users@racket-lang.org list, John Clements

Tony,

that was my initial reaction (point to Tony's world-generalization).

An alternative is to have network take different groups of clauses: the first one for handlers triggered at t = 0, the second one for handlers to be added after the signal is thru at the other end. I am thinking of generalizing presence/absence here from your current model because John may have to generalize this idea yet more. Do send him a copy of your submission.

-- Matthias

John Clements

unread,
Oct 16, 2012, 5:21:13 PM10/16/12
to Tony Garnock-Jones, users@racket-lang.org list

On Oct 12, 2012, at 11:22 AM, Tony Garnock-Jones wrote:

> On 10/11/2012 07:26 PM, John Clements wrote:
>> I can imagine doing something more complicated, but what I really want to ask is this: for those of you with experience in other dataflow languages, how do they solve this?
>
> The systems I've worked with have used a delay node in the graph rather than your previous-value idea. If the delay is nonzero (positive), it can be placed into a feedback cycle. You still have the problem of specifying what value to use for the delay until the first signals make it through to the other end, but it could be a less roundabout way of thinking about the problem?
>
> (define simple-ctr
> (network ()
> [out (delay (add1 out) 1 #:init 0)]))
>
> vs
>
> (define simple-ctr
> (network ()
> [out (add1 (delay out 1 #:init 0))]))

I like this, but I'm going to require the delay to be closer to the named node; in fact, I'm going to skootch it in all the way, where it basically turns into my existing "prev". So, another way of saying this is that the #:init is now attached to the "prev", rather than to the clause. In fact, I think I'm just going to require it. You're probably right that "delay" is a better term than "prev", though.

This means I'm stuck with a three-node solution:

(define simple-ctr
(network ()
[a (delay b 0)]
[b (+ 1 a)]
[out a]))

… but it saves me from doing extensive macro processing to isolate the clause references within an arbitrary definition, because (in my proposed world) the 'delay' must be wrapped immediately around that reference.

I think this will be okay, because most people will just be using these abstractions, rather than defining them….

Many thanks!

John Clements



>
> Regards,
> Tony

Reply all
Reply to author
Forward
0 new messages