2014年5月26日

erlang - edoc

對於一個 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

有四種

  1. edoc:application/2 產生一個 application 的文件
  2. edoc:packages/2 產生多個 packages 的文件
  3. edoc:files/2 產生數個 source files 的文件
  4. edoc:run/3 上面三個 functions 的最一般化的函數版本

overview page

在文件的目的目錄放置一個 overview.edoc 檔案,內容可以使用 Overview 與 Generic tags

Generic tags

可用在任何一個地方

  1. @clear
    會把前面所有 tags 都丟棄
  2. @docfile
    Reads a plain documentation file
  3. @end
    標記這是上一個 tag 的結尾
  4. @headerfile
    類似 @dodfile,通常是讀取 heade files: .hrl 檔案
  5. @todo 或 @TODO
    內容是 XHTML text,描述還沒有做完的工作
  6. @type
    a type declaration or definition

Overview tags

文件的目的目錄的 overview.edoc 檔案裡面可使用這些標籤

  1. @author
  2. @copyright
  3. @doc
  4. @reference
  5. @see
  6. @since
  7. @title
  8. @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 一樣

  1. @author
  2. @copyright
  3. @deprecated
  4. @doc
  5. @hidden
    不會出現在文件中,通常是 sample code, test modules
  6. @private
    標記這是 private module
  7. @reference
  8. @see
    refernce to a module, function, datatype, or application
  9. @since
  10. @version

Function tags

  1. @deprecated
  2. @doc
  3. @equiv
    等同於另一個 function call/expression
  4. @hidden
  5. @private
  6. @see
  7. @since
  8. @spec
    指定 function type
  9. @throws
  10. @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()

內建已經預先定義的型別是:

  1. any(): 指任何 erlang 的資料型別,term() 是 any() 的別名
  2. atom(), binary(), float(), function(), integer(), pid(), port(), reference(): erlang 的基本資料型別
  3. bool(): atom(true 或 false)
  4. char(): integer() 的子集合,代表字元
  5. iolist(): 遞迴地定義為 [char() | binary() | iolist()],通常用來產生高效率的字元輸出
  6. tuple()
  7. list(L): 是 [L] 的別名
  8. nil(): 就是 []
  9. string(): list(char()) 的別名
  10. depp_string(): 遞迴地定義為 [char()|deep_string()]
  11. 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 都有三種可能的形式

  1. TypeVar
    型別變數,這代表未知型別(跟 erlang 的變數無關)
  2. TypeVar::Type
    型別變數後面跟著一個型別
  3. 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。