• 聊聊 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/

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

  • Elixir.tw online meetup 指南

    Meetup 形式與主題

    每次預計 45min ~ 1hr 主題 talks, 另外有 30min 左右的發問與閒聊時間。而每次聚會的talks 部份會測錄並上線。

    • 2020/04/14 Elixir 基本語法
    • 2020/05/12 Phoenix

    其它主題(日期與形式規劃中)

    • 進階語法
    • macro
    • Phoenix LiveView & Channel
    • Concurrent, OTP & Flow
    • Ecto
    • ETS
    • Nerves
    • deployment
    • rustler

    事前準備

    線上 meetup 將使用 Google Hangout Meeting,並用 Visual Studio Code 的 LiveShare 功能輔助。另外我們建議在你的電腦上安裝好 Elixir 的開發環境。

    教材

    若你對 Elixir 語法還不熟悉,建議你可以用 Elixir School 的教學做為參考。第一次講解的內容基本上涵蓋該教材基礎部份的 1 ~ 7 章。

    其它

    若你有任何想要分享的主題或是特別感興趣的方向,歡迎在 Facebook 粉絲團,Slack,或來信 ask@elixir.tw 與我們討論。


    Elixir 能用來做什麼,好處在哪?

    誰在用 Elixr (或它的底層: Erlang)

    Elixir:

    • Discord
    • Pinterest
    • Moz
    • Toyota Connected
    • Grinder
    • 怪物彈珠
    • 任天堂

    Erlang

    • Ericsson
    • WhatsApp
    • RabbitMQ
    • Riak
  • 安裝 Elixir 環境

    本文介紹幾種在電腦上安裝 Elixir / Erlang 環境的方法。

    Mac

    A. 使用 asdf (推薦)

    asdf 是類 unix 作業系統上類似 rvm, rbenv 或 nvm 的語言版本管理套件。特別之處在於它可以安裝不同的 plugin 來管理多種不同的語言

    pre request

    • homebrew
    • git

    Install

    1. 安裝 asdf 及 erlang 需要的元件
    $ brew install \
      coreutils automake autoconf openssl \
      libyaml readline libxslt libtool unixodbc \
      unzip curl wxmac asdf
    
    1. 將 asdf init script 加到 zshrc 中,若你用的是 bash 或 fish 請參考官方說明這步之後需要重啟 shell
    $ echo -e '\n. /opt/homebrew/opt/asdf/libexec/asdf.sh' >> ~/.zshrc
    $ echo -e '\n. /opt/homebrew/share/zsh/site-functions' >> ~/.zshrc
    
    1. 安裝 asdf 的 elixir 及 erlang plugin
    $ asdf plugin-add erlang
    $ asdf plugin-add elixir
    
    1. 用 asdf 安裝最新版的 Erlang。這一步一般來說需要很久,可以去聽個兩首歌再回來。
    asdf install erlang latest
    
    1. 用 asdf 安裝最新版的 Elixir。由於 Elixir 是預編譯版本,所以可以選擇用符合自己 Erlang 版本編譯的版本。
    asdf install latest elixir
    

    Postgresql 及 NodeJs

    如果想試試 phoenix,那麼需要安裝 postgresql 及 nodejs 5.5 以上的版本。這裡一樣使用 asdf 來安裝 nodejs,若你的系統中已存在其它 nodejs 版本,可以省略後面兩步。

    # 1. 安裝 postgresql
    $ brew install postgresql
    
    # 2. 安裝 asdf 的 nodejs plugin
    $ brew install gpg
    $ asdf plugin-add nodejs
    
    
    # 3. 用 asdf 安裝 nodejs
    $ asdf install nodejs lts
    

    完工

    試一下會不會動:

    $ elixir -v
    $ which erl
    
    ## 如果有裝 phoenix 的話
    $ mix phx.new --version
    

    B. 使用 homebrew

    由於這個方法會跟著 homebrew upgrade 一起更新版本,所以比較適合下載來玩一下的情況。長時間的正式專案開發可能會遇到一些雷。

    $ brew install erlang elixir
    

    C. 使用 Docker

    1. 安裝 docker
    $ brew cask install docker
    
    1. 拉官方 image。
    $ docker pull elixir
    
    1. 跑起來試試看。按兩次 Ctrl+C 結束
    $ docker run -it --rm elixir iex
    
    1. 執行本機上的 elixir 檔案
    $ docker run -it --rm -v $(pwd):/tmp elixir elixir /tmp/my_elixir_file.ex
    

    Ubuntu/Debian

    A. 使用 asdf

    1. 安裝 asdf 及 erlang 需要的元件
    $ apt-get -y install build-essential autoconf m4 \
    libncurses5-dev libwxgtk3.0-dev libgl1-mesa-dev libglu1-mesa-dev \
    libpng-dev libssh-dev unixodbc-dev
    

    接著參考 Mac 的 2 ~ 6 步


    CentOS 7

    wget --no-verbose -P /tmp https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
    
    yum install -q -y /tmp/epel-release-latest-7.noarch.rpm
    
    yum update -y -q
    
    yum upgrade -y -q --enablerepo=epel
    
    yum install -y -q wget curl unzip make git
    
    yum install -y -q automake autoconf readline-devel ncurses-devel openssl-devel libyaml-devel libxslt-devel libffi-devel libtool unixODBC-devel
    
    groupinstall -y 'Development Tools' 'C Development Tools and Libraries'
    
    yum install -y -q wxGTK3-devel wxBase3 openssl-devel libxslt \
        java-1.8.0-openjdk-devel libiodbc unixODBC erlang-odbc
    
    yum install -y -q install gpg perl perl-Digest-SHA
    

    接著參考 Mac 的 2 ~ 6 步


    Windows

    A. 使用 Linux Sub System (推薦)

    1. 參考微軟官方說明,安裝 Linux 子系統。若不知道要用什麼的話,那就選 Ubuntu 吧。

    2. 啟動 Linux 子系統後,照著 Ubuntu/Debian 小節操作。

    B. 使用 scoop

    參考 Scoop 官網說明

    1. 用系統管理者權限打開 powershell

    2. 貼上以下指令並按 [Enter] 執行

    iwr -useb get.scoop.sh | iex
    
    1. 安裝 elixir
    scoop install elixir
    
    1. 執行看看
    iex.bat
    

    C. 使用 chocolatey

    參考 chocolatey 官網說明

    1. 用系統管理者權限打開 powershell

    2. 貼上以下指令並按 [Enter] 執行 (從官網 copy 會比較方便)

    Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))
    
    1. 安裝 elixir
    choco install elixir
    
    1. 執行看看
    iex.bat
    

    D. 使用 Docker

    同 Mac -> C. 使用 Docker

  • 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

subscribe via RSS