• 聊聊 Elixir 中的 type

    最近有幾位朋友分別來問 Elixir 的 type 的問題,想說中文世界好像沒有比較完整的東西,就把知道的東西整理出來。

    (目前) Elixir 的 type 能做什麼?

    tl;dr: 最主要是文件,然後在某種程度下防止錯誤。

    我覺得這應該是在研究 Elixir 的 type 時最需要知道的事情了。不像 Haskell 及 F# 這種以型別著稱的 ML 系語言,Elixir / Erlang 本質上是個動態語言,所有與型別有關的標註都會被編譯器忽略。而 Erlang 內建的型別檢查工具 dialyzer 是採用 success typing,在型別 有可能 是正確的時候就視為通過 (等一下會看到有趣的範例),所以目前在 Elixir 中撰寫型別標注最主要還是用做於程式相互操作間的文件溝通使用。

    我自己個人的習慣通常是剛開始寫 code 時不會標注型別,然後在寫測試程式時把 public 的函式都標上型別。當然也有專注在資料結構的情況,這時候才會一開始就對型別比較講究。

    Elixir 的型別標注語法

    Elixir 的型別語法主要就看官方文件的 typespecs 這一頁。比較值得另外提的就是 sum type (union type) 跟 product type 的標記方式。在 Elixir 裡 sum type 是用 | 這個符號來分隔。而 product type 就是用 tuple 語法了。來個範例:

    defmodule Card do
      # 自定義的型別用 @type
      # 四種 atom 中的一種
      @type suit() :: :clubs | :diamonds | :hearts | :spades
    
      # 2 到 10 的數字或是四種 atom 中的一種
      @type rank() :: 2..10 | :jack | :queen | :king | :ace
    
      # product type 用 {}
      # t 表示 Remote type。這個會變成 Card module 本身的 type,
      # 可以用 Card 或是 Card.t() 來指涉這個型別
      @type t :: {rank(), suit()}
    
      # 標注函式型別用 spec
      # 接收 list of cards, 回傳 card
      @spec deal([Card]) :: Card
      # 同一個模組下也可以直接用 t 表示,改成這樣:
      # @spec deal([t]) :: t
      def deal(cards) do
        # not implement yet
      end
    end
    

    比較有用/有趣的內建 type

    數字的話有 neg_integer()non_neg_integer()pos_integer() 可以用。

    高階函式可以用 (type1 -> type2) 等箭號系列當做參數或是回傳值的型別標注。

    nonempty_list 表示不為空的串列,跟 maybe_improper_list,用來表示中間階段,其結尾還不是 [] 的 list,那麼就有延伸的 nonempty_improper_listnonempty_maybe_improper_list

    在 map 的部份可以做出非常細緻的定義,需要有什麼 key,對應的值的型別是什麼,還有可以有 optional 的選項。這些就請看文件了。

    mfa 代表 {module, funciton_name, arity} 的 tuple,這個在做 meta-programming 的時候蠻好用的。

    其它的標註屬性

    @typep 用來表示這個自定義的型別只在目前的 module 中可見,而 @opaque 則是外界看得到這個型別,但是無法知道裡面的結構。

    @callback@macrocallback 則是在操作 Behaviour 的時候使用的,常見的情況就是在實作 GenServer 或其它的 Behaviour 時,標註 @callback 就會幫你檢查是否所需要的 callback 都有妥善的依規格實作。

    Success typing 是什麼

    我們來寫一個 compare 的例子,給兩張牌,回傳第一張跟第二張相比的大小。

    @spec compare(Card, Card) :: :gt | :eq | :lt
    def compare(_c1, _c2) do
      "opps"
    end
    

    在無視輸入直接回傳一個錯誤的型別, 如果你的編輯器有裝 language server protocol,那麼就會跳出有問題的提示:

    Wrong spec

    或是你也可以在 project 中安裝 dialyxir,這是 Erlang 內建的型別檢查工具 dialyzer (唸 di-a-lai-zer) 的 wrapper,按其說明 compile 並執行,就會開心的看到如下的錯誤訊息:

    Dialyzer error

    那麼把型別改成正確的回傳就沒問題了:

    Type correct

    那如果不是固定的值,而是計算的結果呢?回傳 1 + 1,一樣可以看到型別錯誤的提示。

    Calc type error

    不過我們高興的太早了。如果我們這樣寫的話, success typing 會認為這段程式碼是 合法的

    wat

    原因是因為雖然"我們"知道這段程式碼只會回傳 “wat” 字串,但是 Elixir 不知道。在型別檢查階段,只能確認 Enum.random/1 函式的回傳型別是 any(),所以 dialyzer 覺得呼叫 Enum.random/1 回傳 :eq | :gt | :lt 其中一個的機率不為零,所以就放行了…。這段程式碼只接收了 list of string 的這個事實,要到編譯階段才會知道,但那時型別標注都已經被移掉了。

    看到這裡應該就可以理解為什麼 Elixir / Erlang dev 一直以來沒有非常認真的看待 type 的原因了。

    還有一些其它的

    不過只要是會長期維護的專案,我個人的習慣還是都會加上 dialyxir 並設到 CI 裡。經驗上我還是有好幾次在修改了程式碼後,unit test 通過但是型別檢查噴了錯誤訊息救到我的案例出現。

    而在社群的關注下, José Valim 在 ElixirConf EU 2022 講了很久的 typing 的議題,也宣布了有想要朝 gradual typing 進行認真的研究的方向。在上個月也在官網發了文章把 talk 的內容匯整起來:

    https://elixir-lang.org/blog/2022/10/05/my-future-with-elixir-set-theoretic-types/

    不過官方目前的態度是不知道這個研究會產生什麼結果,所以也只能等著看看了…

  • Steam 上的程式教學類遊戲

    農曆年期間比較有空,玩了一些之前買的遊戲。這次特別試了幾個標榜讓不會寫程式的人學寫程式的遊戲。分享一下試玩的心得。

    1. 7 Billion Humans

    考慮到劇情的話我最喜歡的是 7 Billion Humans。它用拖拉語法的方式下指令,一開始還蠻好上手的,但是因為只有 goto 那樣的結構,而操作的時候又是一次對所有的 worker 下指令,所以常常要想一下執行後每個人運作的順序。但是介面有正體中文,以「想要體驗一下寫程式大概是怎麼一回事」來說還蠻適合的。

    2. while True: learn()

    這個遊戲說的是某個 programmer 發現他的貓會寫程式,所以要開發一個喵語翻譯器。想要模擬的是機器學習的內容,所以是試著組合各種過濾器(神經網路?)。因為精度高的過濾器慢加上有時間限制,所以習慣找最簡單的組合的想法常常會超時,手感跟 deterministic algorithm 差很多。也有正體中文介面不過有些翻譯怪怪的。

    3. Grey hack

    這個遊戲打開就是一付 Linux 樣,有瀏覽器,郵件軟體跟終端機,然後就要去接任務 hack 東西,也可以寫 script。基本上沒什麼「我在玩遊戲」的感覺,往好處來說就是 context switch 成本很小? (不是

    不過我還蠻喜歡的。

    4. Screeps

    Screeps 就要真的寫程式碼了。這個遊戲用 JavaScript 操作,玩類似 starcraft 般的遊戲。所以對程式完全陌生的人應該會蠻辛苦了。一開始的 tutorial 蠻簡單,但是每次都帶上文件連結,「我還是在寫程式啊」的感覺比上個遊戲更強烈。還好我就是愛寫程式。愛就是愛。 XD


    其它看起來比較有趣但是還沒有買的有 InfinifactoryOpus MagnumNeon Noodles

  • Phoenix LiveView 概念篇

    在 2018 九月 ElixirConf 的 ending keynote 中,Phoenix 的作者 Chris McCord 發表了正在開發中的新套件,Phoenix LiveView。而上週五 (3 月 15 日) 這個套件終於在 GitHub 上公開了。 本篇將介紹 Phoenix LiveView 的想解決的問題、基本概念,以及一些個人的想法。

    簡單場景:Server side render

    在介紹 Phoenix LiveView 之前,先來回頭看一個用 server side render 寫出來的表單輸入場景:註冊帳號。首先使用者輸入 username 及 email,按下送出。發現註冊失敗,原來電話號碼是必填欄位。

    接著亂填一個電話號碼,按下送出,錯誤訊息提示電話號碼格式不對。

    把電話號碼改好後,按下送出,才發現這個 email 已經被註冊過了。

    換了一個 email 按下送出,這次才終於註冊成功。只有三個欄位的表單,使用者按下了四次送出鈕才完成。

    下一步:用 JavaScript 改進

    我們想要的是當按下鍵盤按鍵時,就幫我們判斷格式是否正確,欄位是否有填完等等。為了改善這糟糕的互動體驗,主流的做法是引進 JavaScript 。通常會先以原生的 JavaScript ,或許再配上一點 jQuery 用 AJAX 來處理,接著依場景在前端實作將各種欄位的驗證及錯誤提示,當然後端的驗證還是要保留著。不然就會發生前沒多久某屈姓藥妝店的新聞了。

    但隨著功能變多,沒有仔細規劃的話,我們的網頁就逐漸變成了一鍋「事件湯」。各個 listen events 間有錯綜複雜的觸發順序與依賴關係,一不小心就會讓該發生的事沒觸發到,或是不該發生的事件觸發了。

    這時就會開始考慮使用 JavaScript 框架。但撇開漫長的工具棧選擇及組織社會性問題,各個頁面流程都得逐漸遷移到前端去。再來就發現頁面的 SEO 沒了,如果很在乎 SEO 的話,那就得在中間做一層 isomorphic layer,讓爬蟲也能爬到資料…

    即使天時地利人和,總算把整個站改成了 Single Page Application 加上原本的後端的 Server。此時後端的 Server 已經變成一個純 API Server 了,那麼就會開始懷疑後端 Server 的實作方式是否符合目前架構的特性。舉例來說,為什麼要用 Rails 做純 API Server 呢?是不是改成 Sinatra ,甚至用 Golang 會比較好?

    Phoenix LiveView

    在許多情況下,我們只是想要一些些比較好的使用者互動而己。

    可不可以沿用後端的頁面流程及驗證邏輯,卻又能即時的跟使用者互動呢? Phoenix LiveView 就是在這個概念下產生的一種解法。

    Chris McCord 在 announcement post 裡是這麼說的:

    Phoenix LiveView is an exciting new library which enables rich, real-time user experiences with server-rendered HTML. LiveView powered applications are stateful on the server with bidrectional communication via WebSockets, offering a vastly simplified programming model compared to JavaScript alternatives.

    Phoenix LiveView 讓你可以在 HTML tag 上用 phx- 屬性註明綁定的事件,但不是由前端進行處理,而是在事件觸發時,透過 websocket 將資料傳到後端,處理完成時後端主動將資料推至前端進行部份渲染。這麼一來,我們的網頁就有了保持狀態的能力,也就是上面引言中 “Stateful” 的意思。

    在下圖的例子中,我們用 phx-click="inc"幫 + 這個按鈕綁上 click 事件。並在後端用 handle_event/3 處理接收到的 inc 事件資料。這樣一來每次按下這個按鈕,就會觸發事件,用 websocket 傳送資料到後端。處理完成後,一樣用 websocket 將資料傳回前端重新渲染 。由於 handle_event/3 是在 server 端處理的,所以這邊的程式可以直接呼叫原本的流程及驗證邏輯等既有程式。

    Phoenix Channel 效能

    José Valim 跟 Chris McCord 都說過他們開發這個語言/框架的最主要原因,就是平行化處理。因此 Phoenix 自專案開始就內建了 Channel 這個處理 WebSocket 協定的模組,在建立連線後,Server 端除了被動的接收從 Client 來的訊息之外,也可以主動推送資料到 Client 端。適用在聊天室等 Cllient 端需要知道 Server 端的連續狀態變化等場景。

    得益於 Elixir / Erlang 優異的平行處理能力,Phoenix Channel 有在 55,000 使用者同時連線 websocket 的情況下,廣播訊息至 200 人的聊天室裡平均 0.24 秒的記錄。建構於其上的 Phoenix LiveView 甚至在官方 demo 裡放了一個 server side rendering 的動畫範例 rainbow ,純靠 server side 不斷的將更新的 div 推送到前端製造動畫效果。

    在我的電腦 (MacBook pro 15" 2015) 上,在不開 development tools 的情況下,60 fps 相當順暢,超過 85 fps 就會偶爾會出現卡頓感了。

    錯誤處理

    除了平行處理的能力之外,Erlang 的另一個重要特性就是容錯能力 (fault tolerance)。當 phoenix channel 在 server 端發生執行期錯誤、或是接收到不存在的事件時,設計上會使得處理該 channel 的 process 陣亡,並由 supervisor tree 生成另一個新的 channel 與 client side 對接。

    從使用者的角度來看,當發生錯誤時,瀏覽器會短暫停頓(這時顯示的是 websocket 未連線的 fallback 畫面),接著就回復初始的狀態。

    適合場景

    在 Elixir forum 的討論裡,Chris McCord 指出 LiveView 已知適合用在下列情況中:

    • 應用程式裡需要大量使用者互動的地方,如提示訊息、非同步工作狀態顯示、進度條、儀表板、附掛小工具等
    • 表單互動。如驗證、會依不同選項變動的動態表單、設定精靈等
    • 會需要即時知道 server 端狀態的東西
    • 需要 server 參與的使用者互動,如搜尋、自動補完等

    在官方 Example 裡還放上了 LiveView 做出來的貪食蛇及 PACMAN 遊戲。在討論裡 Chris McCord 也說了這樣的話:「我們的計劃是先從小的地方開始,看看我們大家會用這個做出什麼東西來(,再決定之後的方向)。」

    個人想法

    Client side rendering 在這五六年蓬勃發展,也有它無可取代的應用場景。例如 Gmail、Netflix 等等。另外 server 端只做 API,而由不同樣態的客戶端如瀏覽器、手機 App 分別與之對接也是在規模變大時很常見的做法。

    但當 Phoenix LiveView 的出現帶來了另一種輕量級的可能性時,將應用程式改成 client side rendering 的決策壓力線將會向右推遲。而在與其它框架比較時,Phoenix 會在 websocket server 的候選清單中取得更為優勢的地位。

    順帶一提目前已經有人把 Phoenix LiveView 跟 Vue Component 搭在一起用 ,依這個思路與 React 或 Web Component 合併看來也完全可行,只是除了在遷移的過渡情況之外,還想不到能這樣能拿來幹麼就是了。

    下一篇會導覽 Phoenix LiveView 的官方範例程式碼,敬請期待了。 Happy hacking!

    References

    ElixirConf 2018 Ending Keynote - Chris McCord

    Phoenix LiveView Repo

    Phoenix LiveView Examples

    Phoenix LiveView blogpost

    Phoenix Channel vs Rails ActionCable

  • Ruby 2.6 的新功能

    Ruby 2.6 除了主打的 JIT 之外,還引進了一些有趣的新功能。其中有一些可以讓 Ruby 在函數式風格的寫法上更加自然流暢。想來稍微展示一下這些功能的用法。

    1. Compose operator:
    Proc.>>Proc.<<

    雖然 Ruby 天生在某些用法上就採用了函數式風格(例如沒有 for 迴圈,而是用 each + block),但由於高階函式接受的是 block 而非 lambda,lambda 本身也無法簡單組合起來。所以一直以來匿名函式在 Ruby 中除了拿來當參數傳遞之外,用法相當受限。

    在正統的函數式編程中,用 function composition 的方式將多個小函式組合在一起是很基本的操作,舉個之前的例子,假設你想要這樣的連續調用:

    # 注意,因為假設這些函數都是 lambda,所以要用 `.()` 調用
    request = generate_request.()
    response = get_response.(request)
    body = parse_body.(response)
    html = render.(body)
    

    其實除了 html 這個最終結果外,我們不需要中間的臨時變數,而可以改寫成這樣(然後被同事記恨):

    html = render.(parse_body.(get_response.(generate_request.())))
    

    Ruby 2.6 開始你可以用 << (compose) 或是 >> (pipe) 來組合 Proc (或 lambda)。假設我們有兩個 lambda fg,這兩個運算子的作用如下:

    (f >> g).(x)
    # 等同於
    g(f(x))
    
    
    (f << g).(x)
    # 等同於
    f(g(x))
    

    我自己的記憶方式是把這兩個 operator 看成函式呼叫流程的箭頭。f >> g 就是先調用 f ,再將結果傳進 g。反之 f << g 則是先調用 g 再傳給 f

    那麼之前的例子就可以改成下列兩者之一:

    get_html = generate_request >> get_response >> parse_body >> render
    html = get_html.()
    
    # 或是
    
    get_html = render << parse_body << get_response << generate_request
    html = get_html.()
    

    Ruby 2.6 的文件範例如下:

    f = proc {|x| x * x}
    g = proc {|x| x + x}
    
    (f << g).call(2) # => 16
    (f >> g).call(2) # => 8
    

    再進階一點配合 Ruby 原本就有的 Object#methodMethod#curry,想來可以組合出非常有意思的寫法。

    2. Enumerable#filter

    函數式編程最著名的三個高階函式就屬 mapreducefilter 了。但在 Ruby 中,filter 這個高階函式被改名為 select,這一版加上了 filter 的別名,跟其它語言的慣用法一致,再也不會等到測試爆掉才想起函式名稱不一樣了。

    [1, 2, 3, 4].filter {|i| i % 2 == 0} # => [2, 4]
    

    3. Endless range

    現在範圍運算子 .. 可以不加結束的參數:

    [:a, :b, :c, :d][2..] # => [:b, :c, :d]
    
    
    (:b..).lazy.zip(10..).first(3) # => [[:b, 10], [:c, 11], [:d, 12]]
    

    4. Array#union and Array#difference

    Array#union 是聯集,Array#difference 是差集。直接上範例:

    [1].union([2, 3], [1, 2, 4, 5]) # => [1, 2, 3, 4, 5]
    
    
    [1, 1, 2, 2, 3, 3, 4, 5].difference([1, 2, 4]) # => [3, 3, 5]
    

    結語

    除了這些之外,Proc.call 調用快了大約 1.4 倍。另外還有 Object#thenHash#mergeEnumerable#to_h 等等,就找機會試玩看看吧。Happy hacking!

subscribe via RSS