Mastering Dyalog APL (2009) [pdf]
dyalog.com
dyalog.com
In researching an upcoming blog post on monoids, I tried implementing the problem in Guy Steele's "Four Solutions to a Trivial Problem" talk[2] in APL (actually I tried GNU APL for this, because I didn't need anything fancy). This is what I came up with:
+/((⌽⌈\⌽a)⌊⌈\a)-a
That in turn should be fully parallelizable and run fast on the GPU, as it's based on a couple scan (generalized prefix sum) operations.There's a considerable amount of DNA from APL in modern compiler-based machine learning frameworks, mostly by way of numpy. Dr. Hsu makes a good case that APL has quite a bit of power which is considerably diluted in later spins such as numpy. I'm especially fond of the power (⍣) operator[3], which represents parallel iteration and I think can often be translated to a per-thread while or for loop in a compute shader. I don't believe numpy has anything quite like that (but feel free to correct me!).
[1]: https://www.arraycast.com/episodes/episode19-aaron-hsu
[2]: https://www.youtube.com/watch?v=ftcIcn8AmSY
[3]: https://help.dyalog.com/15.0/Content/Language/Primitive%20Op...
Power is for sequential iteration. You may be thinking of rank (⍤).
]boxing on
Was OFF
(⊂⍣5) 2 3 4
┌─────────────┐
│┌───────────┐│
││┌─────────┐││
│││┌───────┐│││
││││┌─────┐││││
│││││2 3 4│││││
││││└─────┘││││
│││└───────┘│││
││└─────────┘││
│└───────────┘│
└─────────────┘
(On GNU APL, it looks like the relevant formulation is ']boxing 3' rather than ']boxing on'; this simply affects the display of nested arrays.)(You can do f⍣g⍤n, but then, again, that is rank doing the work of parallelization. And, performance considerations aside, I am no longer so convinced of the aesthetic utility of ⍤ as I once was. Wrt performance, when targeting CPUs, where branches are not so cheap, it might be worthwhile to design an f which can tolerate being called overly many times. Then give y an extraneous trailing axis the same size as a simd lane and do (f⍣g⍤1) y. That permits the whole computation to be vectorised, though it is also somewhat brittle.)
(Note ≡ is not the same as =. 1 1 1 ←→ 1 2 3 = 1 2 3, but 1 ←→ 1 2 3 ≡ 1 2 3. f⍣≡ is an idiom. f⍣= is meaningless except at rank 0, where is behaves the same as f⍣≡.)
If my (newly refreshed) understanding is correct, you can implement f⍣≡ as a per-thread fixpoint if you know that f is elementwise independent, which it seems like you'd be able to determine at least some of the time by static analysis. But I can also see that would be brittle, and if you fell back to do a dispatch per iteration, performance would tank.
Hmm, yeah, that would work. Though you would also need to account for side effects in f.
> semantics line up somewhat with GPU-friendly parallelism, but not entirely
An APL machine was proposed[0]. I say it is the GPUs that are wrong! :)
https://www.slac.stanford.edu/pubs/slacreports/reports07/sla...
The functions ⌊ ⌈ find min and max values, and \ is functional programming scan operator. Combining ⌈\ gives max-scan which does this:
twentyRandoms
71 61 79 72 84 76 76 88 16 53 51 56 64 50 100 33 60 82 96 53
⌈\twentyRandoms
71 71 79 79 84 84 84 88 88 88 88 88 88 88 100 100 100 100 100 100
moving to the right, the largest value (max) seen so far is kept and carried on.Then ⌽ reverses the values in an array so this (⌽⌈\⌽a) does the same thing going from right to left, by reversing, max-scan, then reversing the result:
twentyRandoms
71 61 79 72 84 76 76 88 16 53 51 56 64 50 100 33 60 82 96 53
(⌽⌈\⌽twentyRandoms)
100 100 100 100 100 100 100 100 100 100 100 100 100 100 100 96 96 96 96 53
Then the ⌊ in the middle compares the two results an item at a time, taking the minimum of each pair: (⌽⌈\⌽twentyRandoms)⌊⌈\twentyRandoms
71 71 79 79 84 84 84 88 88 88 88 88 88 88 100 96 96 96 96 53
The (...a) - a part does item by item subtraction, each number in this result array minus the number in that position from the original array. ((⌽⌈\⌽twentyRandoms)⌊⌈\twentyRandoms)-twentyRandoms
0 10 0 7 0 8 8 0 72 35 37 32 24 38 0 63 36 14 0 0
Then +/ sums them up like a sum() or foldr or reduce: +/((⌽⌈\⌽twentyRandoms)⌊⌈\twentyRandoms)-twentyRandoms
384
Which, if I remember the talk and the code works, means that if you drew this array as a bar graph and poured water into the top middle of it, how much water would it hold before it all overflowed? The highest block on the left and the highest block on the right are the bounds of the highest points where the water can't get past. In between those, the bar heights are the depth at each position, and the water depth is from there up to the lower of the highest bar.Am I way off base? Maybe there are other meanings to the character I'm unaware of.
Oh boy, I had a mind-melting moment with _inverse_ (⍣¯1).
Aaron helped me understand what was going on. He wrote about "Decoding Inverses" here: https://www.sacrideo.us/decoding-inverses/
And that took me from a clunky solution to Dismal Arithmetic (addition):
{10(⊤⍣¯1)⍵}∘{⌈/⍵}∘{10(⊥⍣¯1)⍵}⊢ 100000 10000 1000 100 10 1
111111
To this little gem: da ← 10⊥(⌈/10⊥⍣¯1⊢)
da 169 248
269
(Edit: add code example, fix language)It is a great episode and talks a lot about some interesting history.
The oddest thing about the language was that I started dreaming about it almost immediately after beginning to learn it. It really sticks in your mind.
I spent a lot of time on https://tryapl.org/ before eventually moving on to running it on a raspberry pi ~10 years ago with my family TV as a monitor.
A really great way to spend an evening or two. The syntax is wild in retrospect, and I cannot see where it could be used in a production environment today (if anyone knows, please tell me!) but it holds a nostalgic place for me :)