2018/6/11

tmux


習慣使用 linux terminal 遠端處理 server 工作的人,有時會遇到一個問題,就是在遠端 terminal 處理過程中,有時會遇到一些程式處理很久,或是需要同時查看 log,系統 loading 的狀況,這時,就需要再對同一台機器打開另一個 terminal,導致 terminal 的頁籤越來越多。更麻煩的是有時候遇到網路異常斷線,所有 terminal 的連線中斷了,就必須要重新一個一個再連接 server。


tmux 是一個 terminal multiplexer,可讓使用者以單一terminal,連接多個 terminal sessions或是windows。換句話說,就不需要再連接多個 terminal tab。另外,更重要的功能是,tmux 內建了一個 terminal server,即使 terminal 斷線,只要 tmux server 還存活,任何時候再重連,都可以取回剛剛工作中的 terminal sessions/windows,繼續工作。


安裝


在 centos 安裝 tmux


yum install tmux

在 macos 安裝 tmux


sudo port install tmux

tmux 指令


只要在 terminal 執行 tmux,就會啟動 tmux server,另外還有一些常用的指令


# 啟動新的 terminal session
tmux new -s sessionanme
tmux new -s sessionanme -n windowname

# 列出所有 tmux sessions
tmux ls

# a/at/attach session
tmux at -t sessionname
tmux a #

# kill session
tmux kill-session -t sessioname

# 當 session 內所有shell都結束,該 session 就會中止
exit

進入 tmux 後,terminal 下方就會出現一條綠色的 status bar,很明確的顯示目前正在 tmux 工作 session 中。



tmux 專有名詞的概念


  • tmux server


    啟動 tmux 會產生一個 server,負責管理所有 sessions

  • session


    一個 terminal 可以有多個 sessions,通常一個 project 會使用一個 session。

  • window


    一個 session 可以有多個 window,每一個 window 會佔滿整個 terminal 畫面,可以開多個 window,讓某些 window 在背景運作。

  • pane


    每一個 windows 可切割多個區塊,每一個區塊就是一個 pane。通常會將 window 水平或垂直切割,增加多個 pane。


控制指令


進入 tmux 後,跟平常一樣,會在多個 shell 中切換執行工作,如果需要對 tmux 下指令,要用 Ctrl-b 功能鍵啟動。


因為 Ctrl-b 會有點難按,大部分都會改成其他的 function key,在 ~/.tmux.conf 設定檔中,可增加這些設定,將 Ctrl-b 改為 Ctrl-a


set -g prefix C-a
unbind C-b
bind C-a send-prefix

另外可在設定檔中加上快速鍵,以下設定,可以在 Ctrl-a 後,直接按 | ,就會水平方向增加一個 shell pane。


unbind %
bind | split-window -h
bind - split-window -v

啟用滑鼠,可用滑鼠修改 pane 的大小,捲動視窗


set -g mouse on



  • session 處理的指令
    大部分都是在原本的 terminal 中,不是 tmux 的 function

tmux ls
tmux attach -t 0
tmux kill-session -t 0

Ctrl-a (由 Ctrl-b 改為 Ctrl-a) 後的 fuction


Ctrl-a 後的 function 功能
d detach session
s list session
:new new session
$ 為 session 命名

  • window 指令

Ctrl-a 後的 function 功能
c 產生新 window
& 關閉目前的 window
p 切換到上一個 window
n 切換到下一個 window
w list windows
f find window

  • pane 指令

Ctrl-a 後的 function 功能
% (改為 |) 水平分割新的 pane
" (改為 -) 垂直分割新的 pane
方向鍵 切換到其他 panes
x 關閉目前的 pane
o 交換 pane
空白鍵 切換 layout
q 顯示每個 pane 的編號,再按編號,可切換到該 pane
{ 跟上一個 pane 交換位置
} 跟下一個 pane 交換位置
z 切換 pane 最大/最小化



~/.tmux.conf 設定的內容


set -g default-terminal "screen-256color"
set -g display-time 3000
set -g escape-time 0
set -g history-limit 65535
set -g base-index 1
set -g pane-base-index 1

# Ctrl-b -> Ctrl-a
set -g prefix C-a
unbind C-b
bind C-a send-prefix

