檢視原始碼 Erlang I/O 協定

Erlang 中的 I/O 協定使得客戶端與伺服器之間能夠進行雙向通訊。

  • I/O 伺服器是一個處理請求並在例如 I/O 裝置上執行請求任務的程序。
  • 客戶端是任何希望從 I/O 裝置讀取或寫入資料的 Erlang 程序。

常見的 I/O 協定自 OTP 開始就存在,但一直沒有文件記錄,並且多年來不斷演進。在 Robert Virding 的基本原理的附錄中,描述了原始的 I/O 協定。本節描述了目前的 I/O 協定。

原始的 I/O 協定簡單且彈性。對記憶體效率和執行時間效率的需求,多年來觸發了對協定的擴展,使得協定比原始的協定更大且更難以實作。當然可以說目前的協定太複雜了,但本節描述的是它今天的樣子,而不是它應該是什麼樣子。

原始協定的基本概念仍然成立。I/O 伺服器和客戶端使用一個相當簡單的協定進行通訊,並且客戶端中永遠不會存在伺服器狀態。任何 I/O 伺服器都可以與任何客戶端程式碼一起使用,並且客戶端程式碼不需要知道 I/O 伺服器與之通訊的 I/O 裝置。

協定基礎

如 Robert 的論文中所述,I/O 伺服器和客戶端使用 io_request/io_reply 元組進行通訊,如下所示:

{io_request, From, ReplyAs, Request}
{io_reply, ReplyAs, Reply}

客戶端向 I/O 伺服器發送一個 io_request 元組,伺服器最終發送一個相應的 io_reply 元組。

  • From 是客戶端的 pid/0,也就是 I/O 伺服器向其發送 I/O 回覆的程序。

  • ReplyAs 可以是任何資料,並在相應的 io_reply 中返回。io 模組監控 I/O 伺服器,並使用監控參考作為 ReplyAs 資料。更複雜的客戶端可以向同一個 I/O 伺服器發出許多未完成的 I/O 請求,並且可以使用不同的參考(或其他東西)來區分傳入的 I/O 回覆。I/O 伺服器應將元素 ReplyAs 視為不透明。

    請注意,I/O 伺服器的 pid/0 並未明確出現在元組 io_reply 中。回覆可以從任何程序發送,而不一定是實際的 I/O 伺服器。

  • RequestReply 描述如下。

當 I/O 伺服器收到 io_request 元組時,它會根據 Request 部分執行操作,並最終發送具有相應 Reply 部分的 io_reply 元組。

輸出請求

要在 I/O 裝置上輸出字元,存在以下 Request

{put_chars, Encoding, Characters}
{put_chars, Encoding, Module, Function, Args}
  • Encodingunicodelatin1,表示字元(如果是二進制資料)編碼為 UTF-8 或 ISO Latin-1(純位元組)。如果列表元素包含大於 255 的整數,且 Encoding 設定為 latin1,一個運作正常的 I/O 伺服器也應返回錯誤指示。

    請注意,這並未以任何方式說明如何在 I/O 裝置上放置字元或由 I/O 伺服器處理字元。不同的 I/O 伺服器可以以任何想要的方式處理字元,這僅告訴 I/O 伺服器預期資料的格式。在 Module/Function/Args 的情況下,Encoding 表示指定函式產生的格式。

    另請注意,面向位元組的資料使用 ISO Latin-1 編碼發送最簡單。

  • Characters 是要放置在 I/O 裝置上的資料。如果 Encodinglatin1,則這是一個 iolist/0。如果 Encodingunicode,則這是 Erlang 標準混合 Unicode 列表(每個字元在列表中為一個整數,二進制資料中的字元表示為 UTF-8)。

  • ModuleFunctionArgs 表示呼叫以產生資料的函式(例如 io_lib:format/2)。

    Args 是該函式的參數列表。該函式將以指定的 Encoding 產生資料。I/O 伺服器應呼叫該函式,如 apply(Mod, Func, Args),並將返回的資料放置在 I/O 裝置上,就像它是在 {put_chars, Encoding, Characters} 請求中傳送的一樣。如果該函式返回二進制資料或列表以外的任何內容,或拋出異常,則應將錯誤發送回客戶端。

I/O 伺服器使用 io_reply 元組回覆客戶端,其中元素 Reply 是以下其中之一:

ok
{error, Error}
  • Error 向客戶端描述錯誤,客戶端可以隨意處理它。io 模組通常「按原樣」返回它。

