-
Joy of q: Let it snow
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 aFall
method forSnowflake
with a small random element, make a collection ofSnowflake
instances in a property of aSky
object that iterates theirFall
methods and plots them on a display. Almost writes itself.Wouldnt you know? Theres a solution in one line of qs ancestor language APL. It illuminates how array programmers approach problems: thinking more about gross data structures than breaking the problem into small pieces.
Well start here by replicating the APL solution in q, then improving it a bit. Then well cut to a different approach entirely to add more features, and we shall all give silent thanks to the languages brevity.
The APL solution exploits its IDE, whose editor instantly reflects changes to a variable. Well use a browser to call q. Heres
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. Anadvance
function shifts the frame down and right and generates some more flakeslt
for the left and top. The HTTP GET callback.z.ph
advances the state and sends it to the browser as an HTMLpre
block.We can do better. Snowflakes dont 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 well now shift to a different model. Well 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@
Well move distant flakes less than near flakes, so we need a distance scale, and positions will be floats. The
Flakes
table start empty; globalFALL
specifies how many new flakes in each cycle andWIND
the horizontal wind speed. With a distance scaleTRIG
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 ameta
element in thehead
to autorefresh.
Over to you.
Log in to reply.