# enable mouse
set -g mouse on

# split window
unbind %
bind | split-window -h
bind - split-window -v

# <prefix> or Ctrl- or <prefix> Ctrl-
#   k --- previous pane
# h   l --- previous window / next window
#   j --- next pane

# navigate windows
unbind-key l
bind-key h select-window -t :-
bind-key C-h select-window -t :-
bind-key -n C-h select-window -t :-
bind-key l select-window -t :+
bind-key C-l select-window -t :+
bind-key -n C-l select-window -t :+

# navigate panes
bind-key j select-pane -t :.-
bind-key C-j select-pane -t :.-
bind-key -n C-j select-pane -t :.-
bind-key k select-pane -t :.+
bind-key C-k select-pane -t :.+
bind-key -n C-k select-pane -t :.+

References


終端機 session 管理神器 — tmux


終端機必備的多工良伴:tmux


tmux ,不只是 terminal multiplexer


Tutorial — tmux Basics


tmux cheatsheet


Tmux 快捷鍵 & 速查表


使用 tmux 與 tmuxifier 打造 Console 開發環境(比 screen 更棒)

2018/6/4

資料科學可以回答的問題


資料科學會使用 Machine Learning 的演算法,這些演算法的使用方式,都是這三個步驟:讀取資料,轉譯,提供答案,但在選擇演算法之前,要先知道這些演算法能回答什麼問題,要問對問題,才能找到答案。


以下這些是可以回答的問題


  1. 這是A,還是B? Is this A or B? (two-class classification)
  2. 這是A、B、C 還是 D? Is this A or B or C or D? (multi-class classification)
  3. 有沒有奇怪的地方? Is this Weird? (anomaly detection)
  4. 這有多少/有幾個? How Much/How Many? (regression)
  5. 用迴歸演算法解決多元分類問題 Multi-Class Classification as Regression
  6. 用迴歸演算法解決二元分類問題 Two-Class Classification as Regression
  7. 資料是由什麼組成的?怎麼分類? How is this Data Organized? (unsupervised learning, clustering)
  8. 接下來該怎麼做? What Should I Do Now? (reinforcement learning)

這是A,還是B? Is this A or B? (two-class classification)


二元分類 two-class classification,用來解決只有兩種答案的問題,例如:


  • 這位客戶會不會續約?
  • 這張照片是貓還是狗?
  • 這位顧客會不會點最上面的連結?
  • 如果再開一千英里,這個輪胎會不會爆胎?
  • 五元折價券或是打七五折,哪一個促銷手段能吸引更多顧客?

這是A、B、C 還是 D? Is this A or B or C or D? (multi-class classification)


多元分類 multi-class classification,用來解決有多種答案的問題,例如:


  • 這是哪種動物的圖片?
  • 這是哪種飛機的雷達訊號?
  • 這篇新聞屬於哪一個主題?
  • 這則 twitter 隱含了哪一種情緒?
  • 這則錄音裡的講者是誰?

有沒有奇怪的地方? Is this Weird? (anomaly detection)


異常偵測 anomaly detection,用來辨別不正常的資料,當分析的情況發生率很低,導致樣本數也很少的時候,異常偵測就顯得特別有用。感覺上跟 二元分類 two-class classification 很像,差別在於二元分類的原始資料中,就包含了兩種答案,但是異常偵測則不一定。


例如:


  • 是不是信用卡盜刷
  • 壓力大小有任何異狀嗎?
  • 這則網路訊息正常嗎?
  • 這些消費記錄跟這位使用者過去的行為落差很大嗎?
  • 這些用電量在這個季節和時間算是正常的嗎?

這有多少/有幾個? How Much/How Many? (regression)


當解決的問題涉及數字而非分類時,這一類的演算法就稱為迴歸(regression),例如:


  • 下週二的氣溫為何?
  • 第四季在葡萄牙的銷售量會有多少?
  • 三十分鐘後,我的風力發電廠會有多少千瓦(kW)的需求?
  • 我下週會獲得多少新追蹤者?
  • 每一千個使用這種軸承的產品裡,有多少個能被使用超過一萬小時?

用迴歸演算法解決多元分類問題 Multi-Class Classification as Regression