輸入請求

要從 I/O 裝置讀取字元,存在以下 Request

{get_until, Encoding, Prompt, Module, Function, ExtraArgs}
  • Encoding 表示如何將資料發回客戶端,以及將哪些資料發送到由 Module/Function/ExtraArgs 表示的函式。如果提供的函式返回的資料為列表,則資料將轉換為此編碼。如果提供的函式以其他格式返回資料,則無法進行轉換,並且由客戶端提供的函式以正確的方式返回資料。

    如果 Encodinglatin1,則盡可能將整數 0..255 的列表或包含純位元組的二進制資料發回客戶端。如果 Encodingunicode,則將包含整個 Unicode 範圍內整數的列表或以 UTF-8 編碼的二進制資料發送給客戶端。使用者提供的函式始終看到整數列表,而不是二進制資料,但如果 Encodingunicode,則列表可以包含大於 255 的數字。

  • Prompt 是字元列表(非混合,沒有二進制資料)或一個原子,將作為 I/O 裝置上輸入的提示輸出。Prompt 通常被 I/O 伺服器忽略;如果設定為 '',則始終將其忽略(並且不會寫入任何內容到 I/O 裝置)。

  • ModuleFunctionExtraArgs 表示一個函式和參數,以確定何時寫入了足夠的資料。該函式應採用兩個參數,最後的狀態和字元列表。該函式應返回以下其中之一:

    {done, Result, RestChars}
    {more, Continuation}

    Result 可以是任何 Erlang 術語,但如果它是 list/0,則如果 I/O 伺服器設定為二進制模式(請參閱下文),則 I/O 伺服器可以在將其返回給客戶端之前將其轉換為適當格式的 binary/0

    該函式會使用 I/O 伺服器在其 I/O 裝置上找到的資料進行呼叫,並返回以下其中之一:

    • {done, Result, RestChars} 當讀取足夠的資料時。在這種情況下,Result 會被發送到客戶端,並且 RestChars 會保留在 I/O 伺服器中作為稍後輸入的緩衝區。
    • {more, Continuation},表示需要更多字元才能完成請求。

    Continuation 會在有更多字元可用時作為稍後呼叫該函式的狀態發送。當沒有更多字元可用時,該函式必須返回 {done, eof, Rest}。初始狀態為空列表。當 I/O 裝置達到檔案結尾時的資料為原子 eof

    使用以下函式可以(低效地)實作 get_line 請求的模擬:

    -module(demo).
    -export([until_newline/3, get_line/1]).
    
    until_newline(_ThisFar,eof,_MyStopCharacter) ->
        {done,eof,[]};
    until_newline(ThisFar,CharList,MyStopCharacter) ->
        case
            lists:splitwith(fun(X) -> X =/= MyStopCharacter end,  CharList)
        of
      {L,[]} ->
                {more,ThisFar++L};
      {L2,[MyStopCharacter|Rest]} ->
          {done,ThisFar++L2++[MyStopCharacter],Rest}
        end.
    
    get_line(IoServer) ->
        IoServer ! {io_request,
                    self(),
                    IoServer,
                    {get_until, unicode, '', ?MODULE, until_newline, [$\n]}},
        receive
            {io_reply, IoServer, Data} ->
          Data
        end.

    請注意,呼叫函式時,Request 元組中的最後一個元素([$\n])會附加到參數列表中。該函式將由 I/O 伺服器呼叫,如 apply(Module, Function, [ State, Data | ExtraArgs ])

使用以下 Request 請求固定數量的字元:

{get_chars, Encoding, Prompt, N}
  • EncodingPromptget_until 相同。
  • N 是要從 I/O 裝置讀取的字元數。

使用以下 Request 請求單行(如先前範例中所示):

{get_line, Encoding, Prompt}
  • EncodingPromptget_until 相同。

顯然,get_charsget_line 可以使用 get_until 請求實作(實際上它們最初就是這樣實作的),但是對效率的需求使得這些新增功能成為必要。

I/O 伺服器使用 io_reply 元組回覆客戶端,其中元素 Reply 是以下其中之一:

Data
eof
{error, Error}
  • Data 是讀取的字元,以列表或二進制資料形式(取決於 I/O 伺服器模式,請參閱下一節)。
  • 當達到輸入結束且沒有更多資料可供客戶端程序使用時,將返回 eof
  • Error 向客戶端描述錯誤,客戶端可以隨意處理它。io 模組通常按原樣返回它。

