發表文章

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  的意義。最後用  ⌈  (max) 選出最大的乘積同樣仰賴&quo

以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%。 難怪我會壯烈犧牲。

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

圖片
先看看題目要求: 這是相對容易的一條題目吧,以下用 NARS2000 解題。 題目要求寫一個函數Z,當Z被輸入引數 N(0至100)的時候,傳回相對應的字母評級,就好似小學生的考試評級一樣。 思路非常簡單:建立字母陣列V,V 由65個F(0分至64分)、5個D(65分至69分)、10個 C(依此類推)、10個B及11個A順序組成;然後用N做陣列V的指數,返回V[N+1]的值。用圖像表示: 提交的程式碼如下: 解說一下: 表達式 (65 5 10 10 11)/'FDCBA' 用 replicate 函數 " /"  建構陣列 V 於是陣列 V  由65個  F ,5個  D ,10個  C ,10個  B ,11個  A  組成 基於右引數 ⍵ ,提取V的第 ⍵+1 個元素(何以?因為 Z 0 要傳回 V 的第 1 個元素,而 Z 100 則需要傳回V的第 101 個元素,如此類推) 注意,這個寫法容許 ⍵ 係 陣列 ,也是題目的要求。例如: APL 的簡潔總是給人一種不能言喻的美感。 一項更正: 原文介紹 " / " 時把它稱作運算子,但在以上用法中,其作用應為函數。