有些看起來很像多元分類的問題,但更適合用迴歸解決。例如


  • 讀者對哪則新聞最感興趣


    乍看之下是個分類問題,但如果將問題換成「對讀者來說,每則新聞的有趣程度為何」並為每則新聞評分,接下來就只需要選出最高分的新聞。這類問題通常和排名或對比有關。

  • 我的車隊中,哪台廂型車最需要保養


    可以換成「我的車隊裡,每台廂型車需要保養的程度為何」

  • 哪 5% 的顧客隔年會跳槽到對手公司


    可以換成「每名顧客明年跳槽到對手公司的機率為何」。


用迴歸演算法解決二元分類問題 Two-Class Classification as Regression


二元分類問題也可以換成迴歸問題,這類問題也通常以「有多少可能性」、「有多少比例」開頭。例如:


  • 這位使用者有多大機率會點我的廣告?
  • 這台拉霸機有多少比例的回合會給獎金?
  • 這名員工有多大機率會造成內部安全的威脅?
  • 今天有多少比例的航班會準時抵達?

二元分類、多元分類、異狀偵測和迴歸等四種演算法之間都很相近,它們都是監督式學習(supervised learning)下的演算法。共通之處,在於建模時都用了一組包含回答的資料(這個過程稱作訓練,training),並被用來分類或預測一組不包含回答的資料(這個過程稱作評分,scoring)。


資料是由什麼組成的?怎麼分類? How is this Data Organized? (unsupervised learning, clustering)


這是非監督和強化式學習(unsupervised and reinforcement learning)的演算法。


判斷資料分類的方法有很多,其中一類是聚類法(clustering),包括資料群集(chunking)、分組(grouping)、聚束(bunching)、分段(segmentation)等等。聚類法所分析的資料不包含任何用來引導分群、說明分群意義和數量的數字或名字。聚類法的基礎是衡量資料之間的距離或相似度,也就是距離度量(distance metric)。距離度量可以是任何可測量的數據。


  • 哪些消費者對農產品有相似的品味?
  • 哪些觀眾喜歡同一類電影?
  • 哪些型號的印表機有類似的故障問題?
  • 這間變電所在每週的哪幾天有類似的電力需求?
  • 用什麼方法可以自然地將這些文件分成五類?

另一類演算法稱作降維法(dimensionality reduction)。降維是另一種簡化資料的方法,它可以讓資料的溝通變得更容易、處理變得更快、而且存取變得更簡單。降維的運作原理是創造出一套簡化資料的方法。等第積分平均(GPA)是一個很簡單的例子。


  • 哪幾組飛機引擎偵測器的數據呈同向(和反向)變化?
  • 成功的 CEO 有哪些共通的領導力特質?
  • 全美的油價起伏有哪些相似的特徵?
  • 這些文件裡有哪幾組詞彙常常同時出現?(它們和哪些主題有關?)

接下來該怎麼做? What Should I Do Now? (reinforcement learning)


第三類演算法和行動有關,即強化學習(reinforcement learning)演算法。這些演算法和監督式和非監督式都不太一樣。


比方說,迴歸演算法雖然可以用來預測明天的最高溫為華氏 98 度,但它不能用來判斷該做什麼;另一方面,強化學習演算法就可以用來判斷該採取的行動,例如趁天氣還沒變熱的時候,先開辦公大樓內上半層的冷氣。


強化學習演算法很適合用於需要在無人監督情況下、完成許多簡單決策自動化系統,例如電梯、電熱器、冷氣和照明系統。由於強化學習最初被開發的目的是用來操縱機器人,任何自動物件也能使用這套演算法,像是偵查用無人機或掃地機器人。強化學習的問題總是和該採取什麼行動有關,雖然最後往往還是機器在處理這些問題。


  • 我該把廣告放在網頁何處,才能讓讀者最有機會點到它?
  • 我該把溫度調高或調低一點,還是維持現狀?
  • 我該再掃一次客廳還是繼續充電?
  • 我現在該買入多少股?
  • 看到黃燈時,我該保持當前速度、煞車還是加速?

References


What Types of Questions Can Data Science Answer?


五種可以用機器學習回答的問題


Which Algorithm Family Can Answer My Question?

2018/5/28