I/O 伺服器模式

從 I/O 伺服器讀取資料時,對效率的需求不僅導致了 get_lineget_chars 請求的增加,還增加了 I/O 伺服器選項的概念。沒有強制要求實作任何選項,但 Erlang 標準函式庫中的所有 I/O 伺服器都支援 binary 選項,這允許 io_reply 元組的元素 Data可能的情況下為二進位制,而不是列表。如果資料以二進位制形式傳送,Unicode 資料將以標準 Erlang Unicode 格式(即 UTF-8)傳送(請注意,無論 I/O 伺服器模式如何,get_until 請求的功能仍然會取得列表資料)。

請注意,get_until 請求允許使用一個函式,其中指定的資料始終為列表。此外,此類函式的傳回值資料可以是任何類型(當向 I/O 伺服器傳送 io:fread/2,3 請求時確實是這種情況)。客戶端必須準備好接收以各種形式作為這些請求的答案的資料。但是,I/O 伺服器應在可能的情況下將結果轉換為二進位制(也就是說,當提供給 get_until 的函式傳回列表時)。這在帶註解和可運作的 I/O 伺服器範例一節的範例中說明。

二進位制模式下的 I/O 伺服器會影響傳送給客戶端的資料,因此客戶端必須能夠處理二進位制資料。為了方便起見,可以使用以下 I/O 請求設定和檢索 I/O 伺服器的模式

{setopts, Opts}
  • Optsproplists 模組(以及 I/O 伺服器)識別的格式的選項列表。

例如,互動式 shell 的 I/O 伺服器(在 group.erl 中)理解以下選項

{binary, boolean()} (or binary/list)
{echo, boolean()}
{expand_fun, fun()}
{encoding, unicode/latin1} (or unicode/latin1)

選項 binaryencoding 對於 OTP 中的所有 I/O 伺服器都是通用的,而 echoexpand 僅對此 I/O 伺服器有效。選項 unicode 通知字元如何在實體 I/O 裝置上放置,也就是說,終端本身是否支援 Unicode。它不會影響字元在 I/O 協定中的傳送方式,其中每個請求都包含所提供或傳回資料的編碼資訊。

I/O 伺服器將以下其中之一作為 Reply 傳送

ok
{error, Error}

如果 I/O 伺服器不支援該選項(例如,如果向純文字檔案的 setopts 請求中傳送了 echo 選項),則預期會出現錯誤(最好是 enotsup)。

要檢索選項,請使用以下請求

getopts

此請求會要求 I/O 伺服器支援的所有選項的完整列表以及其目前值。

I/O 伺服器會回覆

OptList
{error, Error}
  • OptList 是元組 {Option, Value} 的列表,其中 Option 始終是原子。

多個 I/O 請求

Request 元素本身可以使用以下格式包含多個 Request

{requests, Requests}
  • Requests 是協定的有效 io_request 元組的列表。它們必須按照列表中出現的順序執行。執行將繼續,直到其中一個請求導致錯誤或列表被消耗完。最後一個請求的結果將傳回給客戶端。

對於請求列表,I/O 伺服器可以根據列表中的請求,在回覆中傳送以下任何有效結果

ok
{ok, Data}
{ok, Options}
{error, Error}

選用的 I/O 請求

以下 I/O 請求是選用的,客戶端應準備好接收錯誤傳回

{get_geometry, Geometry}
  • Geometry 是原子 rows 或原子 columns

I/O 伺服器將以下其中之一作為 Reply 傳送

N
{error, Error}
  • N 是 I/O 裝置具有的字元行數或列數,如果適用於 I/O 伺服器處理的 I/O 裝置,否則 {error, enotsup} 是一個好的答案。

未實作的請求類型

如果 I/O 伺服器遇到無法識別的請求(也就是說,io_request 元組具有預期的格式,但 Request 未知),I/O 伺服器應傳送帶有錯誤元組的有效回覆

{error, request}

這樣可以擴充使用選用請求的協定,並讓客戶端在某種程度上向後相容。

帶註解和可運作的 I/O 伺服器範例

I/O 伺服器是任何能夠處理 I/O 協定的程序。沒有通用的 I/O 伺服器行為,但可能會有。該框架很簡單,一個處理傳入請求的程序,通常是 I/O 請求和其他 I/O 裝置特定的請求(定位、關閉等)。

範例 I/O 伺服器將字元儲存在 ETS 表格中,組成一個相當粗略的 RAM 檔案。

