Let it snow
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↓⍵
}
{} {pic∘←next ⍵⊣⎕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;
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: 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}
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
Flakes: flip`r`c`d!0#'0 0 0f / ([]row;col;depth)
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
TRIG
we are ready to start.
TRIG:2*atan .5%1+til RCD 2
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;
# .
.
# *
* #
. *
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
Exercises
-
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"$
-
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
-
Instead of using
.h.hp
, compose the HTML document with ameta
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
-
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.