CQRS: Command Query Responsibility Separation


CQS (Command Query Separation) 是由 Bertrand Meyer (Eiffel 語言的爸爸) 在 1988 於 "Object Oriented Software Construction" 這本書中提出的軟體架構概念,所有的 computing method 只會分成兩類,一種是執行某個 action 的 command,另一種是呼叫查詢並取得回傳資料,但不應該同時做兩種工作。換句話說,發問時,不能在該 method 裡面修改答案。


CQRS (Command Query Responsibility Separation) 應用 CQS 的概念,進一步將 Query 及 Command 物件分離,分別處理取得資料及修改資料的工作。


CQS 遇到的問題,會是 re-entrant 及 multi-thread,就是當一件工作處理到一半,被新的工作中斷這樣的問題,也就是 thread-safe 的問題,也就造成實作複雜度的問題。


然而這樣的問題,搭配著 Event Sourcing 的方法,將某個 APP state 的變更,收集成一個 sequence of events,以這種方式處理 command action,就能解決 thread-safe 的問題。


不過我們還是會遇到,command action 執行的速度跟 query 的時機點的問題,如果修改資料的動作在 query 以前還沒有完成,那麼前端就會查詢到舊狀態的資料,但資料還是會達到最終一致性,而不會有強一致性。


CQRS 的優點:


  1. Command 及 Query 分工明確,可分別進行效能調整及最佳化
  2. 將業務上的命令和查詢的職責分離能夠提高系統的性能、可擴展性和安全性
  3. 企業邏輯簡單清楚,能夠從事件歷程看到系統中的那些行為或者操作導致了系統的狀態變化。
  4. 將開發的邏輯概念,從數據驅動 (Data-Driven) 轉到任務驅動 (Task-Driven) 以及事件驅動 (Event-Driven)

在以下狀況,可以考慮使用CQRS模式:


  1. 當在業務邏輯層有很多操作需要相同的實體或者對象進行操作的時候。CQRS使得我們可以對讀和寫定義不同的實體和方法,從而可以減少或者避免對某一方面的更改造成衝突
  2. 用在特定任務的用戶互動系統,通常系統會引導用戶通過一系列複雜的步驟和操作,通常會需要一些複雜的領域模型。寫入資料的部分有很多和業務邏輯相關的命令操作,輸入驗證,業務邏輯驗證來保證數據的一致性。讀取資料沒有業務邏輯,僅僅是回傳 DTO。讀與寫的資料只需要達到最終一致性。
  3. 適用於一些需要對查詢性能和寫入性能分開進行優化的系統,尤其是讀/寫比非常高的系統。例如在很多系統中讀取資料的使用次數遠大於寫入資料。
  4. 對於系統在將來會隨著時間不斷演化,有可能會包含不同版本的模型,或者業務規則經常變化的系統
  5. 需要和其他系統整合,特別是需要和事件歷程 Event Sourcing 進行整合的系統,這樣子系統的臨時異常不會影響整個系統的其他部分。

在以下狀況,不適合使用CQRS:


  1. 領域模型或者業務邏輯比較簡單,這種情況下使用CQRS會把系統弄得太複雜
  2. 對於簡單的,CRUD模式的用戶介面以及與之相關的數據訪問操作已經足夠的話,都只是一個簡單的對數據進行增刪改查,沒有必要使用CQRS
  3. 不適合在整個系統中全部都使用 CQRS,在特定模組中CQRS可能比較有用

以下是一些查詢到的 CQRS 架構圖,從圖片可以看到跟傳統的 CRUD Data-Driven 架構的差異。


這種架構區分了 Business 及 Query model 的 DataBase,也可以想成將資料寫入了 Business Database,而前端使用者不會觸及該資料庫,是比較強調資料安全性的方式,但不能保證 Query Model 的 DB 裡面的資料一定會跟 Business Model DB 的資料一樣。



這種架構比較接近一個一般性的系統,資料庫是單一的,且可以在 Event Handler 中確認並檢查 Database 及 Analysis Database 的資料一致性。



這種架構圖比較強調資料流的過程,但基本上架構跟上面那個很接近,不過在 Comamnd 的部分,可注意到 Command 沒有 Reply DTO,也可以說,只要 Comamnd 有送進 Command Handler,就視為一個成功執行的 Command。



