mpercivalxyz

this website!
Log | Files | Refs

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...)_