Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How does reduce/scan work in APL with user defined functions?

I'm trying to find the length of the longest unbroken chain of 1s in a boolean vector in APL. In Haskell, if I had a boolean list represented by 1s and 0s, I could do something like this:

Prelude> scanl (\acc x -> x*(acc+1)) 0 [0,0,1,1,0,1,1,1,1,0,1]
[0,0,0,1,2,0,1,2,3,4,0,1]

and then take a maximum.

I tried to do a similar thing in APL:

{⍵×⍺+1}\0 0 1 1 0 1 1 1 1 0 1
0 0 1 2 0 4 8 16 32 0 64

which is not at all the vector I expected. I had assumed the ⍺ would refer to the accumulator in the context of a scan/reduce, and the ⍵ would refer to the next element of the vector, but my understanding seems to be off quite a bit. These simple examples are confusing me as well:

{⍵}\1 1 1 1 1
1 1 1 1 1
{⍵+⍵}\1 1 1 1 1
1 2 4 8 16
{⍵×⍵}\1 1 1 1 1
1 1 1 1 1

Is it even possible (in practice) to use user-defined function with scan/reduce in APL? If so, how does it work, and what do ⍺ and ⍵ refer to?

like image 469
ackien Avatar asked Aug 02 '14 10:08

ackien


1 Answers

First, let's look at the basic questions. ⍺ and ⍵ are the left and right argument of an anonymous function:

      1 {⍺} 2
1
      1 {⍵} 2
2
      1 {⍺+⍵} 2
3

Reduce and scan can be used with user defined functions, and they often are. They work like this¹:

f/1 2 3 4 … ←→ 1 f 2 f 3 f 4 …
f\1 2 3 4 … ←→ (f/1) (f/1 2) (f/1 2 3) (f/1 2 3 4) …

Using these definitions, let's evaluate the examples that puzzled you:

{⍵}\1 1 1 1 1
({⍵}/1) ({⍵}/1 1) ({⍵}/1 1 1) … 
1 (1 {⍵} 1) (1 {⍵} (1 {⍵} 1)) …
1 1 (1 {⍵} 1) …
1 1 1 …

{⍵+⍵}\1 1 1 1 1
({⍵+⍵}/1) ({⍵+⍵}/1 1) ({⍵+⍵}/1 1 1) ({⍵+⍵}/1 1 1 1) …
1 (1 {⍵+⍵} 1) (1 {⍵+⍵} (1 {⍵+⍵} 1)) (1 {⍵+⍵} (1 {⍵+⍵} (1 {⍵+⍵} 1))) …
1 (1+1) (1 {⍵+⍵} (1+1)) (1 {⍵+⍵} (1 {⍵+⍵} (1+1))) …
1 2 (1 {⍵+⍵} 2) (1 {⍵+⍵} (1 {⍵+⍵} 2)) …
1 2 (2+2) (1 {⍵+⍵} 4) …
1 2 4 (4+4) …
1 2 4 8 …

{⍵×⍵}\1 1 1 1 1
…

The important thing to notice here is that, according to the usual APL evaluation rules, each reduction is done from right to left. Compare this to Haskell's scanl, which returns a list of successive reduced values, where each reduction is done from from left to right:

scanl f z [x1,x2,…] == [z,z `f` x1,(z `f` x1) `f` x2,…]

So, evaluating the second example using scanl we get:

scanl (\x y -> y+y) 1 [1,1,1,1,1]
[1,(\x y -> y+y) 1 1,(\x y -> y+y) ((\x y -> y+y) 1 1) 1,…]
[1,1+1,(\x y -> y+y) 1+1,…]
[1,2,(\x y -> y+y) 2 1,…]
[1,2,1+1,…]
[1,2,2,…]

Haskell's scanr1, the right to left dual of scanl1, works similar to APL's scan. (The functions ending in 1 only differ in that they don't need a starting value):

scanr1 (\x y -> y+y) [1,1,1,1,1]
[…,(\x y -> y+y) 1 ((\x y -> y+y) 1 1),(\x y -> y+y) 1 1,1]
[…,(\x y -> y+y) 1 (1+1),1+1,1]
[…,(\x y -> y+y) 1 2,2,1]
[…,2+2,2,1]
[…,4,2,1]

What APL's scan does is actually something between the functionality of scanl and scanr. While the reductions themselves are done from right to left, as in scanr, it returns the intermediate reductions starting from the left, like scanl:

f\1 2 3 4 ←→ 1 (1 f 2) (1 f (2 f 3)) (1 f (2 f (3 f 4)))

Now, it should be clear what happens when we evaluate your attempted solution:

{⍵×⍺+1}\0 0 1 1 0 1 1 1 1 0 1
({⍵×⍺+1}/0) ({⍵×⍺+1}/0 0) ({⍵×⍺+1}/0 0 1) ({⍵×⍺+1}/0 0 1 1) …
0 (0 {⍵×⍺+1} 0) (0 {⍵×⍺+1} (0 {⍵×⍺+1} 1)) (0 {⍵×⍺+1} (0 {⍵×⍺+1} (1 {⍵×⍺+1} 1))) …
0 (0×1) (0 {⍵×⍺+1} (1×1)) (0 {⍵×⍺+1} (0 {⍵×⍺+1} (1×2))) …
0 0 (0 {⍵×⍺+1} 1) (0 {⍵×⍺+1} (0 {⍵×⍺+1} 2)) …
0 0 (1×1) (0 {⍵×⍺+1} (1×2)) …
0 0 1 (0 {⍵×⍺+1} 2) …
0 0 1 (1×2) …
0 0 1 2 …

Concerning what you were actually trying to do, there are of course many solutions. Here is the first I came up with (v being the vector of zeros and ones):

⌈/¯1+2-⍨/(v=0)/⍳⍴v←0,v,0

As the answer is already pretty long, I'll leave analyzing it as an exercise. As I said, it's just the first thing that came to my mind and uses a different approach from yours. I'm sure that some more experienced APLer will come up with a better and more elegant solution.

Edit: And here is a solution that is as close to your original approach as I could get. For some reason, I couldn't get it to work with GNU APL, which is the implementation I normally use, so it took me a little longer. It might be a bug, but I don't know. The GNU APL developers are unfortunally not very fond of language extensions like dynamic functions, so it might be on purpose. I'll look into it when I get the time. It worked for me in NGN and Dyalog. Maybe it could be further improved, but it's pretty much what you wanted to do:

      v ← 0 0 1 1 0 1 1 1 1 0 1
0 0 1 1 0 1 1 1 1 0 1
      {⍺×⍵+1}/¨⌽¨,\v
0 0 1 2 0 1 2 3 4 0 1
      ⌈/{⍺×⍵+1}/¨⌽¨,\v
4

(Edit: As Elias points out in a comment below, you can get this to work in GNU APL by wrapping the reduction in an anonymous function: {{⍺×⍵+1}/⍵}¨⌽¨,\v.)

Second edit: This is probably the most elegant solution I can come up with:

      v ← 0 0 1 1 0 1 1 1 1 0 1
      ⌈/∊⍴¨⊂⍨v
4

(Also, note that there may be some errors in the above evaluations. Computers are much better at such tedious jobs than humans. Anyway, the gist should be clear.)

¹ Actually, this is an oversimplification. This explanation uses the so called insert reduction style. Implementations may also use an enclose reduction style, which leads to subtle differences. Also, this is the naive, O(n²), definition of scan. Implementations will usually use more efficient implementations for associative functions.

like image 167
danlei Avatar answered Nov 29 '22 00:11

danlei