這裡強調 Command Bus 是以非同步的方式,送進 Command Handler,非同步可強化系統效能的表現,但如果要用同步的方式也可以,要等到 Command Event 送進 Event Database 後,才回應給前端確認該命令已經完成。而 Query 是用同步的方式,發送 Query 並等待要回傳的 ViewModel。



References


CQRS - Martin Fowler


CQRS 介紹


CQRS 命令查詢職責分離模式介紹


從CQS到CQRS


From CQS to CQRS


深度長文:我對CQRS/EventSourcing架構的思考


DDD CQRS架構和傳統架構的優缺點比較


Introduction to Domain Driven Design, CQRS and Event Sourcing

2018/5/21

Elixir 9 Protocol




inpsect 可以用 printable binary 形式回傳任何 value。但 elixir 是用什麼方式實作的? 是不是 guard clause

def inspect(value) when is_atom(value), do: ...
def inspect(value) when is_binary(value), do: ...

protocol 允許不同的資料類型用於相同的函數,不同資料型別的相同函數形態會有相同的行為。很像是 behavior,但 behavior 用在 module 裡面,protocol 可在 module 外面實作,這表示我們可以自由擴充 module 不足的功能。

defprotocol 定義 protocol,defprotocol 只定義 function,defimpl 實作要放在不同的地方。

defprotocol Inspect do
    def inspect(thing, opts)
end

增加了這兩個實作,就可以 inspect PID

defimpl Inspect, for: PID do
    def inspect(pid, _opts) do
        "#PID" <> IO.iodata_to_binary(:erlang.pid_to_list(pid))
    end
end

defimpl Inspect, for: Reference do
    def inspect(ref, _opts) do
        '#Ref' ++ rest = :erlang.ref_to_list(ref)
        "#Reference" <> IO.iodata_to_binary(rest)
    end
end
iex(1)> inspect self()
"#Process<0.89.0>"

可在 for: 後面使用的 Types 有

Any
Atom
BitString
Float
Function
Integer
List
Map
PID
Port
Record
Reference
Tuple

is_collection.exs

defprotocol Collection do
  @fallback_to_any true
  def is_collection?(value)
end

defimpl Collection, for: [List, Tuple, BitString, Map] do
  def is_collection?(_), do: true
end

defimpl Collection, for: Any do
  def is_collection?(_), do: false
end

Enum.each [ 1, 1.0, [1,2], {1,2}, %{}, "cat" ], fn value ->
  IO.puts "#{inspect value}:  #{Collection.is_collection?(value)}"
end
$ elixir is_collection.exs
1:  false
1.0:  false
[1, 2]:  true
{1, 2}:  true
%{}:  true
"cat":  true

Protocol and Structs

Elixir 沒有 classes,但支援 user-defined types

defmodule Blob do
  defstruct content: nil
end
iex(1)> c "basic.exs"
[Blob]
iex(2)> b = %Blob{content: 123}
%Blob{content: 123}
iex(3)> inspect b
"%Blob{content: 123}"

## structs 其實是 map,key為 __struct__
iex(4)> inspect b, structs: false
"%{__struct__: Blob, content: 123}"

Built-In Protocols

elixir 有以下內建的 protocols

  • Enumerable and Collectable
  • Inspect
  • List.Chars
  • String.Chars

首先定義一個以 0s 1s 表示的 integer

bitmap.exs

defmodule Bitmap do
  defstruct value: 0

  @doc """
  A simple accessor for the 2^bit value in an integer

      iex> b = %Bitmap{value: 5}
      %Bitmap{value: 5}
      iex> Bitmap.fetch_bit(b,2)
      1
      iex> Bitmap.fetch_bit(b,1)
      0
      iex> Bitmap.fetch_bit(b,0)
      1
  """
  def fetch_bit(%Bitmap{value: value}, bit) do
    use Bitwise

    (value >>> bit) &&& 1
  end
end
  • Enumerable and Collectable

Enumerable 定義了三個 functions

defprotocol Enumerable do
    # collection 的元素數量
    def count(collection)

    # 是否包含某個 value
    def member?(collection, value)

    # reduce fun to collection elements
    def reduce(collection, acc, fun)
