5

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?

danlei
  • 14,121
  • 5
  • 58
  • 82
ackien
  • 521
  • 1
  • 5
  • 10

2 Answers2

6

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.

danlei
  • 14,121
  • 5
  • 58
  • 82
  • 1
    Your problem with is caused by GNU APL not having support for the point-free style. You can work around it like this: `{{⍺×⍵+1}/⍵}¨⌽¨,\v` – Elias Mårtenson Aug 06 '14 at 07:17
  • Thanks for pointing this out, @Elias. I'll add it to the answer. I assumed it would be something along those lines, but I wasn't quite sure. I've already pointed out my position concerning proper lexically scoped lambdas and tacit programming in a few lenghty posts to the mailing list some time ago. While I'm glad and grateful that there's now a free APL2 implementation, I'm also a little disappointed that there is no interest in implementing these modern extensions that fit the APL programming style so well. – danlei Aug 06 '14 at 18:46
  • I wouldn't be too sure that'll never happen. It has been pointed out that extensions will be considered once the core APL implementation can be considered "complete". – Elias Mårtenson Aug 07 '14 at 06:21
  • Well, while I'm sure that *some* extensions will happen, I rather had the impression that there is no interest in implementing lexically scoped lambdas (which I consider pretty essential) or other modern FP features like tacit programming. This is, however, not the right place to go into details about this, especially in Jürgen's and the other ML member's absence. I sure hope you're right, but I don't hold my breath for it. – danlei Aug 07 '14 at 08:21
0

For an old school APLer, this problem looks very much like partitioned plus reduction. The partitioning technique was used heavily before the advent and onset of nested arrays, until around the late 1980s. For more information, see

www.sudleyplace.com/APL/boolean.pdf (p. 10 for pPLRED and p.15 for its helper function N-delta)

and

http://www.chilton.com/~jimw/apl85.html (search for pPLUSRED)

Lobachevsky
  • 1,222
  • 9
  • 17