Sunday, May 28, 2006

Erlang

หนึ่งในคำแนะนำของบทความ Teach Yourself Programming in Ten Years
ก็คือ
Learn at least a half dozen programming languages. Include one language that supports class abstractions (like Java or C++), one that supports functional abstraction (like Lisp or ML), one that supports syntactic abstraction (like Lisp) ...

เนื่องจากผมยังรู้ไม่ครบ 6 ภาษา
Erlang ก็เลยเป็นหนึ่งในภาษาที่ผมสนใจ
เพราะคิดว่าน่าจะสามารถนำมาประยุกต์กับงานที่ทำอยู่ได้ (server side)
เนื่องจาก Erlang มีชื่อเสียงในด้าน Concurrency, Distribution, Soft real-time
(อ่านโฆษณาของเขาได้ใน White Paper)

วิธีที่ดีที่สุดในการเรียนรู้ภาษาใหม่ก็คือ
การหาตัวอย่างการใช้งานจริง ที่เรารู้วิธี implement อยู่ก่อนแล้ว
ตัวอย่างที่ผมใช้สำหรับการเรียน Erlang ครั้งแรกก็คือ
Pushing events to the browser via Ajax
จาก blog BLUISH CODER
ซึ่งเป็นตัวอย่างการของใช้ long-lived ajax connection
มา implement server push

ตัวอย่างของเขา เขาใช้ yaws เป็น server
yaws ย่อมาจาก "Yet Another WebServer"
เป็น server ที่เขียนด้วย Erlang
โดยออกแบบมา เพื่อให้ serve dynamic page ที่เขียนด้วย Erlang
แต่การตอบสนองต่อ static page ก็ทำได้ดีมากเลย
(ไม่แพ้ apache)

มาลองดูตัว code กัน
(ขอแนะนำว่าให้ใช้ เอกสาร document-part-1 อ่านประกอบไปด้วย)
ในตัวอย่างที่ download มา เขาจะเริ่มต้นด้วยการ start process ขึ้นมา 2 process
process แรกทำหน้าที่ generate random page id เพื่อใช้จำแนก page ที่ต่อเข้ามายัง server
ส่วน process ที่สอง ทำหน้าที่เป็น register process
ที่ใช้ลงทะเบียนว่า ตอนนี้มี page ไหนกำลังต่อมาที่ server อยู่บ้าง
%% Start all processes used by this module.
start() ->
register(page_registry, spawn(html_rpc, page_registry, [[]])),
register(random_name, spawn(html_rpc, random_name, [])).

Note: คำสั่ง spawn ก็คือการแตก process ออกมา
ผลลัพท์ที่ได้จากคำสั่ง spawn ก็คือ pid (process id)
จะถูก map เข้ากับ naming ผ่านทางคำสั่ง register
การที่เราต้อง map เลข pid เข้ากับ name ก็เพื่อให้สะดวกต่อการ
ติดต่อกับ process นั้นๆ


การทำงานเริ่มจาก user request ขอหน้าจอหลักเข้ามา
เราก็จะทำการ generate page_id แปะกลับไปด้วย
<html>
<head>
<title>html_rpc Test1</title>
<script type="text/javascript" src="html_rpc.js"></script>
</head>
<body>
<script type="text/javascript">
<erl>
# out เป็นชื่อ function ที่ใช้ output ผลลัพท์ (เหมือนพวก System.out, Writer, หรือพวก output stream ใน java)
# แต่อันนี้เขียนใน style ของ callback
# ก็คือ yaws จะเรียกใช้ out เมื่อต้องการผลลัพท์
out(A) ->
Name = html_rpc:get_random_name(),
# เก็บตัวแปรไว้ใน scope ของ Page
# เพื่อที่ dynamic block อื่นๆ จะสามารถใช้ ตัวแปรนี้ได้
put(page_name, Name),
{html, "var page = '" ++ Name ++ "';"}.
</erl>
function process_message(message) {
message()
}
html_rpc_receive("ajax1.yaws?page="+page, process_message);
</script>

Note: วิธีการเขียน dynamic html page
ของ yaws ก็เหมือนด้วย jsp หรือ rhtml
นั่นคือมีลักษณะเป็น template


เมื่อ browser ได้รับ page แล้ว ก็จะเกิดการ evaluate javascript
ซึ่งจะทำการ call กลับมายัง browser ผ่านทาง XmlHttpRequest
โดยมีการ pass ค่า page id มาด้วย

ที่ฝั่ง server เมื่อได้รับ request ของ ajax มา
ก็จะแตก process ออกมาอีก เพื่อรองรับ request นั้นๆ

