Friday, February 09, 2007

CAL, haskell(like) on JVM

CAL เป็นส่วนหนึ่งของ Open Quark Framework for Java
มี syntax และ feature ไกล้เคียงกับ haskell มาก
แต่ compile เป็น bytecode และ run บน java virtual machine

code ข้างล่างเป็นตัวอย่างการแปลง array เป็น java ArrayList
outputListWith :: [a] -> (a -> JObject) -> JList;
public outputListWith !list f =
let
javaList :: JList;
javaList = jArrayList_new;
outputListWithHelper :: [a] -> (a -> JObject) -> JList ->
JList;
outputListWithHelper !list elementMappingFunction !javaList =
case list of
[] -> javaList;
x : xs ->
if (jList_add javaList (elementMappingFunction x)) then
outputListWithHelper xs elementMappingFunction
javaList
else
error "Adding an element to a Java list failed.";
;
in
outputListWithHelper list f javaList;

ส่วน Quark Framework ยังอ่านไม่เข้าใจ
ดูเหมือนจะเป็นชุดเครื่องมือที่มุ่งเรื่อง reuse business logic
โดย define code unit เล็กๆที่เรียกว่า Gem (เขียนโดย CAL)
จากนั้นก็สามารถนำ Gem พวกนี้มาต่อๆกันเป็นสายของ process ได้

Related link from Roti

Thursday, February 08, 2007

Declarative with groovy

กำลังทำงานอยู่ชิ้นหนึ่ง งานนี้เป็นงานรับข้อมูลงบการเงินเข้ามา
แล้วทำการสรุปพวก financial ratio ต่างๆออกมา
โดยสูตรของการคำนวณขึ้นอยู่กับ context ของผู้ที่ส่งงบเข้ามาด้วย
(เช่น สูตร ratio ของงบการเงินของธนาคาร ย่อมต่างจาก งบการเงินของพวกโรงงาน)
รายละเอียดปลีกย่อยมียุบยับ แต่ละไว้แล้วกัน

ประเด็นก็คือ เราอยากเขียนโปรแกรมให้กำหนดสูตรในลักษณะ declarative ได้
แทนที่จะเขียนเป็น method หรือ class ใน java แบบปกติ
เมื่ออยากได้ solution ที่เป็น declarative ก็เลยต้องมองหาพวก script language เข้ามาช่วย
ก็เลยหยิบเอา groovy มาลองทดสอบดู

ในเบื้องต้น เรามีสูตรแบบนี้อยู่
current_ratio = สินทรัพย์หมุนเวียน / หนี้สินหมุนเวียน


เขียนเป็น groovy ตรงๆก็คือ
current_ratio = element('asset') / element('liabilities')

โดย method element คือ helper ที่ช่วยดึงข้อมูลจากงบการเงิน

แต่พอลองทดสอบดู ก็พบว่า เราไม่สามารถ bind method element เข้าไปตรงๆได้
ต้องแปลงสูตรให้เป็น
current_ratio = env.element('asset') / env.element('liabilities')

การ evaluate code นี้ทำได้โดย

Binding binding = new Binding();
binding.setVariable("env", helperObject);
GroovyShell shell = new GroovyShell(binding);
shell.evaluate(src);
float current_ratio = binding.getVariable("current_ratio");

ใช้งานได้ แต่ดูแล้ว การที่ต้องใช้ "env" ก่อนนั้นดูไม่งามเลย
นอกจากนี้ยังมีประเด็นเรื่อง fix ชื่อสูตรไว้ใน code ด้วย
ประกอบกับที่พอนำ idea ไปคุยกับ user, user บอกว่า
อยากให้ script มันสามารถ include กันได้ด้วย
เพราะว่า sector แต่ละ sector มันมี common ratio อยู่

ก็เลยออกแบบใหม่
เอา Closure เข้ามาช่วย
สุดท้ายได้หน้าตาประมาณนี้ออกมา
formulas.declare {
include 'common.script'

formula('current ratio') {
element('assets')/element('liabilities')
}

formula('quick ratio') {
element('cash') + .... / element('liabilities')
}
}

ซึ่งการ evaluate script นี้จะยังไม่ได้ผลลัพท์ทันที
แต่จะได้ Closure จำนวนหนึ่งออกมา ซึ่งเราจะเก็บไว้ก่อน
แล้วค่อยนำ Closure พวกนี้ไป run ใน context ใดๆที่เราต้องการ

