cancel
Showing results for
Search instead for
Did you mean:

## Joy of q: Let it snow

Contributor III

In a post on the Array Thinking blog I explored an array-oriented approach to a simple problem: representing snowflakes falling through the air.

The problem is a classic for an object-oriented approach: define a `Snowflake` class, set wind speed as a global, define a `Fall` method for `Snowflake` with a small random element, make a collection of `Snowflake` instances in a property of a `Sky` object that iterates their `Fall` methods and plots them on a display. Almost writes itself.

Wouldn’t you know? There’s a solution in one line of q’s ancestor language APL. It illuminates how array programmers approach problems: thinking more about gross data structures than breaking the problem into small pieces.

We’ll start here by replicating the APL solution in q, then improving it a bit. Then we’ll cut to a different approach entirely to add more features, and we shall all give silent thanks to the language’s brevity.

The APL solution exploits its IDE, whose editor instantly reflects changes to a variable. We’ll use a browser to call q. Here’s `snow.q`.

``````FRAME:30 80
generate:{"@**......... " x#prd[x]?100}
pic:generate FRAME
.z.ph:{.h.hp pic::advance pic}
advance:{
lt:generate each FRAME-0 1;
lt[0],'enlist[lt 1], -1 _ -1 _'x }
PORT:5000+sum`long\$"snow"
system "p ",string PORT
-1 "Listening on ",string PORT;``````

Not the brutal elegance of the APL line, but straightforward enough. A `generate` function returns a character array with snowflakes: dots for distant flakes, larger glyphs for nearer flakes. An `advance` function shifts the frame down and right and generates some more flakes `lt` for the left and top. The HTTP GET callback `.z.ph` advances the state and sends it to the browser as an HTML `pre` block.

We can do better. Snowflakes don’t fall in straight lines, not even diagonal ones. They jiggle about a bit with random gusts. And if the sun is out, some of them might twinkle.

``````.z.ph:{.h.hp pic::advance jiggle twinkle pic}
twinkle:{
v:raze x;
v:@[v;where v="+";:;"."]; /dim
i:where v=".";
FRAME#@[v;floor[.1*count i]?i;:;"+"] }
jiggle:{
f:v i:where not null v:raze x;
j:(prd[FRAME]-1)& 0|i + count[i]?-2 0 2 where 1 8 1;
v[i]:" ";
v[j]:f;
FRAME#v }
``````

This is better – a little less rigid.

But the big missing is that the near flakes should be moving faster than the far flakes.

We could do that on the character array – we have already jiggled the flakes –  but we’ll now shift to a different model. We’ll tabulate the flakes as vectors (of row, column and depth positions) and project them onto a character array. (Thank goodness for terse languages.) Amend At is perfect for that – and notice how elegantly the distance positions get converted to glyphs. Notice also the use of the frame size as an arithmetic base: `FRAME sv x`r`c` converts coordinates to index positions.

``````FRAME:2#RCD:30 80 10 / rows; columns; depth
BOUNDS:`r`c`d!0,'RCD-1 / stay within
Flakes:([]r:0#0.;c:0#0.;d:0#0.) / row, col; depth
rnd:floor .5+
disp:{FRAME#@[prd[FRAME]#" ";FRAME sv x`r`c;:;"#**......."@x`d]} rnd@``````

We’ll move distant flakes less than near flakes, so we need a distance scale, and positions will be floats. The `Flakes` table start empty; global `FALL` specifies how many new flakes in each cycle and `WIND` the horizontal wind speed. With a distance scale `TRIG` we are ready to start.

``````FALL:9 / flakes per cycle
PORT:5000+sum`long\$"snow"
/ apparent movement diminishes with distance
TRIG:2*atan .5%1+til RCD 2 /https://elvers.us/perception/visualAngle/
WIND:0.3
advance:{[f]
dwd:TRIG rnd f`d; /diminish with distance
gust:-.5+first 1?1f;
f:update r:r+dwd, c:c+(WIND+gust)*dwd from f;
f:update r:r+dwd*(count[f]?2.)-1, c:c+dwd*(count[f]?2.)-1 from f; /jiggle
f:delete from f where any each not f within'\:BOUNDS; /fallen
f upsert flip 0 1 1f*FALL?'RCD } /new flakes
/ callback
.z.ph:{.h.hp disp Flakes::advance Flakes}

system "p ",string PORT
-1 "Listening on ",string PORT;
``````

In the last line of `advance` new flakes – as float vectors – get appended to the table. We begin with an empty sky. Now we see the near flakes moving faster.

`snow2.q`

``````/ constants
FRAME:2#RCD:30 80 10 / rows; columns; depth
BOUNDS:`r`c`d!0,'RCD-1 / stay within
FALL:9 / flakes per cycle
PORT:5000+sum`long\$"snow"
/ apparent movement diminishes with distance
TRIG:2*atan .5%1+til RCD 2 / https://elvers.us/perception/visualAngle/
WIND:0.3
/ globals
Flakes:([]r:0#0.;c:0#0.;d:0#0.) / row, col; depth
/ functions
rnd:floor .5+
disp:{FRAME#@[prd[FRAME]#" ";FRAME sv x`r`c;:;"#**......."@x`d]} rnd@
advance:{[f]
dwd:TRIG rnd f`d; / diminish with distance
gust:-.5+first 1?1f;
f:update r:r+dwd, c:c+(WIND+gust)*dwd from f;
f:update r:r+dwd*(count[f]?2.)-1, c:c+dwd*(count[f]?2.)-1 from f; / jiggle
f:delete from f where any each not f within'\:BOUNDS;
f upsert flip 0 1 1f*FALL?'RCD }
/ callback
.z.ph:{.h.hp disp Flakes::advance Flakes}

system "p ",string PORT
-1 "Listening on ",string PORT;``````

## To do

• Make flakes sparkle at random in the sunlight.
• Wind gusts could be stronger – eddies in the air. And vertical as well as horizontal.
• Instead of using `.h.hp`, compose the HTML document returned with a `meta` element in the `head` to autorefresh.

Over to you.

3 REPLIES 3
New Contributor II

Very cool. A possible improvement might be to asynchronously push on a timer from the q server once connection has been established. That way you don't have to refresh every time to see the snow fall. Can't wait to try for myself.

Contributor III

Ajax would surely be better! (Post a script here?)

New Contributor II

A bit more than just Ajax... Joy of q: It's snowing again