end

針對剛剛的 Bitmap 實作 Enumerable

defimpl Enumerable,  for: Bitmap do
  import :math, only: [log: 1]

  def reduce(bitmap, {:cont, acc}, fun) do
    bit_count =  Enum.count(bitmap)
    _reduce({bitmap, bit_count}, { :cont, acc }, fun)
  end

  defp _reduce({_bitmap, -1}, { :cont, acc }, _fun), do: { :done, acc }

  defp _reduce({bitmap, bit_number}, { :cont, acc }, fun) do
    with bit = Bitmap.fetch_bit(bitmap, bit_number),
    do:  _reduce({bitmap, bit_number-1}, fun.(bit, acc), fun)
  end

  defp _reduce({_bitmap, _bit_number}, { :halt, acc }, _fun), do: { :halted, acc }

  defp _reduce({bitmap, bit_number}, { :suspend, acc }, fun), 
  do: { :suspended, acc, &_reduce({bitmap, bit_number}, &1, fun), fun } 

  def member?(value, bit_number) do
    { :ok, 0 <= bit_number && bit_number < Enum.count(value) }
  end

  def count(%Bitmap{value: value}) do              
    { :ok, trunc(log(abs(value))/log(2)) + 1 }
  end
end


fifty = %Bitmap{value: 50}

IO.puts Enum.count fifty    # => 6

IO.puts Enum.member? fifty, 4    # => true
IO.puts Enum.member? fifty, 6    # => false

IO.inspect Enum.reverse fifty       # => [0, 1, 0, 0, 1, 1, 0]
IO.inspect Enum.join fifty, ":"     # => "0:1:1:0:0:1:0"
iex(1)> c ("bitmap.exs")
[Bitmap]
iex(2)> c ("bitmap_enumerable.exs")
6
true
false
[0, 1, 0, 0, 1, 1, 0]
"0:1:1:0:0:1:0"
[Enumerable.Bitmap]

defimpl Collectable,  for: Bitmap do
  use Bitwise

  # 回傳 tuple,(1) value (2) function
  def into(%Bitmap{value: target}) do
    {target, fn
      acc, {:cont, next_bit} -> (acc <<< 1) ||| next_bit
      acc,  :done            -> %Bitmap{value: acc}
      _, :halt               -> :ok
    end}
  end
end
iex(3)> c("bitmap_collectable.exs")
[Collectable.Bitmap]
iex(4)> Enum.into [1,1,0,0,1,0], %Bitmap{value: 0}
%Bitmap{value: 50}
  • Inspect
defmodule Bitmap do
  defstruct value: 0

  defimpl Inspect do
    def inspect(%Bitmap{value: value}, _opts) do
      "%Bitmap{#{value}=#{as_binary(value)}}"
    end
    defp as_binary(value) do
      to_string(:io_lib.format("~.2B", [value]))
    end
  end
end
iex(6)> c("bitmap_inspect.exs")
[Bitmap, Inspect.Bitmap]
iex(7)> fifty = %Bitmap{value: 50}
%Bitmap{50=110010}
iex(8)> inspect fifty
"%Bitmap{50=110010}"
iex(9)> inspect fifty, structs: false
"%{__struct__: Bitmap, value: 50}"
iex(10)> %Bitmap{value: 12345678901234567890}
%Bitmap{12345678901234567890=1010101101010100101010011000110011101011000111110000101011010010}

defmodule Bitmap do
  defstruct value: 0

  defimpl Inspect, for: Bitmap do
    import Inspect.Algebra
    def inspect(%Bitmap{value: value}, _opts) do
      concat([
        nest(
         concat([
           "%Bitmap{",
           break(""),
           nest(concat([to_string(value),
                        "=",
                        break(""),
                        as_binary(value)]),
                2),
         ]), 2),
        break(""),
        "}"])
    end
    defp as_binary(value) do
      to_string(:io_lib.format("~.2B", [value]))
    end
  end
end
iex(1)> c("bitmap_algebra.exs")
[Bitmap, Inspect.Bitmap]
iex(2)>
nil
iex(3)> big_bitmap = %Bitmap{value: 12345678901234567890}
%Bitmap{12345678901234567890=
    1010101101010100101010011000110011101011000111110000101011010010}
