文章

APL運算子

圖片
在前幾篇文章之中的例子都有使用或者提及 APL運算子(operator),用的時候都沒有好好解說。 主流語言中,運算子一般作用在變數或值之上,以導出新的值。 舉例來說,Java 語言之中有數學運算子、邏輯運算子等等。以邏輯運算子為例,表達式 A != B 中使用了 !=  邏輯運算子,假如變數 A 及 B  的值分別為  1  及  2, 那麼表達式即可被演繹為 1 != 2 ,繼而導出新的布林值 true 。 APL 之中,運算子並不作用於變數或值,而是函數之上。所返回的是導來函數 (derived function)。以運算子建立的新函數,使用方法與內建函數無異。 舉一個簡單的實例,用 reduce 運算子 / 配合內建函數  + ,產生新的函數 sum : +/  於之前的帖文中出現過好幾次,相信不難理解。另一個頗為有趣的 commute 運算子  ⍨ ,將函數的左、右參數對調: F⍨ 的作用就好像導來函數  {⍵ F ⍺} 一樣,將自身的左、右參數對調 (即 commute 的本義) 然後輸入至函數 F 。 再介紹另一個極為強大的 APL 運算子: ⍣  (power)。好似下面例子: ⍣ 運算子的左引數為函數 f ,右引數為整數 10 ,則產生呼叫 f  十次的導來函數。有了 ⍣ 運算子,即時不使用程序語言的 while 迴圈也能做到迭代 (iteration) 的效果。以上例子就好比 C 語言中的  int x = 5; for (int k = 0; k < 10; ++k) { x += 1; } (Power 運算子的另一個稍為進階的使用例子,可以看看 前一篇文章 ) 作為運算子的初步簡介,最後介紹極為常用的  ∘.  外積運算子 (outer product)。配合 ×(乘積)函數說明的話,概念上外積就好像小學生的數學乘法表一樣,將 A 、 B 兩列數分別橫、直排列,再以乘法將兩者的元素結合: ...

Let's Split!

圖片
這是2020年度的Dyalog APL的其中一道題目在,此分享一下解答過程的思考歷程。 先看看題目: 簡單解說一下:寫出函數 F,右引數 Y 可以為向量或陣列,左引數 X 為任何非零的數字,任何情況下返回一個陣列 C。當 X 為正數時,C 的第一個元素包含 Y 的首 X 個元素,C 的第二個因素為餘下之 Y 的元素;當 X 為負數時,C 的第一個元素包含 Y 的尾 X 個元素,C 的第二個因素為餘下之 Y 的元素。 好似越講越複雜,還是看看上圖的示例比較直觀,反正就係 "split" 嘛。 過往,要不使用 if-else 邏輯的情況下同時處理向量、陣列輸入會有點困難。不過題目裏面貼心地提示可以使用 ↑ (take) 函數。而 ↑ 函數正好可以"乾淨地"解決這問題。示範一下 ↑、↓ 的用法: 3 ↑ Y 意思即從 Y 中提取 (take) 頭3個元素。當左引數為負數時,將從 Y 的末端取出相應數量元素: 而另一個相反效應的函數係 ↓ (drop),果效正如其名字: 以同一個數字 X 同時 take、drop 同一個陣列,就可達到分開陣列的效果: 不過題目還要求左引數為負數時,末段的元素要放置在輸出的前方。抽象一點理解,即係有條件地反轉上面輸出的陣列的兩個元素。於是之前介紹過的 ⍣ (power) 運算子便派上用場,用於有條件地呼叫 ⊖ (rotate) 函數一次: 由於 take、drop 本身可以處理向量及陣列輸入,上面的表達式同樣可以正確分隔向量成為單一因素的陣列,以及一個空陣列(否則過不了 Dyalog 網頁的 test case): 綜合上述後的答案是: {(⊖⍣(⍺<0)) (⍺↑⍵) (⍺↓⍵)}

簡單比較APL與Javascript

