簡單比較APL與Javascript

網上搜尋『陣列』的編程問題時,見到Mozilla Developer Network有Javascript的陣列操作例子。於是思考,用APL寫的話又會如何?以下係兩種語言的比較,其中Javascript的程式碼係直接取自MDN網頁:

陣列建構

// Javascript
      let fruits = ['Apple', 'Banana']
⍝ APL
      fruits←'Apple' 'Banana'

陣列索引

// Javascript
      let first = fruits[0]
⍝ APL
      first←fruits[1]

另外在APL之中,也可以用函數 提取陣列元素:

⍝ APL
      first←1⌷fruits

以迴圈列印陣列元素

// Javascript
      fruits.forEach(function(item, index, array) {
        console.log(item, index)
      })
⍝ APL
      {⎕←fruits[⍵],⍵}¨⍳⍴fruits
⍝ APL again
      ⍪fruits,¨(⍳⍴fruits)

一如其他直譯式語言,在APL輸入表達式後都會列印出表達式的值。所以我提供第二個簡潔一點方案,即係將 fruits⍳⍴fruits 產生的指數陣列以函數 逐個配對,並以函數 (1st axis catenate) 轉化為2行 x 1列的二維陣列,顯示出來便有垂直排列效果:

加入新元素至陣列末端

// Javascript
      let newLength = fruits.push('Orange')
APL
      newLength←≢fruits←fruits,⊂'Orange'

需要解釋一下APL語句的原理。執行此語句前,陣列 fruits 有兩個元素 "Apple" 及 "Banana"。要留意的是,"Apple" 及 "Banana" 本身就係由英文字母組成的陣列。作為陣列元素,它們被 "封裝" 成為APL的向量 (scalar),因此若以索引存取 fruits 的元素,所得的係被封裝後、維度係零的一個 "盒子",盒子內就是字串:

存取字串就需要使用函數 (disclose) 將 "盒子" 解封:

所以新的元素 "Orange" 需要先被函數 (enclose) 封裝後,再與原先的 fruits 連結。函數 (tally) 的功能自然係找出新的 fruits 的元素數量,即長度。

去掉最後一個元素

// Javascript
      let last = fruits.pop()
⍝ APL
      last←¯1↑fruits ⋄ fruits←¯1↓fruits

函數 (drop) 從陣列移除元素,負數1代表從末端移除一個因素。因為 ¯1↓fruits 只回傳新的陣列而非被 "pop" 走的元素,這裡先以 ¯1↑fruits 提取、保存最後一個元素,再更新 fruits。 "" 係分隔陳述句的符號。

去掉第一個元素

// Javascript
      let first = fruits.shift()
⍝ APL
      first←1↓fruits ⋄ fruits←1↓fruits

與上一例雷同。

在陣列前端加入元素

// Javascript
      let newLength = fruits.unshift('Strawberry')
⍝ APL
      newLength←≢fruits←(⊂'Strawberry'), fruits

與加入新元素至陣列末端一樣,"Strawberry" 需要先被封裝,否則結果將會變成:

而非:

找出元素在陣列中的索引 (位置)

// Javascript
      let pos = fruits.indexOf('Banana')
⍝ APL
      pos←fruits⍳⊂'Banana'

函數 意思係index-of,同樣地,要搜尋的陣列元素係 ⊂'Banana'。假如去除 會導致甚麼的結果?姑且看看:

(index-of) 被輸入了陣列 "Banana",並會回傳 "Banana" 的元素在 fruits 之中的位置。顯然,字母 B、n、a 均不是 fruits 的元素,所以 回傳了索引陣列 [3, 3, 3, 3, 3, 3, 3],而3大於 fruits 的長度,表示遍尋不獲。

移除指定位置的元素

假定 fruits = ["Strawberry", "Banana", "Mango"],以Javascript 移除索引為1的 "Banana":

// Javascript
      let removedItem = fruits.splice(pos, 1)
⍝ APL
      fruits←((⍳⍴fruits)≠fruits⍳⊂'Banana')/fruits

解釋APL的運作需要更長的敘述。根據上一例可知 fruits⍳⊂'Banana' 回傳 "Banana" 的位置,即數字2 (APL的陣列索引始於1而非0)。⍳⍴fruits 產生一條與 fruits 長度相同的指數陣列 [1, 2, 3],函數 將之與2比較,得出布林 (boolean) 陣列 [1, 0, 1]。通過函數 "/" 用布林陣列篩選 fruits,餘下元素 ["Strawberry", "Manago],並重新指定至 fruits

從指定位置移除N個元素

假定 vegetables = ['Cabbage', 'Turnip', 'Radish', 'Carrot'],從位置1移除兩個元素:

// Javascript
      let pos = 1
      let n = 2
      let removedItems = vegetables.splice(pos, n)
      console.log(vegetables)
      // ["Cabbage", "Carrot"] (the original array is changed)
⍝ APL
      vegetables←vegetables[(⍳⍴vegetables)~1+⍳2]

(⍳⍴vegetables) 產生索引陣列,而 1+⍳2 產生數字陣列 [2, 3],並將之從 (⍳⍴vegetables) 中移除。得出陣列 [1, 4],以此達致 Javascript splice 的效果。

深複製 (Deep copy)

這裡恐怕MDN也犯了錯誤,意思應該係 deep copy 而非變量名稱所指的 shallow copy。

// Javascript
      let shallowCopy = fruits.slice()
⍝ APL
      deepCopy←fruits

APL的變量指定只有值的指定,一般情況下並沒有 shallow copy,部分支援物件編程的實作除外。

至此完成了MDN上"基本操作"的部分。

留言

這個網誌中的熱門文章

APL Code Golfing

Project Euler第八題

Dyalog APL比賽-2019年-Phase 1-第2題