cancel
Showing results for 
Search instead for 
Did you mean: 

Joy of q: Let it snow

SJT
Contributor III
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

jbetz34
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.

SJT
Contributor III
Contributor III

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

jbetz34
New Contributor II

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