圖片
網上搜尋『陣列』的編程問題時,見到 Mozilla Developer Network 有Javascript的陣列操作例子。於是思考,用APL寫的話又會如何?以下係兩種語言的比較,其中Javascript的程式碼係直接取自MDN網頁: 陣列建構 &sol;&sol; Javascript let fruits = ['Apple', 'Banana'] ⍝ APL fruits←'Apple' 'Banana' 陣列索引 &sol;&sol; Javascript let first = fruits[0] ⍝ APL first←fruits[1] 另外在APL之中,也可以用函數 ⌷ 提取陣列元素: ⍝ APL first←1⌷fruits 以迴圈列印陣列元素 &sol;&sol; 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列的二維陣列,顯示出來便有垂直排列效果: 加入新元素至陣列末端 &sol;&sol; Javascript let newLength = fruits.push('Orange') APL newLength←≢fruits←fruits,⊂'Orange' 需要解釋一下APL語句...

Project Euler第八題

圖片
又係編程解題,先看題目: 題目出自 Project Euler第八題 ,要求找出以上1000位數字之中,任何連續13個數字的乘積的最大值。 這條題目早於2017年就已經解答過並提交了正確的答案。當時的APL程式: 毋庸置疑是一場災難,非常驚嘆自己可以寫出這樣笨的程式,也許出於當時對陣列編程的陌生吧。加上劣拙的英語註解,整片程式與一塊廢鐵無異。當時的思路大致如下: 將1000位數字以字串C儲存 字串C轉化位1000個元素的陣列D,D的每個元素相當於數字的每個位數 將陣列D轉化為12個76行 x 13列的二維陣列(因為1000可以被13整除76次) 再將二維陣列的每一行中的13個位數相乘 最大的乘積即為答案 時值2020年,再看到同一條題目,我想到的答案卻是: 程式碼可以大量簡化是因為當中使用了" / "運算子。在雙價 (dyadic) 使用下," / "運算子將左面的函數安插在右面陣列的元素之中,即係函式語言中的還原 (reduction): 以上的表達式,執行起來就好似以下的一樣: 由此可見使用" / "運算子可以大幅簡化程式碼之餘,卻不會犧牲可讀性。更強大的功能係,在函數左方加入數字引數,可以達致區間還原 (partitioned reduction) 如下: 第一個表達式之中,函數  +  並沒有用作還原整條陣列,而是對每三個順序元素還原一次,區間大小由左引數3決定,亦是題解  ⌈/13×/⍎¨C 中  13 ...

以APL計算黃金比例

圖片
APL 語言之中,有一個運算子"power"可以幫助計算數學中的黃金比例。 APL 表達式幾乎與數學定義一樣漂亮、簡潔: 沒錯,只有7個 unicode 字元的表達式,便計算出黃金比例。對數論稍有認識的同仁,也許知道黃金比例的連續分數是: (圖片連結自 維基百科 ) 其定義不太明顯地使用了遞迴,以一般編程語言表現的話大概離不開: def phi(N): return 1 + (1 % phi(N)) end 根據數論,無理數只能以無限連續分數表達,即上面圖示中的 "..." 應該無窮盡地延伸下去,才為之正確的黃金比例。而上述函數定義亦導致無限遞迴,定義上雖然沒有錯,但運作上會導致 stack overflow,很難說是正確的程式。 那麼,APL 是如何僅憑十個字元計算出黃金比例呢?或者首先應該問:APL 返回的值正確嗎?以下係 Wolfram 所給的黃金比例的小數約數: 1.618033988749894848204586834365638117720309179805762862135... 而 APL 給出的值(使用 NARS2000 實作): 1.618033988749894 可見小數點後16個位被截斷 (truncate) 了,而小數點後1至15位都脗合。先撇除電腦表達、顯示浮點值的複雜問題, 1.618033988749894應該是可接受的約數。 下一個問題:APL 表達式是如何達致遞迴?秘訣在於 ⍣ 稱呼作"power"的運算子。 表達式 {1+1÷⍵}⍣≡1 中, ⍣ 的作用是 呼叫 {1+1÷⍵} 1 結果為整數2,與原先的參數(整數1)以函數 ≡ 比較 2≡1 返回整數0亦即布林值false 如果上一步得出結果為布林值true,則以整數2為答案,否則繼續下一步 返回第一步,以整數2作為輸入 也許將 ≡ 函數替換成先把左、右參數列印的匿名函數會比較容易理解: ...

