Tuesday, January 05, 2010

Dynamically generate code in Erlang

ปัจจุบัน application framework ทั้งหลาย พยายามจะทำให้ชีวิตเราง่ายขึ้น ด้วยการลด noise ที่เราไม่จำเป็นต้องเห็น, generate code ที่จำเป็นต้องใช้ให้เรา

Chicago Boss Framework ก็อยู่ในกระแสนี้เช่นกัน ตัว Relation Mapping layer ของมันก็พยายามจะลดรูปให้เหลือน้อยที่สุด, คำถามสำหรับผมก็คือ ใน Erlang นี่เขาใช้เทคนิคอะไรมาช่วย generate code หรือทำ magic บ้าง

ลองดูตัวอย่างการใช้งานก่อน เริ่มด้วยการ define Domain model
-module(blog_post, [Id, Title, Text, AuthorId]).
-compile(export_all).
-belongs_to(author).

-module(author, [Id, Name]).
-compile(export_all).
-has_many(blog_posts).

เทคนิคแรกที่เขาใช้ก็คือ Parameterized Module ซึ่งช่วยให้ Module มีพฤติกรรมในลักษณะ OOP ได้
ลองดู code ตอนที่เรา new instance domain ของเรา
FakeAuthor = (author:new(id, "YOUR NAME")):save(),
BlogPost = blog_post:new(id,
"BLOG TITLE",
"BLOG CONTENT",
FakeAuthor:id()),
SavedBlogPost = BlogPost:save(),

จะเห็นว่า module ของเรามี function "save", "getter"(ตรงที่ get id จาก fakeauthor) เพิ่มขึ้นมาให้เองโดยที่เราไม่ต้องเขียน คำถามก็คือ เขาใช้เทคนิคอะไรในการ generate code ส่วนนี้

เริ่มแรกสุด code ในส่วน module นี้จะไม่ load ขึ้นมาผ่านกลไกปกติ แต่จะทำผ่านกลไกของตัวเอง โดยเริ่มต้น มันจะทำการ parse erlang file โดยใช้ function epp:parse_file ผลที่ได้เราเรียกว่า Form

ทดลองใช้ epp:parse_file กับ ตัวอย่างโปรแกรมที่น้องป้อเขียน
-module(p).
-export([start/0, say_something/2]).
say_something(What, 0) ->
done;
say_something(What, Times) ->
io:format("~p~n", [What]),
say_something(What, Times - 1).
start() ->
spawn(tut14, say_something, [hello, 3]),
spawn(tut14, say_something, [goodbye, 3]).


จะได้ Form ที่มีหน้าตาประมาณนี้
{ok,[{attribute,1,file,{"p.erl",1}},
{attribute,1,module,p},
{attribute,2,export,[{start,0},{say_something,2}]},
{function,3,say_something,2,
[{clause,3,
[{var,3,'What'},{integer,3,0}],
[],
[{atom,4,done}]},
{clause,5,
[{var,5,'What'},{var,5,'Times'}],
[],
[{call,6,
{remote,6,{atom,6,io},{atom,6,format}},
[{string,6,"~p~n"},{cons,6,{var,...},{...}}]},
{call,7,
{atom,7,say_something},
[{var,7,'What'},{op,7,'-',...}]}]}]},
{function,8,start,0,
[{clause,8,[],[],
[{call,9,
{atom,9,spawn},
[{atom,9,tut14},
{atom,9,say_something},
{cons,9,{...},...}]},
{call,10,
{atom,10,spawn},
[{atom,10,tut14},
{atom,10,say_something},
{cons,10,...}]}]}]},
{eof,11}]}

จะเห็นว่ามีลักษณะเป็น Abstract Syntax Tree + Meta Data

พอได้ form มา เจ้า ChicagoBoss ก็จะทำการแทรก,แปลง code ให้เป็นไปตามต้องการ โดยมันจะใช้ function ใน library erl_syntax ช่วยในการ generate Form

ตัวอย่าง code ในส่วนที่สร้าง getter
parameter_getter_forms(Parameters) ->
lists:map(fun(P) ->
erl_syntax:add_precomments([erl_syntax:comment(
[lists:concat(["% @spec ", parameter_to_colname(P), "() -> ", P]),
lists:concat(["% @doc Returns the value of `", P, "'"])])],
erl_syntax:function(
erl_syntax:atom(parameter_to_colname(P)),
[erl_syntax:clause([], none, [erl_syntax:variable(P)])]))
end, Parameters).

parameter_setter_forms(ModuleName, Parameters) ->
lists:map(
fun(P) ->
erl_syntax:add_precomments([erl_syntax:comment(
[
lists:concat(["% @spec ", parameter_to_colname(P), "( ", P, "::",
case lists:suffix("Time", atom_to_list(P)) of
true -> "tuple()";
false -> "string()"
end, " ) -> ", inflector:camelize(atom_to_list(ModuleName))]),
lists:concat(["% @doc Set the value of `", P, "'."])])],
erl_syntax:function(
erl_syntax:atom(parameter_to_colname(P)),
[erl_syntax:clause([erl_syntax:variable("NewValue")], none,
[
erl_syntax:application(
erl_syntax:atom(ModuleName),
erl_syntax:atom(new),
lists:map(
fun
(Param) when Param =:= P ->
erl_syntax:variable("NewValue");
(Other) ->
erl_syntax:variable(Other)
end, Parameters))
])]))
end, Parameters).


พอ transform code เสร็จ ก็จะทำการ compile เป็น binary ด้วย function compile:forms

compile เสร็จก็จัดการ load เข้า runtime โดยใช้ function code:load_binary

Related link from Roti

2 comments:

iporsut said...

งงดีจริงๆครับ ผมลองเอา ChicagoBoss มาลงบน snow leopard แล้ว start server ได้แต่มี error เรื่อง erlang:universaltime_to_localtime/1 ก็เลยยังไปต่อไม่ได้เลยครับ

polawat phetra said...

พี่แกะแต่ code ไม่ได้ลอง run ทั้งอัน ขี้เกียจลง tokyotyrant