Skip to content

Let it snow

Snowflakes

A post on the Array Thinking blog uses APL to explore an array-oriented approach to a simple problem: visualising 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 a few lines 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.

next{
   (L T){'⍟**∘∘∘...... '[]}¨13⌊?(0 ¯1+⍴)¨100
   L,T¯1 ¯1
}
{} {picnext ⎕DL÷8} 200 pic

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.

Snowflakes as characters

The APL solution exploits its IDE, whose editor instantly reflects changes to a variable. Instead, we’ll use a browser to call q.

snow.q:

/ initial state
FRAME:30 80 
generate:{"@**......... " x#prd[x]?100} 
pic:generate FRAME 

/ step
advance:{lt:generate each FRAME-0 1; 
  lt[0],'enlist[lt 1], -1 _ -1 _'x} 

/ animate on each GET
.z.ph:{.h.hp pic::advance pic} 
PORT:5000+sum`long$"snow" 
system "p ",string PORT 
-1 "Listening on ",string PORT;
Not the brutal elegance of the APL lines, 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 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.

Snowfall
Snowfall: the display changes with each browser Refresh

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.

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 }

.z.ph:{.h.hp pic::advance jiggle twinkle pic} 
This is better, a little less rigid.

Snow: fall, jiggle and twinkle
Jiggling and twinkling

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 it would take us further from the simple strategy of shifting the whole display.

That strategy is played out; we’ll now shift to a different model.

Snowflakes as vectors

We’ll tabulate the flakes as vectors (of row, column and depth positions) and project them onto a character array. (Hooray for terse languages.)

FRAME: 2#RCD:30 80 10                                  / rows; columns; depth 
BOUNDS: `r`c`d!0,'RCD-1                                / stay within 
We begin with an empty sky; positions will be floats.
Flakes: flip`r`c`d!0#'0 0 0f                           / ([]row;col;depth)
Global FALL specifies how many new flakes in each cycle, and WIND the horizontal wind speed.
FALL: 9                                                / flakes per cycle
WIND: 0.3                                              / wind speed
We’ll move distant flakes less than near flakes. With a distance scale TRIG we are ready to start.
TRIG:2*atan .5%1+til RCD 2
Project the flakes onto a blank canvas.
CANVAS: FRAME#" "
FLAKES: "#**......."                                   / snowflakes by depth
disp: {./[CANVAS;flip x`r`c;:;FLAKES x`d] } "j"$       / display Flakes [x]
q)show f: flip`r`c`d!FALL?'RCD                         / random flakes
r  c  d
-------
23 14 1
14 17 0
9  49 5
14 11 1
13 8  0
9  8  0
13 42 1
10 19 7
23 12 6
q)-1 disp f;









        #                                        .
                   .


        #                                 *
           *     #








            . *
The step function advance moves the flakes. In its last line new flakes get appended to the table.
advance:{[f]                                           / [flakes]
  dwd:TRIG "j"$ f`d;                                     / diminish in distance 
  gust:-.5+first 1?1f; 
  f:update r:r+dwd, c:c+(WIND+gust)*dwd from f;          / fall
  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;  / leave frame
  f upsert flip 0 1 1f*FALL?'RCD }                       / new flakes
Now we see the near flakes moving faster.

Snowfall 3

snow2.q

Exercises

  1. Make flakes twinkle at random in the light.

    Answer

    Treat the twinkling as an ephemeral property of the display. That is, there is no change to the value of Flakes.

    twinkle:{@[y;.[?] "j"$count[y]%x,1;:;"+"]}[7;]         / twinkle 1 in 7
    disp:{./[CANVAS;flip x`r`c;:;twinkle FLAKES x`d] } "j"$
    

  2. Wind gusts could be stronger eddies in the air. And vertical as well as horizontal.

    Answer

    Change just two lines of advance:

    (gv;gh): -.7+2?1f;                                     / gust vert & horiz
    f:update r:r+dwd+gv, c:c+(WIND+gh)*dwd from f;         / fall
    

  3. Instead of using .h.hp, compose the HTML document with a meta element in the head to autorefresh.

    Answer

    Replace .h.hp with .h.hn and your own HTML.

    html:{
      r:"<!doctype html>\n";
      r,:"<html>\n";
      r,:"<head>\n";
      r,:"<meta charset=\"utf-8\">\n";
      r,:"<meta http-equiv=\"refresh\" content=\"1\">\n";
      r,:"<style>body {background-color:black;color:white;font:10pt Verdana}</style>\n";
      r,:"</head>\n";
      r,:"<body>\n<pre>\n",("\n"sv x),"</pre>\n</body>\n";
      r,"</html>\n" }
    
    .z.ph: {.h.hn["200";`html;] html disp Flakes::advance Flakes}             / callback
    
    Snowfall 5

  4. 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. [jbetz34 on community.kx.com]

    No, I don’t know how to do this either.

An earlier version of this article appeared on community.kx.com.