iex(4)>
nil
iex(5)> IO.inspect big_bitmap
%Bitmap{12345678901234567890=
    1010101101010100101010011000110011101011000111110000101011010010}
%Bitmap{12345678901234567890=
    1010101101010100101010011000110011101011000111110000101011010010}
iex(6)> IO.inspect big_bitmap, structs: false
%{__struct__: Bitmap, value: 12345678901234567890}
%Bitmap{12345678901234567890=
    1010101101010100101010011000110011101011000111110000101011010010}
  • List.Chars & String.Chars

bitmap_string.exs

defimpl String.Chars, for: Bitmap do
  def to_string(bitmap) do
    import Enum
    bitmap
    |> reverse
    |> chunk(3)
    |> map(fn three_bits -> three_bits |> reverse |> join end)
    |> reverse
    |> join("_")
  end
end
iex(1)> c("bitmap.exs")
[Bitmap]
iex(2)> c("bitmap_enumerable.exs")
[Enumerable.Bitmap]
iex(3)> c("bitmap_string.exs")
[String.Chars.Bitmap]


iex(4)> fifty = %Bitmap{value: 50}
%Bitmap{value: 50}
iex(5)> "Fifty in bits is: #{fifty}"
"Fifty in bits is: 110_010"

References

Programming Elixir



2018/5/14

Elixir 8 Macros


Macro 可能會讓程式碼更難閱讀,如果可以使用 function,就不要用 macro。本文以 erlang/elixir 不支援的 if 語法,說明如何以 macro 實作 if 語法。


if Statement


myif «condition» do
    «evaluate if true»
else
    «evaluate if false»
end

----

myif «condition»,
    do: «evaluate if true»,
    else: «evaluate if false

可用 function 實作 if 的語法


defmodule My do
    def myif(condition, clauses) do
        do_clause = Keyword.get(clauses, :do, nil)
        else_clause = Keyword.get(clauses, :else, nil)

    case condition do
        val when val in [false, nil]
            -> else_clause
        _otherwise
            -> do_clause
        end
    end
end

$ iex my.exs
iex(1)> My.myif 1==2, do: (IO.puts "1==2"), else: (IO.puts "1 != 2")
1==2
1 != 2
:ok

但結果是不正確的,因為 elixir 同時 evaluate do: 及 else:


Macro Inject Code


defmacro 定義 macro,當傳送參數給 macro,elixir 不會直接 evaluate,而是會以 tuple 方式發送程式碼。


defmodule My do
  defmacro macro(param) do
    IO.inspect param
  end
end

defmodule Test do
  require My

  # These values represent themselves
  My.macro :atom        #=> :atom
  My.macro 1            #=> 1
  My.macro 1.0          #=> 1.0
  My.macro [1,2,3]      #=> [1,2,3]
  My.macro "binaries"   #=> "binaries"
  My.macro { 1, 2 }     #=> {1,2}
  My.macro do: 1        #=> [do: 1]

  # And these are represented by 3-element tuples,以三個元素的 tuple 表示這些 macro

  My.macro { 1,2,3,4,5 }
  # =>  {:"{}",[line: 20],[1,2,3,4,5]}

  My.macro do: ( a = 1; a+a )
  # =>  [do:
  #      {:__block__,[],
  #        [{:=,[line: 22],[{:a,[line: 22],nil},1]},
  #         {:+,[line: 22],[{:a,[line: 22],nil},{:a,[line: 22],nil}]}]}]


  My.macro do
    1+2
  else
    3+4
  end
  # =>   [do: {:+,[line: 24],[1,2]},
  #       else: {:+,[line: 26],[3,4]}]

end

$ iex dumper.exs
:atom
1
1.0
[1, 2, 3]
"binaries"
{1, 2}
[do: 1]
{:{}, [line: 29], [1, 2, 3, 4, 5]}
[do: {:__block__, [],
  [{:=, [line: 32], [{:a, [line: 32], nil}, 1]},
   {:+, [line: 32], [{:a, [line: 32], nil}, {:a, [line: 32], nil}]}]}]
[do: {:+, [line: 40], [1, 2]}, else: {:+, [line: 42], [3, 4]}]

