dyalog-scan.md (6410B)
1 --- 2 title: "Is dyalog APL's Scan "broken"?" 3 date: 2023-02-22T19:57:02Z 4 draft: false 5 --- 6 7 I saw [this](https://twitter.com/code_report/status/1628131083760898052/photo/1) recent tweet by Conor Hoekstra, which has some artistic code comparisons. 8 9 The problem he gives is to write a function that can tell whether there are at least 3 consecutive odd numbers in a given array. 10 11 He broadly compares three algorithmic solutions across a few different languages: 12 13 {{< img src="/assets/three-odd-ints.png" width="900" height="480">}} 14 The image highlights a few questions that I'd like to touch upon, namely: 15 16 - what's up with BQN partitioning? 17 - is APL's scan really broken? 18 - (sorry q, perhaps I'll learn about you another time.) 19 20 --- 21 22 So, why is writing a "ChunkBy" algorithm harder in BQN than APL? 23 24 The main difference is in the following glyphs: APL has a dedicated '[partition](http://help.dyalog.com/latest/index.htm#Language/Primitive%20Functions/Partition.htm)' function (dyadic 25 {{< highlight APL "hl_inline=true, style=catppuccin-mocha" >}} 26 ⊆ 27 {{< /highlight >}}), whereas BQN has the more generalised '[group](https://mlochbaum.github.io/BQN/help/groupindices_group.html)' 28 {{< highlight BQN "hl_inline=true, style=catppuccin-mocha" >}} 29 (⊔) 30 {{< /highlight >}}. 31 32 Group has a bit of a learning curve to it, but is the more [flexible](https://mlochbaum.github.io/BQN/commentary/problems.html#how-to-choose-a-partitioning-function) of the two primitives; the trade-off is that partitions in BQN have to be done [idiomatically](https://mlochbaum.github.io/BQN/doc/group.html#partitioning) rather than as a single glyph. 33 34 Here is a potential BQN [partition](https://mlochbaum.github.io/bqncrate/?q=Cut%20slice#) solution to Conor's problem: 35 36 {{< highlight BQN "linenos=true, style=catppuccin-mocha" >}} 37 F ← { 3 ≤ ⌈´≠¨(¬-˜⊢×·+`»⊸>)⊸⊔ 2 | 𝕩 } 38 39 F ⟨1,2,3,4,5,6⟩ 40 # 0 41 F ⟨1,2,3,7,5,6⟩ 42 # 1 43 {{< /highlight >}} 44 45 Or, [alternatively](https://mlochbaum.github.io/bqncrate/?q=Split%20the%20removing#): 46 47 {{< highlight BQN "linenos=true, style=catppuccin-mocha" >}} 48 G ← { 3 ≤ ⌈´≠¨ 0 ((⊢-˜+`׬)∘=⊔⊢) 2 | 𝕩 } 49 50 G ⟨1,2,3,4,5,6⟩ 51 # 0 52 G ⟨1,2,3,7,5,6⟩ 53 # 1 54 {{< /highlight >}} 55 56 These both look overly cumbersome, and well, they are. 57 58 However, I think there is a hidden clue as to why: within each partition function is a plus scan 59 {{< highlight APL "hl_inline=true, style=catppuccin-mocha" >}} 60 (+`) 61 {{< /highlight >}}, and isn't that already the basis for the first algorithm? 62 63 I would take that as a hint from the cosmos that partitioning is not the highly baconic approach here, and to instead go with either a scan or windowed reduction. 64 65 --- 66 67 The second curiosity is the missing APL solution in the above image. Is APL's '[scan](http://help.dyalog.com/latest/index.htm#Language/Primitive%20Operators/Scan.htm)' really broken? That sounds like a rather damning indictment. 68 69 Here is what a plus scan looks like: 70 71 {{< highlight APL "linenos=true, style=catppuccin-mocha" >}} 72 + 3 6 1 8 5 73 ⍝ 3 9 10 18 23 74 {{< /highlight >}} 75 76 Which seems fine at first, but once you start plugging in _non-commutative_ functions e.g. subtraction it starts to get weirder. 77 78 This example from [Mastering Dyalog APL](https://www.dyalog.com/uploads/documents/MasteringDyalogAPL.pdf#page=406) highlights the problem: 79 80 {{< highlight APL "linenos=true, style=catppuccin-mocha" >}} 81 - 3 6 1 8 5 82 ⍝ 3 ¯3 ¯2 ¯10 ¯5 83 {{< /highlight >}} 84 85 Scan in APL is applied left-to-right, so why do we not instead get 86 {{< highlight APL "hl_inline=true, style=catppuccin-mocha" >}} 87 (3 ¯3 ¯4 ¯12 ¯17) 88 {{< /highlight >}}? 89 90 The missing ingredient is that each intermediate is really a minus reduction 91 {{< highlight APL "hl_inline=true, style=catppuccin-mocha" >}} 92 (-/) 93 {{< /highlight >}} of the elements up to n, and reductions in APL are evaluated _right-to-left_. Therein lies the problem! 94 95 For example, applying a minus _reduction_ to the first four elements yields the fourth element of the minus scan: 96 {{< highlight APL "linenos=true, style=catppuccin-mocha" >}} 97 -/ 3 6 1 8 ⍝ i.e. (3-(6-(1-8))) 98 ⍝ ¯10 99 {{< /highlight >}} 100 101 So it works as intended, albeit with a slightly odd design. 102 103 I think that the perceived "brokenness" with APL scan is more that it doesn't behave in line with other languages. 104 105 Take BQN for example: 106 107 {{< highlight BQN "linenos=true, style=catppuccin-mocha" >}} 108 -` ⟨3,6,1,8,5⟩ 109 #⟨ 3 ¯3 ¯4 ¯12 ¯17 ⟩ 110 {{< /highlight >}} 111 112 BQN also scans left-to-right, but each intermediate is instead equivalent to a _left_ fold: 113 114 {{< highlight BQN "linenos=true, style=catppuccin-mocha" >}} 115 -˜´⌽ ⟨3,6,1,8,5⟩ # equivalent to ((((3-6)-1)-8)-5) 116 # ¯17 117 {{< /highlight >}} 118 119 Whilst this is slightly at odds with 120 {{< highlight BQN "hl_inline=true, style=catppuccin-mocha" >}} 121 ´ 122 {{< /highlight >}} 123 being a right fold in BQN (left fold is 124 {{< highlight BQN "hl_inline=true, style=catppuccin-mocha" >}} 125 𝕗˜´⌽ 126 {{< /highlight >}}), 127 I think that this is still the more intuitive scan primitive, so I do sympathise with Conor's assertion. 128 129 As another example, Haskell also agrees with this left scan definition: 130 131 {{< highlight haskell "linenos=true, style=catppuccin-mocha" >}} 132 ghci> scanl1 (-) [3,6,1,8,5] 133 -- [3,-3,-4,-12,-17] 134 {{< /highlight >}} 135 136 I had a go at unrolling the APL behaviour to create a left scan in line with Haskell and BQN, which resulted in the following mess: 137 138 {{< highlight APL "linenos=true, style=catppuccin-mocha" >}} 139 -⍨/∘⌽¨, 3 6 1 8 5 140 ⍝ 3 ¯3 ¯4 ¯12 ¯17 141 {{< /highlight >}} 142 143 This is taking the [prefixes](https://aplcart.info/?q=Prefixes%20of%20a%20vector) by doing a join scan 144 {{< highlight APL "hl_inline=true, style=catppuccin-mocha" >}} 145 (,) 146 {{< /highlight >}} 147 and then composing a minus 'left' reduction 148 {{< highlight APL "hl_inline=true, style=catppuccin-mocha" >}} 149 (-⍨/∘⌽) 150 {{< /highlight >}} 151 for each 152 {{< highlight APL "hl_inline=true, style=catppuccin-mocha" >}} 153 (¨) 154 {{< /highlight >}} 155 prefix (yes, I am trying to sound smart). 156 157 An APL 'scan' solution to Conor's problem could look something like the following: 158 {{< highlight APL "linenos=true, style=catppuccin-mocha" >}} 159 {3≤⌈/(⊢×+)⍨/∘⌽¨,2|⍵} 1 2 3 4 5 6 160 ⍝ 0 161 {3≤⌈/(⊢×+)⍨/∘⌽¨,2|⍵} 1 2 3 7 5 6 162 ⍝ 1 163 {{< /highlight >}} 164 165 It works, but perhaps the cosmos are whispering something about APL's scan here too. 166 167 _(...iiitttsss bbbrrroookkkeeennn...)_