對於一個 erlang library 來說,因為 erlang 習慣把 client 跟 server 使用的 code 都放進同一個 module 裡面,而且程式裡面只能利用 export 的宣告,來知道這個 module 提供什麼 API 介面,至於要怎麼使用,就只能靠文件說明來輔助。
edoc 是 erlang 程式的 document generator,它是因應 Java 語言裡 Javadoc 的概念啟發,語法跟 javadoc 類似。
多國語言
因為 erlang R17 才預設 erl 原始程式碼都要使用 utf8 編碼,以 R16 的狀況,我們必須根據 Using Unicode in Erlang 文件的說明,因為 R16B 預設coding 為 bytewise (or latin1) encoding,如果在 module 的程式碼裡面要寫上中文字,而檔案要存成 utf-8 時,erl module source file 的第一行就必須要寫上
%% -*- coding: utf-8 -*-
如果沒有加上這個,產生 edoc 的文件就會直接變成亂碼。
edoc 實例分析
我們以 rabbitmq erlang client 為例,直接看一個正式的 erlang library 是怎麼使用 edoc。
首先,要先下載原始程式碼,通常我們會直接在官方網站 下載 原始程式碼,目前是 rabbitmq-server-3.2.4.tar.gz,但是下載後,查閱程式碼,Makefile 裡面只有很簡單的編譯語法,因此,我們就改變一下,到 rabbitmq-erlang-client github 下載程式碼,這裡的 Makefile 就有比較完整的 edoc 處理的資訊。
關於文件的部份,首先要修改 Makefile 前面的 VERSION 變數,原本是 0.0.0,但應該修改成 3.2.4。
# Makefile
VERSION=3.2.4
###############################################################################
## Documentation
###############################################################################
documentation: $(DOC_DIR)/index.html
$(DOC_DIR)/overview.edoc: $(SOURCE_DIR)/overview.edoc.in
mkdir -p $(DOC_DIR)
sed -e 's:%%VERSION%%:$(VERSION):g' < $< > $@
$(DOC_DIR)/index.html: $(DEPS_DIR)/$(COMMON_PACKAGE_DIR) $(DOC_DIR)/overview.edoc $(SOURCES)
$(LIBS_PATH) erl -noshell -eval 'edoc:application(amqp_client, ".", [{preprocess, true}, {macros, [{edoc, true}]}])' -run init stop
相關的變數是放在 common.mk 裡面,並直接由 Makefile 引用
# common.mk
SOURCE_DIR=src
DIST_DIR=dist
DEPS_DIR=deps
DOC_DIR=doc
......
先直接執行看看,直接 make 或 make all 都沒有看到 doc 文件的結果,所以要再 make documentation
> make documentation
mkdir -p doc
sed -e 's:%%VERSION%%:3.2.4:g' < src/overview.edoc.in > doc/overview.edoc
ERL_LIBS=deps:dist erl -noshell -eval 'edoc:application(amqp_client, ".", [{preprocess, true}, {macros, [{edoc, true}]}])' -run init stop
第一步是建立 doc 目錄
第二步是把 src/overview.edoc.in 移動到 doc/overview.edoc,同時修改版本號碼的內容
第三步是使用 edoc module 裡的 application,ERL_LIBS是 erl 接受的環境變數值,內容是erl執行時,所需要的其他 library 的目錄。接下來就 -eval 一段 erlang srcipt,最後再執行 init module 的 stop method 把erl關掉。
ERL_LIBS=deps:dist erl 的寫法,是因為 bash IFS 預設值為 space, a tab and a newline,寫在同一行,可以讓 erl 使用一個暫時的環境變數 ERL_LIBS,卻又不會永久影響該環境變數。
別的執行方式
> erl -noshell -run edoc_run packages '[""]' '[{dir, "doc"},{source_path, ["src"]}]'
在 Windows 因為參數 parsing 的問題,參數兩頭必須都要用雙引號,因此上面的指令要調成
erl -noshell -run edoc_run packages "[\"\"]" "[{dir, \"doc\"},{source_path, [\"src\"]}]"
在 linux 環境,單引號或雙引號的寫法都可以運作。
也可以這樣執行:
erl -noshell -run edoc_run application "'rabbitmq erlang client'" "\".\"" "[{def,{vsn,\"3.2.4\"}},{source_path, [\"src\"]}]"
edoc module
edoc_run 裡面有把如何從 erlang startup 的時候,就直接呼叫 edoc。但因為 rabbitmq 例子是從 shell 執行一段 script,所以要直接看 edoc 的文件,script是執行 edoc:applation/3 這個 method。
application(Application::atom(), Dir::filename(), Options::proplist()) -> ok
文件會被輸出到第二個參數目錄下的 doc 子目錄。preprocess 設定可讓所有 source files 都先讓 erlang preprocessor (epp) 先處理一次,當程式裡面使用了 macro 的時候,就一定要打開 preprocess 設定。同時要在 macros 列出所需要的 macro definitions,也就是要使用 edoc 裡面定義的巨集。
amqp_rpc_client.erl
amqp_rpc_cient edoc 是 edoc 的頁面,每個文件都有三個部份 (1) Description (2) Function Index (3) Function Details
對應到 amqp_rpc_client.erl 原始程式碼,最前面是版權宣告,在一行空白之後,就是 %% @doc 開頭的一段文字,這段文字的第一句話,會自動變成文件一開始的簡介,然後這一整段文字,都會成為 (1) Description 的內容。
接下來是 API 的部份,amqp_rpc_client 有三個 public function,每個function都有 %% @spec 與 %% @doc 兩段內容,這兩個部份會成為文件的 (3) Function Details 的段落。
而(2) Function Index可看到三個 public function 的 table。
後面的 function 都沒有 @spec 與 @doc 的說明,甚至到了 callback function ,還標記了 %% @private,這表示這些 function 屬於 library 內部使用,使用者不需要在文件上知道這些 function 的說明。
%% The contents of this file are subject to the Mozilla Public License
%% Version 1.1 (the "License"); you may not use this file except in
%% compliance with the License. You may obtain a copy of the License at
%% http://www.mozilla.org/MPL/
%% ...
%% @doc This module allows the simple execution of an asynchronous RPC over
%% AMQP. It frees a client programmer of the necessary having to AMQP
%% plumbing. .....
-module(amqp_rpc_client).
...
%%--------------------------------------------------------------------------
%% API
%%--------------------------------------------------------------------------
%% @spec (Connection, Queue) -> RpcClient
%% where
%% Connection = pid()
%% Queue = binary()
%% RpcClient = pid()
%% @doc Starts a new RPC client instance that sends requests to a
%% specified queue. This function returns the pid of the RPC client process
%% that can be used to invoke RPCs and stop the client.
start(Connection, Queue) ->
{ok, Pid} = gen_server:start(?MODULE, [Connection, Queue], []),
Pid.
...
%%--------------------------------------------------------------------------
%% gen_server callbacks
%%--------------------------------------------------------------------------
%% Sets up a reply queue and consumer within an existing channel
%% @private
init([Connection, RoutingKey]) ->
....
edoc tags
在 edoc 可使用的完整 tag 列表,可在 edoc user's guide 裡面看到,tags 有分 generic, overview, module, function 四種,文件中也刻意把最複雜的 @spec @type 獨立一個章節 Type specifications。
running edoc
有四種
- edoc:application/2 產生一個 application 的文件
- edoc:packages/2 產生多個 packages 的文件
- edoc:files/2 產生數個 source files 的文件
- edoc:run/3 上面三個 functions 的最一般化的函數版本
overview page
在文件的目的目錄放置一個 overview.edoc 檔案,內容可以使用 Overview 與 Generic tags
Generic tags
可用在任何一個地方
- @clear
會把前面所有 tags 都丟棄 - @docfile
Reads a plain documentation file - @end
標記這是上一個 tag 的結尾 - @headerfile
類似 @dodfile,通常是讀取 heade files: .hrl 檔案 - @todo 或 @TODO
內容是 XHTML text,描述還沒有做完的工作 - @type
a type declaration or definition
Overview tags
文件的目的目錄的 overview.edoc 檔案裡面可使用這些標籤
- @author
- @copyright
- @doc
- @reference
- @see
- @since
- @title
- @version
以 amqp erlang client 為例,overview.edoc 內容為
@title AMQP Client for Erlang
@author GoPivotal Inc. <support@rabbitmq.com>
@copyright 2007-2013 GoPivotal, Inc.
@version 3.2.4
@reference <a href="http://www.rabbitmq.com/protocol.html" target="_top">AMQP documentation</a> on the RabbitMQ website.
@doc
== Overview ==
....
%% ```f(X) ->
%% case X of
%% ...
%% end'''
== Overview == 是 h3 的縮寫,就像是 Markdown 一樣
== Heading ==
=== Sub-heading ===
==== Sub-sub-heading ====
另外這個語法
```...'''
會自動變成 pre,以免跟 xml tag 有衝突
<pre><![CDATA[...]]></pre>
Module tags
這些用在 module 宣告中,有很多標籤都跟 Overview tags 一樣
- @author
- @copyright
- @deprecated
- @doc
- @hidden
不會出現在文件中,通常是 sample code, test modules - @private
標記這是 private module - @reference
- @see
refernce to a module, function, datatype, or application - @since
- @version
Function tags
- @deprecated
- @doc
- @equiv
等同於另一個 function call/expression - @hidden
- @private
- @see
- @since
- @spec
指定 function type - @throws
- @type
函數規格 @spec
在 function 中,參數與回傳值的資料型別並不是清楚地寫在 function 的定義上,我們需要一個方式,來告訴使用這個函數的progarmmer,該怎麼使用它。erlang 社群開發了一種記號法,但這個記號法並不是 erlang 程式碼的一部分,而只是一種寫文件的工具。
這個記號法只能用在文件上,在程式碼中,會用 %% 將該行視為註解。通常會這樣寫 %% @spec
-module(math)
-export([fac/1]).
%% @spec fac(int()) -> int().
fac(0) -> 1;
fac(N) -> N * fac(N-1).
在使用此型別記號法時,要定義兩件事:型別與函數的規格。
定義型別
名稱為 typeName 的型別會寫成 typeName()
內建已經預先定義的型別是:
- any(): 指任何 erlang 的資料型別,term() 是 any() 的別名
- atom(), binary(), float(), function(), integer(), pid(), port(), reference(): erlang 的基本資料型別
- bool(): atom(true 或 false)
- char(): integer() 的子集合,代表字元
- iolist(): 遞迴地定義為 [char() | binary() | iolist()],通常用來產生高效率的字元輸出
- tuple()
- list(L): 是 [L] 的別名
- nil(): 就是 []
- string(): list(char()) 的別名
- depp_string(): 遞迴地定義為 [char()|deep_string()]
- none(): 沒有資料型別,用在不會產生回傳值的函數,例如無窮的接收迴圈,表示此函數不會返回
使用者自己定義型別可以寫成
@type newType() = TypeExpression
範例
@type onOff() = on|off.
@type person() = {person, name(), age()}.
@type people() = [person()].
@type name() = {firstname, string()}.
@type age() = integer().
指定函數的輸入與輸出型別
寫法為
@spec fuinctionName(T1, T2, ..., Tn) -> Tret
T1, T2, ..., Tn 是 參數的型別, Tret 是回傳值的資料型別
每個 T 都有三種可能的形式
- TypeVar
型別變數,這代表未知型別(跟 erlang 的變數無關) - TypeVar::Type
型別變數後面跟著一個型別 - Type
型別表示式
範例
@spec file:open(FileName, Mode) -> {ok, Handle} | {error, Why}.
@spec file:read_line(Handle) -> {ok, Line} | eof.
file:open/2 意思是,要開啟 FileName,會取得回傳值 {ok, Handle} 或是 {error, Why}
FileName 跟 Mode 是型別變數,但我們不知道它確切的型別是什麼。
範例
@spec lists:map(fun(A)->B, [A]) -> [B].
@spec lists:filter(fun(X) -> bool(), [X]) -> [X].
範例
@spec file:open(FileName::string(), [mode()]) -> {ok, Handle::file_handle()} | {error, Why::string()}.
@type mode() = read|write|compressed|raw|binary| ...
範例
@spec file:open(string(), Modes) -> {ok, Handle} | {error, string()}
Handle() = file_handle(),
Modes = [Mode],
Mode = read|write|compressed|raw|binary| ...
範例
@spec file:open(string(), [mode()]) -> {ok,file_handle()} | error().
@type error() = {error, string()}.
@type mode() = read|write|compressed|raw|binary| ...
where
也可以用變數,搭配 where 來寫函數的定義
Spec ::= FunType "where"? DefList? | FunctionName FunType "where"? DefList?
範例
%% @spec (Connection, Queue) -> RpcClient
%% where
%% Connection = pid()
%% Queue = binary()
%% RpcClient = pid()
上面的範例寫法,是先填上變數名稱,第二行之後,再加上 where 來更明確地限制每一個變數實際上的資料型別。
結論
對於 erlang 來說,applcation 的標準文件 edoc 是很重要的,我們必須透過 edoc 才能知道怎麼使用這個 library。
沒有留言:
張貼留言