在一個 module 定義 macro,另一個 module 使用時,必須要先用 require,這樣才能確保 macro module 在目前這個 module 前先被編譯。




quote function


quote 可讓 code 保持尚未 evaluated 的形式。quote/2 能把 Elixir 代碼轉換成底層表示形式。


iex(1)> quote do: :atom
:atom
iex(2)> quote do: 1
1
iex(3)> quote do: 1.0
1.0
iex(4)> quote do: [1,2,3]
[1, 2, 3]
iex(5)> quote do: "binaries"
"binaries"
iex(6)> quote do: {1,2}
{1, 2}
iex(7)> quote do: [do: 1]
[do: 1]

iex(8)> quote do: {1,2,3,4,5}
{:{}, [], [1, 2, 3, 4, 5]}

iex(9)> quote do: (a = 1; a + a)
{:__block__, [],
 [{:=, [], [{:a, [], Elixir}, 1]},
  {:+, [context: Elixir, import: Kernel],
   [{:a, [], Elixir}, {:a, [], Elixir}]}]}

iex(10)> quote do: [ do: 1 + 2, else: 3 + 4]
[do: {:+, [context: Elixir, import: Kernel], [1, 2]},
 else: {:+, [context: Elixir, import: Kernel], [3, 4]}]



eg.exs


defmodule My do
  defmacro macro(code) do
    IO.inspect code
    # 會 evaluate code
    code
  end
end
defmodule Test do
  require My
  My.macro(IO.puts("hello"))
end

eg1.exs


defmodule My do
  defmacro macro(code) do
    IO.inspect code
    # 只會 evaluate do 裡面的 code,quota 裡面的 code 會回傳給呼叫 macro 的 code,然後被 evaluate
    quote do: IO.puts "Different code"
  end
end
defmodule Test do
  require My
  My.macro(IO.puts("hello"))
end

$ elixir eg.exs
{{:., [line: 17], [{:__aliases__, [counter: 0, line: 17], [:IO]}, :puts]},
 [line: 17], ["hello"]}
hello

$ elixir eg1.exs
{{:., [line: 17], [{:__aliases__, [counter: 0, line: 17], [:IO]}, :puts]},
 [line: 17], ["hello"]}
Different code



unquote function


知道了如何獲取代碼的內部表示,那怎麼修改它呢?可利用 unquote/1 來插入新的代碼和值。當我們 unquote 一個表達式的時候,會把它運行的結果插入到 AST。


iex(1)> denominator = 2
2
iex(2)> quote do: divide(42, denominator)
{:divide, [], [42, {:denominator, [], Elixir}]}
iex(3)> quote do: divide(42, unquote(denominator))
{:divide, [], [42, 2]}

Expanding a list using unquote_splicing


# insert [3,4]
iex(4)> Code.eval_quoted(quote do: [1,2,unquote([3,4])])
{[1, 2, [3, 4]], []}

# insert 3,4 到前面的 list
iex(5)> Code.eval_quoted(quote do: [1,2,unquote_splicing([3,4])])
{[1, 2, 3, 4], []}

# '1234' 是 lists of characters
iex(6)> Code.eval_quoted(quote do: [?a, ?= ,unquote_splicing('1234')])
{'a=1234', []}


iex(7)> fragment = quote do: IO.puts("hello")
{{:., [], [{:__aliases__, [alias: false], [:IO]}, :puts]}, [], ["hello"]}
iex(8)> Code.eval_quoted fragment
hello
{:ok, []}
iex(9)> Code.eval_string("[a, a*b, c]", [a: 2, b: 3, c: 4])
{[2, 6, 4], [a: 2, b: 3, c: 4]}



myif Macro


defmodule My do
  defmacro if(condition, clauses) do
    do_clause   = Keyword.get(clauses, :do, nil)
    else_clause = Keyword.get(clauses, :else, nil)
    quote do
      case unquote(condition) do
        val when val in [false, nil] -> unquote(else_clause)
        _                            -> unquote(do_clause)
      end
    end
  end
end

defmodule Test do
  require My
  My.if 1==2 do
    IO.puts "1 == 2"
  else
    IO.puts "1 != 2"
  end
end

$ elixir myif.ex
1 != 2

References


Programming Elixir