APL Code Golfing

圖片
APL實在是code golfing的利器。 今日突然想練一練code golf,馬上到 code-golf.io 看看,見到一道列印首二十行 Pascal Triangle 的題目,而排行第一的挑戰者居然使用了52個字元的答案。直覺告訴我APL應該可以更精煉,於是決定試試挑戰。 先公佈我的題解,只需要26個字元: 題目比預期中難,用了差不多半個小時仍然處理不好base case,即單一元素的陣列{1}。按下面來自維基的示範,當輸入為陣列V = {1, 3, 3, 1},應先將第1、2個元素相加並得出新元素1 + 3 = 4,如是者再重複相加第2、3及3、4個元素,得出新元素3 + 3 = 6與3 + 1 = 4: (圖片鏈結自 維基百科 ) 於是問題便轉化為產生陣列{1, 3, 3, 1}的指標的二元組(2-tuple) {1, 2},{2, 3},{3, 4}。用以存取陣列V的元素,並相加起來: 再費一些功夫抽取對角線上的元素{4, 6, 4}便可以了,感覺好像快將完成解答。 幾個想法突然湧現: 這樣的程式碼只是將程序式編程(procedural programming)的思維照搬不誤 程式碼極為欠缺美感,且勢必多於52個字元 致命的是,當陣列V為{1}的時候,會引致錯誤 原因係當V為{1}時, ⍴(¯1↓⍳⍴V) (1↓⍳⍴V) 的外積(outer product)是空的二維陣列,導致匿名函數  {+/V[⍺ ⍵]} 發生錯誤。縱使以常理推斷,陣列中沒有任何元素的時候,前述的函數應該不會被呼叫才對。例如python之中將必然拋出錯誤的lambda用於空陣列上: 再仔細思考,輸入陣列{1, 3, 3, 1}時只需要將輸入去掉頭、尾 (即{1, 3, 3}與{3, 3, 1})相加便可得出{4, 6, 4},再無條件地將{1}置於{4, 6, 4}的頭、尾,就可以得出Pascal Triangle的第五行{1, 4, 6, 4, 1}。 直觀地翻譯為APL,就得出題解使用的 匿名函數  {1,⍨1,(¯1↓⍵)+1↓⎕←⍵} ,最重要係當輸入為純量1或者陣列{1}時,輸出都會係{1, 1}。 不太理解為何其他參與者不用 J語言...

以APL模擬大話骰

圖片
上星期同幾個朋友去酒吧玩樂,興之所至玩起經典遊戲“大話骰”來。玩家總共有五人,其中一個朋友起手便叫“七個二”,雖則一點可以用作“百搭”,但酒後的直覺告訴我“七個二”應該是很進取的開局。於是我便要求開了。 結果我輸了。 事後我決定找出五人大話骰中“七個二”的概率,但我的所有概率知識都已經還給老師了,怎麼辦? 想了一會,用電腦模擬擲骰好了,省力不費時。用甚麼語言?除了APL,不作他選。 每局開始,5個人分別擲5粒骰,即係產生25個1至6的亂數: 每次模擬一局未免太沒有效率,但為了方便示範,暫時模擬五局好了: 用 二維陣列 裝載擲骰結果,每一“橫行”代表一局。先用變數D暫存結果,方便核對。 現在數算每局中有多少個一(“百搭”)或二點: 由於只模擬了五局,用人眼便可以印證以上結果正確。 那麼五局之中,有多少局出現等於、多於七個二? 所有結果都有多於、等於“七個二”。或者係運氣使然,所以才會完敗?按簡單機率理論,增加試驗次數 (trial) 應該會使結果趨向期望值 (expected value)。只要將變數D替換成很多局的結果便可以: 試驗一萬及十萬次,多於、等於“七個二”的概率分別得出78.47%和77.946%。 難怪我會壯烈犧牲。