該模組以常用的指令開始,一個啟動 I/O 伺服器的函式和一個處理請求的主迴圈

-module(ets_io_server).

-export([start_link/0, init/0, loop/1, until_newline/3, until_enough/3]).

-define(CHARS_PER_REC, 10).

-record(state, {
	  table,
	  position, % absolute
	  mode % binary | list
	 }).

start_link() ->
    spawn_link(?MODULE,init,[]).

init() ->
    Table = ets:new(noname,[ordered_set]),
    ?MODULE:loop(#state{table = Table, position = 0, mode=list}).

loop(State) ->
    receive
	{io_request, From, ReplyAs, Request} ->
	    case request(Request,State) of
		{Tag, Reply, NewState} when Tag =:= ok; Tag =:= error ->
		    reply(From, ReplyAs, Reply),
		    ?MODULE:loop(NewState);
		{stop, Reply, _NewState} ->
		    reply(From, ReplyAs, Reply),
		    exit(Reply)
	    end;
	%% Private message
	{From, rewind} ->
	    From ! {self(), ok},
	    ?MODULE:loop(State#state{position = 0});
	_Unknown ->
	    ?MODULE:loop(State)
    end.

主迴圈接收來自客戶端的消息(客戶端可以使用 io 模組傳送請求)。對於每個請求,都會呼叫函式 request/2,並最終使用函式 reply/3 傳送回覆。

"私有"消息 {From, rewind} 會將偽檔案中的目前位置重設為 0("檔案"的開頭)。這是一個典型的 I/O 裝置特定消息,不屬於 I/O 協定的範例。通常將此類私有消息嵌入 io_request 元組中是一個壞主意,因為這會讓讀者感到困惑。

首先,我們檢查回覆函式

reply(From, ReplyAs, Reply) ->
    From ! {io_reply, ReplyAs, Reply}.

它會將 io_reply 元組傳回給客戶端,並提供請求中收到的元素 ReplyAs 以及請求的結果,如前所述。

我們需要處理一些請求。首先是寫入字元的請求

request({put_chars, Encoding, Chars}, State) ->
    put_chars(unicode:characters_to_list(Chars,Encoding),State);
request({put_chars, Encoding, Module, Function, Args}, State) ->
    try
	request({put_chars, Encoding, apply(Module, Function, Args)}, State)
    catch
	_:_ ->
	    {error, {error,Function}, State}
    end;

Encoding 說明請求中的字元如何表示。我們希望將字元以列表形式儲存在 ETS 表格中,因此我們使用函式 unicode:characters_to_list/2 將它們轉換為列表。轉換函式可以方便地接受編碼類型 unicodelatin1,因此我們可以直接使用 Encoding

當提供 ModuleFunctionArguments 時,我們會套用它,並對結果執行相同的操作,就像直接提供資料一樣。

我們處理檢索資料的請求

request({get_until, Encoding, _Prompt, M, F, As}, State) ->
    get_until(Encoding, M, F, As, State);
request({get_chars, Encoding, _Prompt, N}, State) ->
    %% To simplify the code, get_chars is implemented using get_until
    get_until(Encoding, ?MODULE, until_enough, [N], State);
request({get_line, Encoding, _Prompt}, State) ->
    %% To simplify the code, get_line is implemented using get_until
    get_until(Encoding, ?MODULE, until_newline, [$\n], State);

在這裡,我們作弊了一下,或多或少只實作了 get_until,並使用內部輔助程式來實作 get_charsget_line。在生產程式碼中,這可能效率不高,但這取決於不同請求的頻率。在我們開始實作函式 put_chars/2get_until/5 之前,我們先檢查剩下的幾個請求

request({get_geometry,_}, State) ->
    {error, {error,enotsup}, State};
request({setopts, Opts}, State) ->
    setopts(Opts, State);
request(getopts, State) ->
    getopts(State);
request({requests, Reqs}, State) ->
     multi_request(Reqs, {ok, ok, State});

請求 get_geometry 對於此 I/O 伺服器沒有意義,因此回覆為 {error, enotsup}。我們處理的唯一選項是 binary/list,這是在單獨的函式中完成的。

多重請求標籤 (requests) 在單獨的迴圈函式中處理,該函式會依序套用列表中的請求,並傳回最後一個結果。

如果無法識別請求,則必須傳回 {error, request}

request(_Other, State) ->
    {error, {error, request}, State}.

接下來,我們處理不同的請求,首先是相當通用的多重請求類型

multi_request([R|Rs], {ok, _Res, State}) ->
    multi_request(Rs, request(R, State));
multi_request([_|_], Error) ->
    Error;
multi_request([], Result) ->
    Result.

我們一次迴圈遍歷一個請求,當我們遇到錯誤或列表耗盡時停止。最後一個傳回值會傳回給客戶端(它首先傳回到主迴圈,然後由函式 io_reply 傳回)。

請求 getoptssetopts 也很容易處理。我們只會變更或讀取狀態記錄

setopts(Opts0,State) ->
    Opts = proplists:unfold(
	     proplists:substitute_negations(
	       [{list,binary}],
	       Opts0)),
    case check_valid_opts(Opts) of
	true ->
	        case proplists:get_value(binary, Opts) of
		    true ->
			{ok,ok,State#state{mode=binary}};
		    false ->
			{ok,ok,State#state{mode=binary}};
		    _ ->
			{ok,ok,State}
		end;
	false ->
	    {error,{error,enotsup},State}
    end.
check_valid_opts([]) ->
    true;
check_valid_opts([{binary,Bool}|T]) when is_boolean(Bool) ->
    check_valid_opts(T);
check_valid_opts(_) ->
    false.

getopts(#state{mode=M} = S) ->
    {ok,[{binary, case M of
		      binary ->
			  true;
		      _ ->
			  false
		  end}],S}.

按照慣例,所有 I/O 伺服器都處理 {setopts, [binary]}{setopts, [list]}{setopts,[{binary, boolean()}]},因此使用了 proplists:substitute_negations/2proplists:unfold/1 的技巧。如果傳送給我們的選項無效,我們會將 {error, enotsup} 傳回給客戶端。

請求 getopts 應傳回 {Option, Value} 元組的列表。這具有雙重功能,既提供此 I/O 伺服器的目前值,又提供可用選項。我們只有一個選項,因此傳回該選項。

到目前為止,此 I/O 伺服器相當通用(除了主迴圈中處理的請求 rewind 和建立 ETS 表格之外)。大多數 I/O 伺服器都包含類似於此伺服器的程式碼。

為了讓範例可執行,我們開始實作將資料讀取和寫入 ETS 表格的功能。首先是函式 put_chars/3

put_chars(Chars, #state{table = T, position = P} = State) ->
    R = P div ?CHARS_PER_REC,
    C = P rem ?CHARS_PER_REC,
    [ apply_update(T,U) || U <- split_data(Chars, R, C) ],
    {ok, ok, State#state{position = (P + length(Chars))}}.

我們已經將資料作為(Unicode)列表,因此只需將列表分割成預定義大小的執行次數,並將每次執行放在表格中的目前位置(並向前)。函式 split_data/3apply_update/2 在下面實作。

現在我們要從表格中讀取資料。函式 get_until/5 會讀取資料並套用函式,直到函式表示已完成。結果會傳回給客戶端

get_until(Encoding, Mod, Func, As,
	  #state{position = P, mode = M, table = T} = State) ->
    case get_loop(Mod,Func,As,T,P,[]) of
	{done,Data,_,NewP} when is_binary(Data); is_list(Data) ->
	    if
		M =:= binary ->
		    {ok,
		     unicode:characters_to_binary(Data, unicode, Encoding),
		     State#state{position = NewP}};
		true ->
		    case check(Encoding,
		               unicode:characters_to_list(Data, unicode))
                    of
			{error, _} = E ->
			    {error, E, State};
			List ->
			    {ok, List,
			     State#state{position = NewP}}
		    end
	    end;
	{done,Data,_,NewP} ->
	    {ok, Data, State#state{position = NewP}};
	Error ->
	    {error, Error, State}
    end.

get_loop(M,F,A,T,P,C) ->
    {NewP,L} = get(P,T),
    case catch apply(M,F,[C,L|A]) of
	{done, List, Rest} ->
	    {done, List, [], NewP - length(Rest)};
	{more, NewC} ->
	    get_loop(M,F,A,T,NewP,NewC);
	_ ->
	    {error,F}
    end.

在這裡,我們還處理可以使用請求 setopts 設定的模式(binarylist)。預設情況下,所有 OTP I/O 伺服器都會將資料以列表形式傳回給客戶端,但如果 I/O 伺服器以適當的方式處理,則將模式切換為 binary 可以提高效率。get_until 的實作很難提高效率,因為提供的函式定義為將列表作為引數,但 get_charsget_line 可以針對二進位制模式進行最佳化。但是,此範例不會最佳化任何內容。

重要的是,回傳的資料類型必須根據設定的選項而正確。因此,我們會在回傳前,盡可能將列表轉換為具有正確編碼的二進制資料。在 get_until 請求元組中提供的函數,其最終結果可以回傳任何內容,因此只有回傳列表的函數才能將其轉換為二進制資料。如果請求包含編碼標籤 unicode,則列表可以包含所有 Unicode 碼位,而二進制資料應為 UTF-8 編碼。如果編碼標籤為 latin1,則用戶端只會收到範圍在 0..255 的字元。如果編碼指定為 latin1,函數 check/2 會負責不回傳列表中任意的 Unicode 碼位。如果函數不回傳列表,則無法執行檢查,且結果將會是所提供函數的原始輸出,不做任何變更。

為了操作表格,我們實作了以下實用函數

check(unicode, List) ->
    List;
check(latin1, List) ->
    try
	[ throw(not_unicode) || X <- List,
				X > 255 ],
	List
    catch
	throw:_ ->
	    {error,{cannot_convert, unicode, latin1}}
    end.

如果用戶端請求 latin1,但函數回傳了 Unicode 碼位 > 255 的字元,則函數 check 會提供錯誤元組。

函數 until_newline/3until_enough/3 是輔助函數,與函數 get_until/5 一起使用,以實現 get_charsget_line (效率較低)。

until_newline([],eof,_MyStopCharacter) ->
    {done,eof,[]};
until_newline(ThisFar,eof,_MyStopCharacter) ->
    {done,ThisFar,[]};
until_newline(ThisFar,CharList,MyStopCharacter) ->
    case
        lists:splitwith(fun(X) -> X =/= MyStopCharacter end,  CharList)
    of
	{L,[]} ->
            {more,ThisFar++L};
	{L2,[MyStopCharacter|Rest]} ->
	    {done,ThisFar++L2++[MyStopCharacter],Rest}
    end.

until_enough([],eof,_N) ->
    {done,eof,[]};
until_enough(ThisFar,eof,_N) ->
    {done,ThisFar,[]};
until_enough(ThisFar,CharList,N)
  when length(ThisFar) + length(CharList) >= N ->
    {Res,Rest} = my_split(N,ThisFar ++ CharList, []),
    {done,Res,Rest};
until_enough(ThisFar,CharList,_N) ->
    {more,ThisFar++CharList}.

如您所見,上述函數正是要在 get_until 請求中提供的函數類型。

要完成 I/O 伺服器,我們只需要以適當的方式讀寫表格

get(P,Tab) ->
    R = P div ?CHARS_PER_REC,
    C = P rem ?CHARS_PER_REC,
    case ets:lookup(Tab,R) of
	[] ->
	    {P,eof};
	[{R,List}] ->
	    case my_split(C,List,[]) of
		{_,[]} ->
		    {P+length(List),eof};
		{_,Data} ->
		    {P+length(Data),Data}
	    end
    end.

my_split(0,Left,Acc) ->
    {lists:reverse(Acc),Left};
my_split(_,[],Acc) ->
    {lists:reverse(Acc),[]};
my_split(N,[H|T],Acc) ->
    my_split(N-1,T,[H|Acc]).

split_data([],_,_) ->
    [];
split_data(Chars, Row, Col) ->
    {This,Left} = my_split(?CHARS_PER_REC - Col, Chars, []),
    [ {Row, Col, This} | split_data(Left, Row + 1, 0) ].

apply_update(Table, {Row, Col, List}) ->
    case ets:lookup(Table,Row) of
	[] ->
	    ets:insert(Table,{Row, lists:duplicate(Col,0) ++ List});
	[{Row, OldData}] ->
	    {Part1,_} = my_split(Col,OldData,[]),
	    {_,Part2} = my_split(Col+length(List),OldData,[]),
	    ets:insert(Table,{Row, Part1 ++ List ++ Part2})
    end.

表格是以 ?CHARS_PER_REC 大小的區塊讀寫,必要時會覆寫。實作顯然效率不高,但它能正常運作。

本範例到此結束。它完全可執行,您可以使用例如 io 模組或甚至 file 模組來讀寫 I/O 伺服器。在 Erlang 中實作一個功能完善的 I/O 伺服器就是這麼簡單。