code ที่ใช้อ่าน formula เข้ามา หน้าตาเป็นแบบนี้
พระเอกของเรื่องนี้คือ closure.setDelegate(..)
public FormulaSet load(String name) throws IOException {
final FormulaSet ret = new FormulaSet();
Binding binding = new Binding();
binding.setVariable("formulas", new Delegate() {

public void declare(Closure c) {
c.setDelegate(this);
c.call();
}

public void formula(String name, Closure c) {
Formula f = new FormulaImpl(name, c);
ret.put(name, f);
}

public void include(String name) throws IOException {
FormulaSet set = FormulaLoaderImpl.this.load(name);
ret.add(set);
}

});
GroovyShell shell = new GroovyShell(binding);
InputStream src = getClass().getClassLoader().
getResourceAsStream(scriptPath + name);
if (src == null) throw new IOException("script " + name + " not found!");
Script script = shell.parse(src);
script.run();
return ret;
}

Related link from Roti

Monday, February 05, 2007

ขั้นตอนการ compress javascript ของ dojo

มีคนเขียนมาถามว่า Dojo ใช้วิธีอะไรในการ compress javascript file
ลองไปดู build file ของเขากัน
<target name="-rhino-compress"
unless="nostrip">
<copy overwrite="true" file="${srcFile}" tofile="${dstFile}.uncompressed.js" />
<java jar="./lib/custom_rhino.jar" fork="true" output="${dstFile}">
<arg value="-c" />
<arg value="${srcFile}" />
</java>
</target>


จะเขาใช้ rhino เข้ามาช่วยครับ
แต่ไม่ใช่ rhino ธรรมดานะครับ เขาใช้ custom ของเขาเอง

คือตัว rhino มันคือ javascript interpreter
ซึ่งภายในต้องมี class ที่ทำหน้าที่ parse javascript อยู่แล้ว
เขาก็เลย hack เจ้า class นี้
เพิ่มส่วน writer ที่ write ออกมาในรูปแบบที่ต้องการ
(หลักการเดียวกันกับพวก pretty printer แต่แทนที่จะออกมาสวย
ก็ออกมาไม่สวยแทน)

เราสามารถเอา rhino-custom.jar ของเขามาใช้ใน project เรา
โดยเรียกใช้แบบนี้
java -jar custom_rhino.jar -c infile.js > outfile.js 2>&1


ลองดูตัวอย่าง code custom rhino ที่เขาเขียน
ตอนที่ print out เขาจะ check token ก่อน ว่าเป็นประเภทไหน
ถ้าเป็น NAME ก็จะทำการ compress
    case Token.NAME:

if(Token.OBJECTLIT == source.charAt(jumpPos)){
i = printSourceString(source, i + 1, false, result);
}else{
i = tm.printCompressed( source, i + 1, false, result, prevToken,
inArgsList, braceNesting);
}
continue;

case Token.STRING:
... # a lot of case

จะเห็นว่าเรียกใช้ method printCompressed
ตามไปดู printCompressed จะเห็นว่าเขาเช็คก่อน
ว่าตัวแปรที่กำลังจะ print นั้นเป็น ตัวแปรที่พึ่งประกาศใหม่
หรือตัวแปรที่ถูกอ้างใช้
public int printCompressed(String     source, 
int offset,
boolean asQuotedString,
StringBuffer sb,
int prevToken,
boolean inArgsList,
int currentLevel){

....
// ถ้ามี var นำหน้า หรือ อยู่ใน argement list
// ให้สร้าง mapping ขึ้นมาใหม่
if(((prevToken == Token.VAR)&&(!hasLocalTokenMapping(sourceStr)))||(inArgsList)){
newMapping = true;
}

str = this.getMappedToken(str, newMapping);
....


}


private String getMappedToken(String token, boolean newMapping){
String nt = null;
HashMap tokens = (HashMap)scopeReplacedTokens.get(scopeReplacedTokens.size()-1);
if(newMapping){
lastTokenCount++;
// ตั้งชื่อตัวแปร โดยมี _ นำหน้า
// แล้วตามด้วย เลข running ที่แปลงให้เป็น hex
nt = new String("_"+Integer.toHexString(lastTokenCount));
if(nt.length() >= token.length()){
nt = token;
}

tokens.put(token, nt);
return nt;
}
if(hasTokenMapping(token)){
return getTokenMapping(token);
}else{
return token;
}
}

Related link from Roti