<erl>
out(A) ->
# เริ่มด้วยการ get request parameter ที่ pass มาจาก browser
# เก็บไว้ในตัวแปรชื่อ Page
{ok,Page} = queryvar(A, "page"),
# เรียกใช้ function start_ajax_handler ใน module html_rpc
# โดย pass function handler ไปด้วย (callback function)
html_rpc:start_ajax_handler(Page, fun(A,B,C) -> handler(A,B,C) end).

# function นี้จะทำการ generate javascript ส่งกลับไปยัง browser
handler(Yaws_Pid, Page, Message) ->
case Message of
{alert, Value} ->
"function() { alert('" ++ Value ++ "'); }"
end.

</erl>

ตามไปดู function start_ajax_handler
start_ajax_handler(Page, Callback) ->
# เก็บค่า pid ของตัวเองไว้ใน variable Yaws_Pid
Yaws_Pid = self(),
# แตก process อีกแล้ว โดยเรียกใช้ anonymous function
spawn(fun() ->
# ส่ง message ไปยัง process ที่ register ไว้ในชื่อ page_registry
# โดย pass tuple ที่มี name 'add' , process id ของตัวเอง
# เครื่องหมาย ! เป็น syntax ที่ใช้ในการส่ง message ไปยัง process
page_registry ! {add, Page, self()},
# เรียกใช้ function ที่ทำหน้าที่รับ message ที่ส่งมาให้ process นี้
ajax_handler(Yaws_Pid, Page, Callback)
end),
# อันนี้คือ ผลลัพท์ที่ตอบกลับไปยัง browser เลยทันที (เนื่องจากคำสั่ง spawn มันจะ return กลับมาทันที
[{header, {cache_control, "no-cache"}},
{streamcontent, "text/html", "[ "}].

หน้าตาของ function ajax_handler ที่ทำหน้ารอรับ message ที่จะ push กลับไปยัง browser
ajax_handler(Yaws_Pid, Page, Callback) ->
# receive เป็นการ lookup message queue ของตัวเอง
receive
# ถ้ามี message ที่ชื่อ stop ก็ให้หยุดการทำงาน
# โดยการ close stream ที่เปิดมาจาก browser
%% Stop the iframe handler and inform the HTML page we are
%% finished streaming.
stop ->
yaws_api:stream_chunk_deliver(Yaws_Pid, "true, true ]"),
yaws_api:stream_chunk_end(Yaws_Pid),
page_registry ! {remove, {pid, self()}};
# X แทน any message
# ลักษณะการทำงาน จะเป็นการ delegate message ต่อไปยัง Callback function
# เสร็จแล้วก็ให้จบ process และ unregister page id
X ->
Result = Callback(Yaws_Pid, Page, X),
yaws_api:stream_chunk_deliver(Yaws_Pid, "false, " ++ Result ++ " ]"),
yaws_api:stream_chunk_end(Yaws_Pid),
page_registry ! {remove, {pid, self()}}
# อันนี้สิ สุดยอด
# เพื่อให้รักษา long-lived connection ไว้
# ถ้าครบ 5 วินาที แล้วยังไม่มี message มา
# ก็ให้ส่ง newline กลับไปยัง browser
# after เป็น syntax ที่ใช้ระบุพวก timeout
after 5000 ->
case yaws_api:stream_chunk_deliver_blocking(Yaws_Pid, "\n") of
ok -> ajax_handler(Yaws_Pid, Page, Callback);
{error,{ypid_crash,_}} -> page_registry ! {remove, {pid, self()}}
end
end.

หลังจากที่ browser load page ไป แล้วทำการเปิด connection รอ server push แล้ว
ถ้า server ต้องการ push ข้อมูลกลับ ก็จะเรียกใช้คำสั่งนี้
html_rpc:send("443584617445720273715201", {alert, "Hello World"}).

ตัวเลขยาวๆนั่นคือ page id
การทำงานของ send ก็คือ

%% Send a message to the page.
send(Page, Message) ->
# เรียกขอ process id ที่รอรับ messagae จาก page_registry
# ให้สังเกตุว่า ไม่มีการ return ค่ากลับ แต่ใช้วิธี รอรับ message ที่ตอบกลับมาจาก page_registry
page_registry ! {self(), get, Page},
# รอรับ messagae จาก page_registry
receive
{result, {_, Pid}} ->
# หลังจากได้ pid จาก page_registry
# ก็จะ dispatch ส่ง message ไปยัง process นั้น
Pid ! Message
end.


โอยอธิบายยากจริงๆ
10 ปากว่า ไม่เท่าลงมือทำ
ลอง run ดูแล้วจะเห็นการทำงานของมันครับ

Related link from Roti

No comments: