Thursday, December 22, 2005

JavaScriptGenerator (RJS Template)

JavaScriptGenerator เป็น Plugin ของ Rails
ที่ช่วยให้เราเขียน ajax ได้สะดวกขึ้น
ลองดูตัวอย่าง

สมมติว่าเราต้องการให้มีหน้าจอ Project Listing
ที่ user สามารถ add project ใหม่ๆได้



เมื่อ user กด add ก็จะแสดง form ขึ้นมา



เมื่อ user กด "add" ก็จะ validate
ว่า project name เป็นค่าว่างหรือไม่
ถ้า validate ผ่าน ก็จะ update "project listing" ให้



กรณีีที่ไม่ผ่าน
ก็จะ แสดง error message



ถ้า user กด cancel ก็จะซ่อน form
และ error message

มาดูว่า ถ้าเขียนโดยใช้ JavaScriptGenerator แล้ว
จะต้องทำอย่างไรบ้าง

ขั้นที่ 1 ไป download plugin มาติดตั้งให้เรียบร้อยก่อน
svn ของ RJS Template อยู่ที่
http://wiki.rubyonrails.org/rails/pages/Plugins

ขั้นที่ 2 implement controller
เริ่มทำหน้าจอ listing ก่อน
class RjsController < ApplicationController

def index
list
render :action => 'list'
end

def list
@projects = Project.find(:all)
end

end

โดย rhtml จะมีหน้าตาแบบนี้
<h4>Listing projects</h4>
<ul id="list">
<% for project in @projects %>
<li>
<%=h project.name %></td>
</li>
<% end %>
</ul>

<div id="post_error">
</div>

<div id="input" style="display:none">
<%= form_remote_tag (
:url => { :action => :new }) %>
<label for="name">Name :</label>
<%= text_field_tag ('name', '', :id=>'name-field') %>
<%= submit_tag "add", :id=>'project-add'%>
<%= end_form_tag %>
<%= link_to_remote ('Cancel',
:url => {:action => :cancel_new}) %>
</div>
<br />

<div id="add_link">
<%= link_to_remote ('New project',
:url => { :action => :display_new }) %>
</div>

จะสังเกตเห็นว่า เรามี ul
ที่มี id => "list"
และก็มี div block
id => "post_error" ไว้แสดง error message
id => "input" ส่วนของ ajax form
id => "add_link" ส่วน link ที่เป็นจุดเริ่มต้นการทำงาน

ขั้นที่ 3 เมื่อ user เลือก link "new project"
ก็จะเกิด ajax request "display_new" ไปที่ server
(จริงๆแล้ว ขั้นนี้ทำเป็น javascript ธรรมดาก็ได้
แต่อยากลอง feature ก็เลยทำเป็น ajax)
controller เราก็จะ define method ดังนี้
class RjsController < ApplicationController

...

def display_new
end

end

ส่วน view ที่ render กลับ
เราจะใช้ rjs template เข้ามา render แทน
โดยแทนที่จะใช้ file ที่นามสกุล rhtml
เราจะตั้งเป็นนามสกุล rjs แทน
page.show('input')
page.hide('add_link')
page.visual_effect :highlight, 'input', :duration => 3

จาก code ข้างบน rjs จะทำการ generate
javascript ตอบกลับไป
โดย page.show ก็คือสั่งให้ DOM Element ที่ต้องการ show ขึ้นมา
ส่วน visual effect ก็คือ ไปเรียกใช้ Mir.aculo.us script ให้ทำงาน

ขั้นที่ 4 ถ้า user submit form เข้ามา
class RjsController < ApplicationController

...

def new
if params[:name] == nil or params[:name] == ""
render :action => :new_error
else

## do some business logic here.

render :action => :new_success
end
end

end

โดย rjs template กรณี success จะเป็นดังนี้
page.insert_html :bottom, 'list',
content_tag("li", params[:name])
page.visual_effect :highlight, 'list', :duration => 3

กรณีที่มี error ก็จะเป็นดังนี้
page.replace_html 'post_error',
'Project Name must specific!!!'
page.visual_effect :highlight, 'post_error', :duration => 3


ดูๆแล้วก็ง่ายดีเหมือนกัน
ถ้าอยากรู้ว่าง่ายแค่ไหน ก็ต้องลองเขียน version ที่ไม่ใช้ rjs
เปรียบเทียบกันดู
ผมลองแล้ว แต่ไม่บอก :)

Related link from Roti

CLisp Package Installer?

วันนี้อยากลองเล่น Web Application Framework
ของ CLisp ดูบ้าง ตัวที่อยากลองเล่นมีี่ชื่อว่า KPAX
ก็เลยเข้าไปหาทาง download มาเล่นดู

ในคู่มือของ KPAX ก็อธิบายไว้ว่า คุณควรจะใช้ ASDF
("Another System Definition Facility") ในการติดตั้ง
ASDF ถ้าเปรียบเทียบให้เห็นภาพ ก็คือ software ประเภทเดียวกับ apt-get, fink, gems

เอาก็เอา ทดลอง install asdf
หมดเวลาไป ชั่วโมงกว่า
เพราะใส่ path ผิด ไม่มีเครื่องหมาย "/" ปิดท้าย

จากนั้นก็เริ่ม install
คำสั่ง install ก็ดูง่ายดี

(asdf-install:install "http://weitz.de/files/cl-ppcre.tar.gz")

(สั่ง run จากใน lisp เลย)

หลายจาก screen วูบวาบอยู่สักระยะหนึ่ง
ก็มี error ว่า

*** - Server responded 404 for GET http://www.cliki.net/s-http-server?download
The following restarts are available:
RETRY :R1 Retry installation


โฮ่ๆ dependency ของ CPAX มันอ้างถึง
s-http-server ที่ดันย้ายที่อยู่ของตัวเองไปแล้ว
จัดแจง search หาว่า มันย้ายไปไหน
ได้มาแล้ว ก็มานั่งคิดต่อว่า แล้วกูจะทำไงต่อดีวะ
เคยได้ยินมาว่า lisp เวลา error แล้ว เราสามารถ
เข้าไปเปลี่ยนแปลง definition หรือ code ส่วนที่ผิดได้
แล้วก็ให้มัน run ต่อได้เลย
(ไม่รู้จำผิดหรือเปล่า เคยได้ยินประมาณว่า
พวก software ในยานสำรวจอวกาศ
มันเกิด bug แล้วมาติดอยู่ที่หน้าจอแบบนี้เหมือนกัน
พวก programmer ก็เลยต้องเข้าไปแก้ แล้วก็ run ต่อจากจุดเดิมได้)

หลังจากนั่งจิ้มเป็น ลิงจิ้ม keyboard อยู่พักหนึ่ง
ก็ยอมแพ้ เปลี่ยนเป็นเลือก abort
แล้วก็สั่ง install s-http-server ก่อนแทน

(asdf-install:install "http://homepage.mac.com/svc/s-http-server/s-http-server.tar.gz")


ฮาๆ เหมือนเดิม s-http-server ก็มี dependency ที่ย้ายหนีไปแล้ว
อยู่เต็มไปหมดเลย
นั่งใล่ install dependency ไปเรื่อยๆ
สุดท้ายก็ค้นเจอ page นี้
page นี้มีตารางอธิบายว่า อะไร depend กับ อะไรบ้าง
คราวนี้ง่ายแล้ว ก็ใล่ install จากตัวย่อยสุดขึ้นมาทีละตัว

เฮ้อ! มิน่าไม่มีใครเขาใช้ clisp กัน
แค่ติดตั้ง ก็ต้องทดสอบความอดทนของผู้ใช้ด้วย

Related link from Roti

User User User

ตัดมาจาก "Creating Passionate Users"
เขาเขียนถึงเรื่อง "The Quantum Mechanics of Users"
ซึ่งเขาอ้างมาจากหนังสือ Blink ของ Malcolm Gladwell อีกที
But it's not just the subatomic world that gets weird when you look too closely--in some cases, asking a user to explain his choices changes his choices! In his book Blink, Malcolm Gladwell (author of The Tipping Point) gives an example where students were asked to rank order 44 different kinds of strawberry jams. When compared with the rankings of experts, the students did fairly well -- "even those of us who aren't jam experts known good jam when we taste it." But--and here's where it gets weird--when the students were asked in advance to provide not just the rankings but a written explanation of their choices, the student rankings lost virtually all correlation with that of the experts. As Gladwell puts it, "By making people think about jam, [the researchers] turned them into jam idiots."


อันบนนี้ไม่เลว น่าสนใจ
แต่เจอประโยคนี้ก่อน
The comments about listening to what the users are saying, what they're not saying, and how it's being said reminds me of the quote by Claude Debussy, "Music is the silence between the notes."

ไอ้ประโยคเรื่อง music นี้ผมเคยได้ยินมาก่อนนะ
แต่ไม่เคยคิดจะเอามาเกี่ยวโยงกับ user requirement ได้เลย

Related link from Roti

Wednesday, December 21, 2005

JGoodies, Swing Data Binding

สืบเนื่องจากเรื่อง JFace Data Binding
bact' ถามว่าไม่มี swing binding บ้างหรือ?

ขอแนะนำ JGoodies
ใน JGoodies จะมี subproject อยู่ดังนี้
  • animation - Time-based real-time animations in Java
  • binding - Data Binding Framework
  • forms - Forms layout system
  • looks - look & feel
  • validation - Validation Framework

ตัวที่เราสนใจก็คือ binding

ไม่พูดพล่ามทำเพลง ลองดูตัวอย่าง code ที่ทำการ binding
เริ่มที่ domain ก่อน
public class Person {

private String name;
private String address;
private String telNo;
private BigDecimal salary;

... // setter & getter

}

ให้ salary เป็น BigDecimal เพื่อที่จะได้ทดสอบ case ที่ต้อง convert ด้วย

Swing Form paint โดยใช้ Matisse ใน netbeans 5.0
โคตรใช้ง่ายเลย สวรรค์ดีๆนี่เอง



ลอง binding แบบง่ายๆดู
private javax.swing.JTextArea textAddress;
private javax.swing.JTextField textName;
private javax.swing.JTextField textSalary;
private javax.swing.JTextField textTelNo;
...
...
Bindings.bind(textName, new PropertyAdapter(person, "name"));
Bindings.bind(textAddress, new PropertyAdapter(person, "address"));
Bindings.bind(textTelNo, new PropertyAdapter(person, "telNo"));


ในส่วนของ salary ที่เป็น BigDecimal
ก็ต้องมีการเขียน Wrapper ขึ้นมา เพื่อใช้ convert
โดย JGoodies เตรียม class AbstractConverter ให้เราแล้ว
class SalaryConverter extends AbstractConverter {

public SalaryConverter(ValueModel subject) {
super(subject);
}

public Object convertFromSubject(Object object) {
BigDecimal value = (BigDecimal) object;
if (value == null) {
return "";
}
return value.toString();
}

public void setValue(Object object) {
if (object != null) {
BigDecimal tmp = new BigDecimal((String) object);
subject.setValue(tmp);
} else {
subject.setValue(new BigDecimal("0.00"));
}
}

}

Note: code นี้เป็นการทดลอง implement
ดังนั้นการทำงานจะไม่สมบูรณ์ ไม่สามารถรองรับได้ครบทุก case


เมื่อเตรียม converter ได้ ก็ทำการ binding ด้วยคำสั่งนี้
Bindings.bind(textSalary,
new SalaryConverter(
new PropertyAdapter(person, "salary")));

จะเห็นว่าสามารถนำ adpater มา chain ต่อกันได้

ลองทดสอบอีก โดยเพิ่มการ validate salary เข้าไป
คราวนี้เราจะใช้ class AbstractVetoableValueModel เข้ามาช่วย
Note: สมมติว่า JGoodies ไม่มี framework
ในส่วนของ validation เราก็เลยต้อง implement เอง

class ValidSalary extends AbstractVetoableValueModel {

public ValidSalary(ValueModel subject) {
super(subject);
}

public boolean proposedChange(Object oldValue, Object newValue) {
try {
BigDecimal tmp = new BigDecimal((String) newValue);
textSalary.setBackground(Color.white);

return true;
} catch (Exception e) {
textSalary.setBackground(Color.red);
}
return false;
}


}


code ในการ binding ของเรา ก็จะเปลี่ยนเป็น
Bindings.bind(textSalary,
new ValidSalary(
new SalaryConverter(
new PropertyAdapter(person, "salary"))));


จบดื้อๆ
ใครสนใจลองไปอ่านดูได้
ขอแนะนำ presentation นี้
แล้วจะเห็นว่า JGoodies มีอะไรหลายๆอย่างที่น่าสนใจทีเดียว
(รวมถึงเรื่อง architecture ของ framework
ที่ดูน่าสนใจทีเดียว)

Related link from Roti

Monday, December 19, 2005

Homebrew CPU

สุดยอด
http://www.homebrewcpu.com/

Magic-1 is a homebuilt minicomputer. It doesn't use an off-the-shelf microprocessor, but rather has a custom CPU made out of 74 Series TTL chips. Altogether there are more than 200 chips in Magic-1 connected together with thousands of individually wrapped wires. And, it works. Not only the hardware, but there's also a full ANSI C compiler for Magic-1 (retargeted LCC), and a rudimentary homebrew operating system.


ปล. ในหน้าแรกอย่าลืมอ่าน P.S. กับ P.P.S.
ตลกดี

Related link from Roti

Programming Language

อ่านเจอใน Jonas Boner
เรื่อง "Java Killed The Innovation of Computer Language"

Java killed the innovation of computer languages.


AspectJ is one of the few innovations in computer languages the last years.


in general nothing has really happened the last 10 years. We have seen many new scripting languages popping up lately (Ruby, Python, Jython, groovy etc.) and even though most of them are both fun and useful - non of them are really innovating, merely reusing and sometimes reshaping old ideas

Related link from Roti

JFace Data binding

ได้ยินข่าวแว่วๆมาสักพักแล้ว
อ่านเจอใน The visual Editor
ว่าตอนนี้ Eclipse3.2M4 มี api ของ JFace Data binding Framework แล้ว
โดยมี api ชุดนี้จะ finalize ที่ version3.2M5

ลองดูตัวอย่างว่า ถ้าเรา binding class person เข้ากับ textfield
แล้วจะต้องเขียนประมาณไหน

Composite top = new Composite(parent, SWT.NONE);
Text firstName = new Text(top, SWT.BORDER);
Person person = new Person();

IDataBindingContext dbc = DataBinding.createContext(top);
dbc.bind(firstName, new Property(person, "firstName"), null);

Related link from Roti

Sunday, December 18, 2005

Fitnesse

ว่าจะเขียนถึง Testing Framework ตัวนี้มานานแล้ว
วันนี้เห็นว่าเขาปรับปรุงหน้า web page ของเขาสวยงามดีแล้ว
ก็เลยได้ฤกษ์ทดลองเสียที

ถ้าสงสัยว่ามันต่างจากพวก xUnit อย่างไร
ก็ลองดู quote นี้

xUnit: Building the Code Right

Fitnesse: Building the Right Code

jUnit เป็น Tool ที่ programmer ใช้ช่วยให้
ตัวเอง(และคนอื่น) มั่นใจว่า code ทำงานได้ถูกต้อง
แต่คำว่าทำงานได้ถูกนั้น มันมีความหมายอื่นอีก
ก็คือ "ทำงานได้ถูกต้องตามที่ลูกค้าต้องการ"

FitNesse ก็คือ tool ที่เข้ามาช่วยทำ Acceptance test
โดยการใช้ wiki เข้ามาเป็นช่องทางในการสื่อสารระหว่าง
ลูกค้า กับ developer โดยลูกค้าจะเป็นคน
กำหนดข้อมูลตัวอย่างว่า อะไรที่ควรทำได้ และอะไรที่ไม่ควรทำ
ไว้ใน wiki page
จากนั้นก็สั่งให้ fitnesse run test (จากการ click ใน wiki นั้นเองเลย)
ซึ่งผลลัพท์ที่ได้จากการ run ก็จะนำมา render เปรียบเทียบกับ
ค่าที่ลูกค้าคาดหวังว่าจะได้ ซึ่งเป็นการ feedback
ถึง status ของโปรแกรม ว่าทำงานถูกต้องหรือไม่

ลองมาดูตัวอย่างการใช้งานแบบง่ายๆดู
  • เริ่มด้วย customer สร้าง page ที่กำหนด ค่า input และ output
    ที่คาดหวังว่าจะเป็นก่อน
    (สามารถสร้างใน excel แล้วค่อย import เข้ามาก็ได้)



  • tester จะ edit page เพื่อใส่ class ที่รับผิดชอบ testcase นี้
    รวมทั้งกำหนด classpath ด้วย



  • หน้าตา java code ที่อยู่เบื้องหลัง test case นี้
    import fit.ColumnFixture;

    public class Wage extends ColumnFixture {

    public String givenWage;

    private BigDecimal getGivenWage() {
    return new BigDecimal(givenWage).setScale(2);
    }

    public String roundWage() {
    BigDecimal tmp = getGivenWage();
    ...
    return tmp.toString();
    }

    }


  • เมื่อต้องการ จะ test ก็เพียงแต่กดปุ่ม "test" ที่อยู่ข้างซ้ายของจอ
    ลองดูตัวอย่างผลลัพท์ที่ได้



    สีแดงแสดง error, ส่วนสีเขียวแสดงว่าค่าถูกต้องตรงตามที่คาดหวังไว้


ตัวอย่างที่แสดง เป็นตัวอย่างง่ายๆ
ถ้าอยากเห็นตัวอย่างการใช้งานจริง
(ซึ่งต้องการอะไรที่ซับซ้อนกว่าตัวอย่างนี้)
ลองเข้าไปที่นี่
SuiteAcceptanceTests อันนี้เป็น test ที่ fitnesses test ตัวมันเอง

Update
fitnesse สามารถใช้กับ java, .net, ruby, Python
, Delphi, Smalltalk, Perl

Related link from Roti

Friday, December 16, 2005

การใช้งาน Rails Plugin command

วันนี้เรียนรู้วิธีการใช้งาน Rails Plugin ขึ้นมาอีกหนึ่งอย่าง
คำสั่งแรก ก็คือ

script/plugin discover

คำสั่งนี้ rails จะ scan หา plugins repository site
และ add plugin source ที่พบไว้ใน
file ~/.rails-plugin-sources ของเรา

ตอนแรกที่เห็นคำสั่งนี้ ก็นึกว่าเขาคงมี web service
ที่ให้บริการเรื่อง query เพื่อที่จะสอบถามได้ว่า
มี repositories อะไรที่ลงทะเบียนไว้บ้าง
แต่เปล่าเลย หลังจากเปิดดู source code
ก็พบว่า เขาใช้วิธีง่ายกว่านั้น
กล่าวคือ
ปกติ Rails จะมี page Plugins
ที่ลงรายการ plugin ที่มีคนทำแล้ว พร้อมคำอธิบายสั้นๆ
สิ่งที่คำสั่ง discover ทำ ก็คือ
load page ที่ว่ามา
แล้วก็ใช้ pattern matching หา link ที่มีคำว่า "plugin"

|- class Discover
||+ def initialize(base_command)
||
||+ def options
||
||+ def parse!(args)
||
2- def scrape(uri)
23 require 'open-uri'
23 puts "Scraping #{uri}" if $verbose
23 dupes = []
3- content = open(uri).each do |line|
4- if line =~ /<a[^>]*href=['"]([^'"]*)['"]/
45 uri = $1
5- if uri =~ /\/plugins\// and
uri !~ /\/browser\//
56 uri = extract_repository_uri(uri)
56 yield uri unless dupes.include?(uri) or
Repositories.instance.exist?(uri)
56 dupes << uri
56 end
45 end
34 end
23 end
||
||+ def extract_repository_uri(uri)
|| end


หลังจากได้ listing ของ plugin source มาแล้ว
คราวนี้เราก็สามารถใช้คำสั่ง

script/plugin install PLUGIN_NAME

เพื่อ install plugin ที่เราต้องการ

Note:
ผมชอบใช้วิธี svn export มากกว่าแฮะ

Related link from Roti

คนเดินถนน

เมื่อก่อนบ้านผมอยู่ในซอยที่เงียบสงบ
ต่อมามีโรงหนังดังมาตั้งอยู่ปากซอย
ซอยก็เลยกลายเป็นที่ระบายรถ

ถนนซึ่งไม่มีฟุตบาตให้เดิน ก็เลยกลายเป็นที่วัดใจกัน ระหว่างรถกับคน
ผมพบว่าการเดินหันหลังให้รถ
(เดินทางซ้ายของถนน)
ดูปลอดภัยกว่า
เพราะเดินทางขวาทีไร หมิ่นเหม่ต่อการ
ถูกชนเหลือเกิน

คนขับรถส่วนใหญ่ จะไม่สนใจคนเดินเท้า
ไม่ใช่ว่าเขาเป็นคนไม่ดีแต่อย่างไร
เพียงแต่ "ไม่ใส่ใจ" เท่านั้น
เมื่อไม่ใส่ใจ การขับรถตะบึงผ่านคน
จึงเป็นเรื่องปกติ
การพุ่งรถเข้ามาไกล้ ก่อนที่จะหัดหลบ
ออก ก็เป็นเรื่องปกติ

เท่าที่สังเกตดู คนที่ขับรถแบบนี้
ไม่ได้เป็นเฉพาะเจาะจงที่กลุ่มใด
คือเป็นมันหมด
ไม่ว่า วัยรุ่น ชาย หญิง แก่ หัวหงอก
คนขับรถให้เจ้านาย
คนหล่อ คนไม่หล่อ
สวย หรือ ไม่สวย

แต่ที่เหมือนกันหมด ก็คือแววตา
หรือสีหน้า ที่ไม่รับรู้การมีตัวตนของคนเดินถนน

Related link from Roti

Thursday, December 15, 2005

ผจญภัยกับ Emacs

Note:
post นี้
  • อาจจะช่วยสร้างกำลังใจให้กันคนที่หวาดหวั่นกับ Emacs
  • สนับสนุนสิ่งที่คุณ poonlap post ไว้ใน Bad Knowhow and Good Wrapper


ช่วงนี้ผมกำลังสนใจ Constraint Programming
ก็เลยไป load Mozart Programming System มาลองใช้ดู
Environment การพัฒนาของตัวนี้จะอยู่บน Emacs เป็นหลัก
ผมก็ไม่ถนัด Emacs เท่าไร
พึ่งหัดใช้มาได้ไม่เกิน 2 เดือน
จำคำสั่งอะไรก็ไม่ได้
การใช้ Emacs ก็เลยใช้ mouse เป็นหลัก (XEmacs)
(key-binding มันเยอะแยะไปหมด ใครจะจำได้วะ)

ตอนแรกที่ลองใช้ Mozart ก็ทดลองบน Ubuntu ก็พบว่าใช้ได้ดี
แต่พอทดลองลงบน Mac OS X บ้าง
กลับพบว่ามี error ขึ้นมาดังนี้

Buffer is read-only: #<buffer *Oz Compiler*>

ลอง search หาใน google ดู ก็เห็นมีคนถามเหมือนกัน
แต่ไม่มีคนตอบ

ก็เลยเปลี่ยนไปอ่านคู่มือ Emacs แทน
หาอะไรที่เกี่ยวกับ read-only
ก็พบว่ามีบท 27.7 Read-only Buffers
ก็เลยเข้าไปอ่าน
อ่านไปอ่านมา ก็เห็นว่ามีคำสั่ง toggle-read-only ให้ใช้
ก็เลยไปทดสอบสั่งใน buffer ที่ error ดู
M-x toggle-read-only
"เฮ้ย มหัศจรรย์ มันใช้ได้" !!
Error หายไปแล้ว
เหลือแต่ข้อสงสัยว่า ทำไมมันเปิด buffer มาแบบ read-only วะ

ด้วยความมันส์ในอารมณ์
ก็เข้าไปเปิด script file ของ mozart ที่ใช้ทำงานใน emacs
สำหรับคนที่ไม่เคยใช้ Emacs, script file พวกนี้
จะเขียนด้วยภาษา eLisp
โชคดีที่ผมหัดเรียน scheme มาได้พักใหญ่แล้ว
(ใช้ scheme เป็น ก็จะอ่าน code lisp ออก
เพราะ concept มันคล้ายๆกัน)

หลังจากตาลายอยู่พักใหญ่ ก็เริ่มอ่าน code รู้เรื่องขึ้น
จะว่าไปแล้ว code lisp ที่ดูเหมือนอ่านยาก
ถ้าจับหลักได้ มันก็อ่านง่ายดีเหมือนกันนะ
ในที่สุด ก็เจอ code ส่วนที่เป็นตัว open buffer

(let ((buffer (generate-new-buffer "*Oz Compiler*")))
(setq oz-compiler-buffers (cons buffer oz-compiler-buffers))
(save-excursion
(set-buffer buffer)
(compilation-mode)
(set (make-local-variable 'compilation-parse-errors-function)
'oz-compilation-parse-errors))

หลักการมั่วของผม ก็คือดูแต่โครงๆ
อย่างอันนี้ ก็ให้รู้ว่ามันกำลังสร้าง buffer ใหม่ด้วยคำสั่ง generate-new-buffer
และก็มีการ switch ไปที่ buffer นั้นด้วยคำสั่ง set-buffer
แล้วก็มีเรียกใช้คำสั่ง compiler-mode (ซึ่งก็ไม่รู้เหมือนกันว่ามันคืออะไร)
ผมก็เลยทดลองแทรกคำสั่ง ลงไปแบบนี้

(let ((buffer (generate-new-buffer "*Oz Compiler*")))
(setq oz-compiler-buffers (cons buffer oz-compiler-buffers))
(save-excursion
(set-buffer buffer)
(compilation-mode)
(toggle-read-only)
(set (make-local-variable 'compilation-parse-errors-function)
'oz-compilation-parse-errors))

ทดลอง restart emacs ใหม่
Error ยังอยู่เหมือนเดิม
เกือบจะถอดใจแล้ว
เผอิญเหลือบไปเห็นว่า file ที่ผมแก้อยู่ มันมีนามสกุลอยู่ 2 แบบ (ชื่อเดียวกัน)
อันหนึ่งเป็น el (อันที่ผมแก้อยู่) อีกอันเป็น elc
ก็เลยลองค้น google ดู จึงรู้ว่า elc ก็คือ script ที่ compile ไว้แล้ว

คราวนี้ก็ต้อง search หาอีกว่า จะ compile el script นี้ต้องสั่งอย่างไร
ในที่สุดก็เจอในคุ่มือ elisp บท 16.2 The Compilation Functions

หลังจาก compile เสร็จ
error ก็อัตรธานหายไป
ฮา ฮา, ของไชโย 3 ที

(ส่วนสาเหตุจริงๆ ที่ทำให้เกิด read-only นั้น
ไม่อยากหาต่อแล้ว แค่นี้ก็หมดไปเกือบ 2 ชั่วโมงแล้ว)

สรุป
อย่างที่ post ของคุณ poonlap บอกแหล่ะครับ ยิ่งเล่นเยอะ bad knowhow ก็เยอะขึ้น
การเล่นครั้งนี้ จริงๆก็ไม่ได้มีประโยชน์อะไรที่เป็นชิ้นเป็นอัน
แต่มันจะช่วยสะสม degree ของ bad knowhow ของผมขึ้นมาอีก

Related link from Roti

Wednesday, December 14, 2005

ทดลอง Rails Plugin

Rails version ปัจจุบัน (>=0.14) มีการเพิ่ม architecture
ที่เรียกว่า plugin เข้ามาใหม่
เพื่อให้ใช้เป็นมาตรฐานสำหรับการ customize rails
หรือการเพิ่ม extension ใหม่ๆลงไป

เดิมที่ยังไม่มีมาตรฐานนี้
developer ต่างคนก็ต่างใช้วิธีการของตัวเองในการ add Mixin ลงไป
เช่นบางคนก็ติดตั้ง library ใน gems
แล้วค่อยไปแก้ environment.rb ให้ require เข้ามา
หรือไม่ก็เขียน code ไว้ใน $RAILS_PROJECT/lib directory แล้ว require
ใน code ส่วนที่ต้องการใช้

ส่วนของใหม่ กำหนดให้เก็บ code นี้ไว้ใน
direcotry $RAILS_PROJECT/vendor/plugins
โดยเราต้องสร้าง directory $PLUGIN_NAME/lib ขึ้นมา
และภายในนั้น Rails จะเรียก require file ที่ชื่อ $PLUGIN_NAME.rb
ให้โดยอัตโนมัติ (อันนี้เดาเอานะ เพราะหา source code หรือเอกสารยืนยัน
ยังไม่เจอ แต่ได้ทดลองเปลี่ยนชื่อ หรือสร้าง file ชื่ออื่นๆไว้ มันก็ไม่ได้เรียกใช้แต่อย่างไร)

ส่วนการ install plugin ที่คนอื่นๆเขียนไว้
นิยมใช้ svn export ออกมาจาก repository
โดยเราต้อง cd ไปที่ $RAILS_PROJECT/vendor ก่อน

สำหรับ plugin ที่ผมทดลองใช้แล้วก็คือ Asset Timestamping
โดย install โดยใช้คำสั่ง

$ svn export http://svn.aviditybytes.com/rails/plugins/asset_timestamping

plugin นี้จะช่วยแปะ timestamp ลงไปใน path ของพวก javascript หรือ css
เช่น
เดิมใน view ที่สร้างโดย scaffold มันจะ require css โดยใช้คำสั่งนี้

<%= stylesheet_link_tag 'scaffold' %>

ซึ่งผลลัพท์ html ที่ได้จะได้ link หน้าตาแบบนี้

<link href="/stylesheets/scaffold.css" media="screen" rel="Stylesheet" type="text/css" />

เมื่อ install plugin แล้ว มันจะเปลี่ยนเป็นแบบนี้

<link href="/stylesheets/scaffold.css?1134445305" media="screen" rel="Stylesheet" type="text/css" />


Note: ที่ต้องทำเช่นนี้ ก็เพราะว่า browser มักจะทำ
cache ในส่วนของ javascript กับ css ไว้
เวลาที่เรามีการเปลี่ยนแปลง file พวกนี้
browser มันไม่ยอมรับรู้ตามไปด้วย
เดือดร้อนต้องเปลี่ยนชื่อ หรือเพิ่ม file ใหม่
หรือไม่ก็โทรไปบอกว่า ช่วยลบ cache ทิ้งด้วยครับ

ลองมาดู source code ของ Asset Timestamping กัน
จะได้รู้ว่า Mixin เขียนอย่างไร

12 module ActionView #:nodoc:
13 module Helpers #:nodoc:
14 module AssetTagHelper
15 private
16 def timestamped_compute_public_path(source, dir, ext)
17 public_path = compute_public_path_without_timestamp(source,dir
,ext)
18 if not source.include?('?')
19 file_paths = ["#{RAILS_ROOT}/public/#{dir}/#{source}",
20 "#{RAILS_ROOT}/public/#{dir}/#{source}.#{ext}"
]
21 for path in file_paths
22 if File.exists?(path)
23 return public_path << '?' + File.stat(path).mtime.to_i.t
o_s
24 end
25 end
26 end
27 public_path
28 end
29 alias :compute_public_path_without_timestamp :compute_public_pat
h
30 alias :compute_public_path :timestamped_compute_public_path
31 end
32 end
33 end

หลักการก็คือ มันทำการ mixin เข้าไปที่ module ActionView::Helpers::AssetTagHelper
ทำการตัดต่อเปลี่ยนชื่อ method ก่อน
โดยของเดิมให้เปลี่ยนไปใช้ชื่อใหม่ว่า compute_public_path_without_timestamp (บรรทัดที่ 29)
และให้เอาชื่อ method ที่พึ่งเพิ่มเข้าไปใหม่ ให้ไปแทนที่ของเดิม (บรรทัดที่ 30)

method timestamped_compute_public_path
เนื้อหาก็ไม่มีอะไรมาก
เริ่มแรก ก็ให้ gen url ขึ้นมาโดย method เดิม (บรรทัดที่ 17)
จากนั้นก็หา file และแปะ mtime ของ file นั้นตามหลังไป (บรรทัดที่ 23)

ดูแล้ว ก็รู้สึกสนุกกับ ruby
ที่มี class แบบเปิด
ใครใคร่แก้ แก้ (mixin)
ไม่เหมือน java ที่
ใครไคร่แก้ ก็ต้อง extends เอา (อันนี้สำหรับกรณีที่เขาออกแบบมาเผื่อ)
แต่ถ้าไม่ได้ออกแบบมาเผื่อ
ใครใครแก้ ก็ต้องcglib เอา

Related link from Roti

Tuesday, December 13, 2005

เรื่องทางสาธารณะสุขบ้าง

PracticallyKM
อันนี้อ่านเจอใน GotoKnow
เป็นเรื่องที่คุณหมอ phichet เล่าถึงประสบการณ์การดูงาน
ที่ออสเตรเลีย น่าสนใจทีเดียว

อ่านไปอ่านมาก็เจอที่คุณหมอพูดถึง
นพ.สุภัทร ฮาสุวรรณกิจ
จำชื่อได้ว่า เป็นเพื่อนร่วมกิจกรรม (สมัยเรียน)
ก็เลยตามไปดูใน google
ได้บทความที่เขาเขียนมาจำนวนหนึ่ง
เลยเอามาลง link ไว้



อ่านแล้วก็รู้สึกว่าเราชักจะหมกมุ่นกับ โลกของ technical มากเกินไปแล้ว

Related link from Roti

Monday, December 12, 2005

Javascript in ruby style

นึกแล้วว่า Prototype มันต้องมีอะไรดีๆซ่อนอยู่
ด้วยความที่ว่ามันไม่มีเอกสารให้ดู
เราก็เลยไม่รู้ว่ามันทำอะไรได้บ้าง

วันนี้อ่านเจอ article เรื่อง Prototype Meets Ruby: A Look at Enumerable, Array and Hash
อ่านแล้วต้องร้อง "ว้าว" ดังๆ

Related link from Roti

ทดลองใช้ OpenLaszlo

ผมเคยทดลอง evaluate OpenLaszlo ตั้งแต่สมัยที่มันยังไม่มี คำว่า open นำหน้า
จากการทดลองเล่น ยอมรับเลยว่าติดใจใน interface ที่ดู หรูหรา มาก
แต่ตอนนั้นมีปัญหาว่า compile-time มันสูงมาก
อีกอย่าง swf file ที่ได้ก็มีขนาดใหญ่เกินไป (1 MB นิดๆ)

วันก่อนเจอที่ mk เขียนถึง OpenLaszlo ก็เกิดอาการสนใจอยากลองเล่นอีกครั้ง

เริ่มแรกทดสอบขนาดก่อนเลย
เอาที่ helloworld ก่อนเลย เพราะมันเป็น app ที่เล็กที่สุดที่เป็นไปได้แล้ว

<canvas>
<text>Hello Laszlo!</text>
</canvas>

ขนาดที่ได้ ก็คือ 102,599 Byte
ลองมาดูว่า ตัวเลข 100 กว่า KB นี้มันแถมอะไรมาให้บ้าง
(openlaszlo มี feature ที่ช่วย describe ว่า
app ของเราหลังจาก compile แล้วจะมีอะไรแปะอยู่ข้างในบ้าง)
ใน hello app ของเรา จะแบ่งออกเป็น 2 ส่วนคือ
LFC (laszlo foundation class) กับ Instances
โดย LFC ของ flash version 7 มีขนาดก่อน zip 209 KB
ส่วน instance ของเรามีขนาด 168 Byte
หลังจาก zip แล้วจะได้ขนาด 69 KB
ซึ่งไม่เท่ากับ swf file ที่เราได้กลับมา
แสดงว่ามี overhead ในการแปลงเป็น swf อีก 30 KB
(ตรงนี้ไม่รู้เหมือนกันว่า ขนาด 30 KB นี้
เป็น fix size หรือ variable size
เมื่อโปรแกรมใหญ่ขึ้น)

สรุปขนาดของ App
เล็กสุด ก็น่าจะประมาณ 100 KB

ที่นี่ด้านเวลาบ้าง
เวลาใน version นี้คงไม่เป็นปัญหาแล้ว
เพราะเราสามารถ pre-compile เป็น format lzo ได้
(จริงๆแล้ว binary ข้างในคือ swf format)

ที่นี้ลองมาดู factor สุดท้ายบ้าง
ก็คือเรื่องของ runtime environment
(พัฒนาเสร็จแล้ว จะเอาไปใช้แล้ว)
กรณี static web app
อันนี้หมายถึง file laszlo ของเราไม่ได้ dynamic เปลี่ยนแปลงไปมา
(หมายถึงโปรแกรมนะ ไม่ใช่ data)
การเอาไปใช้ วิธีที่ง่ายที่สุด ก็คือ copy file lzo (ที่ได้จากการ pre-compile)
ไปไว้ใน web application ที่เราต้องการ
โดยแค่เปลี่ยนนามสกุลเป็น swf ก็ใช้ได้แล้ว

ส่วนกรณีที่ lzx (โปรแกรม) ของเราเปลี่ยนไปมาได้ (dynamic)
อันนี้ไม่ขอแนะนำ เพราะ compile time
สูงมาก (>15 sec)

Related link from Roti

Wednesday, December 07, 2005

Switch to Postgresql

อ่านเจอเรื่อง FeedLounge Now running on PostgreSQL
เขาพูดถึงประเด็นว่าทำไมเขาถึง switch จาก MySQL -> PostgreSQL
แรกเริ่มเดิมทีเขาใช้ MySQL 's MyISAM table แต่ต่อมาก็เปลี่ยนไปใช้
mysql 's InnoDB table แทน โดยเขาพบปัญหาจาก
table locking behavior in MyISAM
เนื่องจากลักษณะงานของเขามี write ครึ่งหนึ่งของ read
(read 4 ล้าน quries/day, write 2 ล้าน queries/day)
ผลก็คือทำให้ scalable ของเขาต่ำมาก (< 10 users)

ผลลัพท์จากการใช้ InnoDB เขาบอกว่า
พบปัญหา slow performance
จากการ load Data (ไม่ใช่เรื่อง query)
ก็เลยตัดสินใจเปลี่ยนอีกที
คร่าวนี้ย้ายไป PostgresSQL แทน

เหตุผลที่เป็นแรงจูงใจในการเปลี่ยนคราวนี้ ก็มี
  • Database Size
    เมื่อตอนเปลี่ยนจาก MyISAM -> InnoDB database
    ที่เคยมีขนาด 1 GB ก็กลายเป็น 10+GB
    ปัจจุบัน ณ ขณะที่เปลี่ยน ขนาดได้กลายเป็น 34 GB
    หลังจากย้ายลง Postgres แล้ว ขนาดเหลือเพียง 9 GB
  • Load time
    การ load data ลง database
    เดิม MySQL ใช้เวลา 1 วัน ในการ load
    เมื่อเปลี่ยนมาใช้ PostgreSQL ก็เหลือแค่ 4 ชั่วโมง


นอกจากนี้ยังพบว่า Postgres ใช้ memory
แค่ 1/3 ของ mysql

ที่น่าสนใจก็คือ comment ที่มีคนมา post
เช่น
  • InnoDB ใน MySQL 5 มี feature "compact" row format
    ที่ช่วยลดขนาด table ได้
  • Postgres ไม่สามารถ set ให้ใช้ share buffer
    ได้เต็มที่เหมือน MySQL แต่ Postgers สามารถใช้ประโยชน์จาก
    OS cache ได้, และมีค่า effective_cache_size ที่ใช้ set
    เพื่อให้ optimizer รับรู้
  • Feedlounge ใช้ GUID เป็น primarykey
    คนของ MySQL ก็เลยบอก
    GUIDs tend not to do well for index storage efficiency, since InnoDB stores the uncompressed primary key in all secondary index records.

    ผลก็คือเขาต้องใช้ IO เยอะขึ้นไปอีก (จากการ load index)
  • UUID is really poor choice for primary key for Innodb.
    The data is going to be clustered by it… and it is random which means you will insert in radom spots in giant BTREE (as it holds rows). Furthermore inserts in the middle will frequently result in page spits which causes IO and fragmentation.

Related link from Roti

Monday, December 05, 2005

Samorost



samorost เป็นชื่อ game ที่เขียนโดย
Jakub Dvorský
art มากๆ

Related link from Roti

Constraint Programming

เขียนเรื่อง ruby, rails เยอะแล้ว กลับมาที่ java บ้าง

วันก่อนผมอ่านเจอเรื่อง Solving Sudokus in Java
คนที่ยังไม่รู้จัก sudokus ให้ลองดูคำอธิบายได้ที่ wikipedia on sudokus
(อธิบายละเอียดยิบเลยครับ)
ที่สนใจก็คือ ผมยังไม่เคยได้ยินคำว่า Constraint Programming มาก่อน
ใน tutorial ข้างบนเขาใช้ library ที่ชื่อ Koalog Constraint Solver
ซึ่งเป็น commercial product
ผมก็เลยลอง search หา Library ที่เป็น opensource license ดู
เจอเจ้า Cream: Class Library for Constraint Programming in Java
ผลผลิตจาก Japan (ดีที่ web site เขาเป็นภาษาอังกฤษ)

ลองดูวิธีการใช้ Cream solve Sudokus กัน
(เอกสารของ cream มีไม่เยอะ แต่โชคดีที่เขามีตัวอย่างให้ดู
รวมทั้งมีตัวอย่าง sudokus ด้วย ก็เลยแกะมาเล่าให้ฟังได้)
โดยเราจะใช้โจทย์เดียวกับ tutorial ที่อ้างถึงข้างบน
เริ่มด้วยการ define problem array ก่อน
static int prob[][] = {
{6,0,0,0,5,8,4,9,0},
{0,2,4,0,0,0,0,0,0},
{0,0,0,0,2,0,0,3,6},
{0,0,0,0,0,0,9,0,7},
{7,0,0,3,0,0,8,0,0},
{1,5,0,0,8,9,0,0,0},
{0,3,1,0,0,0,0,0,0},
{0,0,0,0,3,0,5,0,0},
{0,0,8,0,9,5,0,0,0}
};


ก่อนที่จะ solve ก็ต้องมีการสร้างตัวแปรให้ cream รับรู้ก่อน
ในกรณีของเรา ก็คือ เราจะมีตัวแปรทั้งหมด 9 * 9 = 81 ตัว
โดยตัวแปรทั้งหมดจะต้องถูกสร้างภายใต้ context network
Network net = new Network();

IntVariable v[][] = new IntVariable[9][9];
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
if (prob[i][j] == 0) {
v[i][j] = new IntVariable(net, 1, 9);
} else {
v[i][j] = new IntVariable(net, prob[i][j]);
}
}
}

จะเห็นว่าถ้าค่าใน prob array เป็น 0 เราจะ new variable
ด้วย constructor new IntVariable(net, 1, 9)
ความหมายก็คือ ค่าใน variable นี้ เป็นไปได้ตั้งแต่ 1 ถึง 9
ส่วนกรณีที่มีค่าอยู่แล้ว ก็จะใช้ constructor new IntVariable(net, x)
โดย x ก็คือค่าของ variable นั้นๆเลย

ขั้นถัดไปก็คือการ declare constraint ของปัญหา
เริ่มด้วย
  • แต่ละ row ห้ามมีเลขซ้ำกัน
    // แต่ละ row ห้ามมีเลขซ้ำกัน
    IntVariable tmps[] = new IntVariable[9];
    for (int i = 0; i < 9; i++) {
    for (int j = 0; j < 9; j++) {
    tmps[j] = v[i][j];
    }
    new NotEquals(net, tmps);
    }


  • แต่ละแถวห้ามมีเลขซ้ำกัน
    // แต่ละ column ห้ามซ้ำกัน
    for (int j = 0; j < 9; j++) {
    for (int i = 0; i < 9; i++) {
    tmps[i] = v[i][j];
    }
    new NotEquals(net, tmps);
    }


  • ในแต่ละ box 3 * 3 ห้ามมีตัวเลขซ้ำกัน
    // ในแต่ละ box 3x3 ห้ามมีตัวเลขซ้ำกัน
    for (int bi = 0; bi < 3; bi++) {
    for (int bj =0; bj < 3; bj++) {
    int cnt = 0;
    for (int i = 0; i < 3; i++) {
    for (int j = 0; j < 3; j++) {
    tmps[cnt++] = v[i+(bi*3)][j+(bj*3)];
    }
    }
    new NotEquals(net, tmps);
    }
    }


จะเห็นว่า constraint ข้างบนใช้ constructor NotEquals(net, array_of_variable)
สร้างขึ้นมา
ความหมายก็คือ ในตัวแปรที่ผ่านเข้าไปทั้งหมดนั้น ห้ามมีตัวใดตัวหนึ่งซ้ำกันเลย

พอเสร็จจากขั้นการ declare constraint ก็เป็นขั้นการ solve แล้ว
Solver solver = new DefaultSolver(net);
Solution sol = solver.findFirst();

for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
System.out.print(sol.getIntValue(v[i][j]));
if (j < 8) {
System.out.print(",");
}
}
System.out.println();
}

โดยในการ call เราสามารถเลือก call โดยกำหนด timeout ได้ด้วย
จากการจับเวลาเครื่องผม
กรณีใช้ findFirst จะใช้เวลาประมาณ 76-80 ms
ส่วนกรณี findBest จะใช้เวลาประมาณ 90-100 ms

คนที่สนใจ Constraint Programming ลองเข้าไปดูที่นี่ครับ

Related link from Roti

Rails Migrations

เคยเขียนเรื่อง Migration ของ RoR ไปทีหนึ่ง
ในเรื่อง ทดลอง acts_as_tree feature ของ ActiveRecord (Ruby on Rails)
วันนี้เห็น link tutorial ที่เข้าใจง่ายดี
ก็เลยเอามาลงให้ดูกัน
The Joy of Migrations

ประโยคคำสั่งที่เราสามารถใช้ใน Migration class ก็มี
  • create_table
    syntax ของ create_table ก็คือ
    create_table :test do |t|
    t.column :col0, :string
    t.column :col1, :text
    t.column :col2, :integer
    t.column :col3, :float
    t.column :col4, :datetime
    t.column :col5, :timestamp
    t.column :col6, :time
    t.column :col7, :date
    t.column :col8, :binary
    t.column :col9, :boolean
    t.column :col10, :int2
    end

    ตั้งแต่ col0 -> col9 เป็น datatype ที่ ActiveRecord define ไว้แล้ว
    ซึ่งมันจะทำการแปลงเป็น datatype จริงๆให้อีกที
    โดยขึ้นอยู่กับ Database ที่เราเลือกใช้
    ส่วนกรณี col10 เป็น datatype ที่ ActiveRecord ไม่ได้ define ไว้
    เวลา generate sql ActiveRecord ก็จะ generate ออกไปตามที่เราพิมพ์เลย

    sql ที่ได้จากข้างบน จะหน้าตาดังนี้
     CREATE TABLE test ("id" serial primary key,
    "col0" character varying(255),
    "col1" text,
    "col2" integer,
    "col3" float,
    "col4" timestamp,
    "col5" timestamp,
    "col6" time,
    "col7" date,
    "col8" bytea,
    "col9" boolean,
    "col10" bigint)


    กรณีที่เป็น varchar เราสามารถกำหนด length ได้แบบนี้
      t.column :name, :string, :limit=>25


    กรณีที่ต้องการกำหนด null, default ก็เขียนดังนี้
      t.column :col11, :string, :limit=>25, :default=>"blank", :null=>false

    ซึ่งจะได้ออกมาดังนี้
     "col11" character varying(25) DEFAULT 'blank' NOT NULL)


    รายละเอียดปลีกย่อยของ create_table ยังมีอีกจำนวนหนึ่ง
    ให้ลองดูใน RDoc ของ activeRecord ดู
  • drop_table
    อันนี้ชัดเจน ใช้ drop table
  • add_column
    format ของ add_column เหมือนตอนที่เราเขียน t.column
  • remove_column
    อันนี้ชัดเจน ไม่ต้องอธิบาย
  • rename_table
  • rename_column
  • change_column
    เปลี่ยน definition ของ column
  • change_column_default
  • add_index
    ตัวอย่าง
    add_index(:accounts, [:branch_id, :party_id], :unique => true)

  • remove_index

Related link from Roti

Sunday, December 04, 2005

MySQL Compatiblility Functions

อ่านเจอจาก Robby on Rails
ใน pgFoundry มี project MySQL Compatiblility
A reimplemenation of as many MySQL functions as possible in PostgreSQL, as an aid to porting.

ใน release ล่าสุดมี function แบ่งออกเป็น 8 กลุ่มคือ
  • aggregate
  • bit
  • controlflow
  • datetime
  • information
  • mathematical
  • operators
  • string


สำหรับคนที่ไม่ต้องยุ่งกับการ port mysql -> postgres
ก็สามารถ load มาศึกษาว่า
user define function บน postgres เขียนอย่างไร?

Related link from Roti

Wednesday, November 30, 2005

คำถามดีๆ

คราวก่อนที่ post เรื่อง Higher Order Procedure ไป
bact ได้ post ถามว่า
method(op) กับ : ใน oper[:+]

นี่คือทำให้เราพิมพ์ + กับ - ได้ใช่มั๊ยครับ ?
คือ + กับ - เป็น object ด้วย

เป็นคำถามที่ดีจริงๆ
ผมชักชอบ blog มากขึ้นแล้วหล่ะ
คำถามใน blog ทำให้ความรู้เราเพิ่มขึ้น
(เพราะมันช่วยชี้ให้เห็นว่า เราลืมมองอะไรไปบ้างหรือเปล่า)
อย่างคราวก่อนที่ mk ถามเรื่อง "?" ที่อยู่หลัง ruby method
นั่นก็เป็นคำถามที่ดีเหมือนกัน
เพราะสมัยนั้นที่ผมมอง ruby code ผมไม่เคยตั้งคำถามเรื่องนั้นเลย
คำถามนั้นทำให้เราเห็นจุดอ่อนของเรา

กลับมาที่คำถามของ bact
ตอนแรกผมก็มองเหมือน bact (อาศัยความเคยชินจาก scheme)
+ มันต้องเป็น object ประเภท procedure แน่เลย
แต่กลายเป็นไม่ใช่
ลองดู code ต่อไปนี้
อันนี้ จาก scheme48

pphetra@[~]: scheme48
Welcome to Scheme 48 1.1 (made by pphetra on Sun Nov 13 15:03:12 ICT 2005).
Copyright (c) 1993-2004 by Richard Kelsey and Jonathan Rees.
Please report bugs to scheme-48-bugs@martigny.ai.mit.edu.
Type ,? (comma question-mark) for help.
> +
#{Procedure 52 +}
>

อันนี้ชัด + ใน scheme คือ procedure

ต่อไปจาก ruby บ้าง

pphetra@[~]: irb
irb(main):001:0> :+.class
=> Symbol
irb(main):002:0> 1.method(:+).class
=> Method
irb(main):003:0> 1.method(:+).call(2)
=> 3
irb(main):004:0> 1.method("+").class
=> Method
irb(main):005:0> 1.method("+").call(2)
=> 3
irb(main):006:0>

อา :+ จริงๆแล้วคือ symbol
ใน ruby, Object ทุกตัวจะมี method ที่ชื่อ method
คำสั่ง 1.method(:+) return ค่า
ออกมาเป็น method + ของ object 1
ซึ่งเราสามารถใช้ method call เพื่อสั่ง run จรีิงๆ
ปกติ method method จะรับ symbol เป็น parameter
แต่ก็สามารถส่งค่าเป็น String ให้ก็ได้เหมือนกัน

คำถามถัดไป ก็คือ แล้ว symbol คืออะไร
อืมม์ ยังไม่ค่อยแจ่มเหมือนกัน
แต่ดูตัวอย่างนี้ก่อน

pphetra@[~]: irb
irb(main):001:0> f1 = :pok
=> :pok
irb(main):002:0> f2 = :pok
=> :pok
irb(main):003:0> f1.object_id
=> 3918094
irb(main):004:0> f2.object_id
=> 3918094

เห็นในคำอธิบายเขาบอกว่า symbol จะมี instance เดียว
เสมอ ไม่ว่าจะอ้างถึงกี่ครั้งก็ตาม
หรือ ไม่ว่าจะอ้างจากคนละ context ก็ตาม

pphetra@[~]: irb
irb(main):001:0> module A
irb(main):002:1> $f1 = :pok
irb(main):003:1> end
=> :pok
irb(main):004:0> module B
irb(main):005:1> module C
irb(main):006:2> $f2 = :pok
irb(main):007:2> end
irb(main):008:1> end
=> :pok
irb(main):009:0> $f1.object_id
=> 3920142
irb(main):010:0> $f2.object_id
=> 3920142

Related link from Roti

Tuesday, November 29, 2005

Run JettyLaucher in Ðebug Mode

ช่วงหลังๆนี่ผมเปลี่ยนมาใช้ JettyLaucher แทน Tomcat Plugin
เพราะดูมัน lightweight ดี
แต่มีที่ติดใจอยู่ก็คือ กรณีที่เราแก้ java file
เราจะต้องสั่ง restart web app ใหม่ทุกครั้ง

วันนี้อ่านเจอ Tip ง่ายๆ
ที่ทำให้เราไม่ต้อง restart web app ทุกครั้งที่แก้ java file
เส้นผมบังภูเขาแท้ๆ
ให้ start jetty ใน debug mode แทน
เพราะ eclipse ใน debug mode
มันจะทำ hot replace class file ให้เราโดยอัตโนมัติ

Related link from Roti

Monday, November 28, 2005

Python Lambda & Ruby Block

เห็น mk เขียนเล่าเรื่องเรียนรู้ Ruby แล้ว
มีประเด็นที่สะกิดใจอยู่นิดหนึ่งตรง
เรื่อง iterator ที่ mk ยังคาใจอยู่

ที่สะกิดใจ ก็คือ เกิดความสงสัยว่า python
ก็น่าจะมี concept คล้ายๆแบบนี้เหมือนกัน
ก็เลยลองเปิดหนังสือดู
ก็พบในบทว่าด้วยเรื่อง lambda & filter
ลองดูตัวอย่างนี้

เริ่มที่ ruby ก่อน
ary = [1, 2, 3, 4] 
result = ary.select { |x| x % 2 == 0 }
result # => [2, 4]


ถ้าเขียนแบบเดียวกันใน python
ary = [1, 2, 3, 4]
result = filter(lambda x: x%2 == 0, ary)
print result # => [2, 4]


โจทย์เดียวกัน
ลองเขียน ruby block อีกแบบดู
ary = [1, 2, 3, 4]
prc = lambda {|x| x%2 == 0}
result = ary.select(&prc)
result # => [2, 4]

Note: สังเกตุเครื่องหมาย & หน้า ตัวแปร prc
เป็นการบอกว่า pass ค่า block นะ ไม่ใช่ argument

น่าจะพอสรุปได้ว่า ruby block กับ
python lambda เป็นเรื่อง concept เดียวกัน
เพียงแต่ ruby ปรับปรุง syntax ให้สามารถ
pass block (lambda) ได้ง่ายกว่า

Note: แฟนผมเดินเข้ามาแอบอ่าน post นี้
ดูสักพัก แล้วก็เดินส่ายหัวจากไปพร้อมกับเสียงงืมงัม python ruby ruby python ...
(ช่วงนี้ผมเขียน post บ่อย แกกลัวว่าจะแอบ chat
กับสาวๆ)

Related link from Roti

ETL

ETL => extract, transform and load
CloverEtl

Related link from Roti

Higher Order Procedures

วันนี้เจอ ruby code ที่เขียนแบบนี้
oper = proc do |op|
proc do |x, y|
x.method(op).call(y)
end
end

add = oper[:+]
add[1,2] # => 3

sub = oper[:-]
sub[2,1] # => 1


นี่ถ้าเป็นเมื่อก่อน ตอนที่ยังไม่เรียน scheme
ก็คง งงงวย พักใหญ่
(การเขียน คง กับ งงงวย ติดกัน อาจทำให้ผู้อ่านเกิด
อาการเมา blog ได้)
แต่หลังจากเรียนรู้ scheme แล้วก็เลยรู้ว่า
มันคือ higher order procedure นี่เอง


• A procedure is said to be “first-order” if
none of its arguments is itself a procedure.
• A procedure is said to be “higher-order” if
one or more of its arguments is a procedure.


ถ้าเขียนใน scheme ก็จะเป็นแบบนี้
(define oper
(lambda (op)
(lambda (x y)
(op x y))))

(define add
(oper +))

(define sub
(oper -))

(add 1 2) ;; => 3
(sub 2 1) ;; => 1

Related link from Roti

Sunday, November 27, 2005

Enhance xmp (example printer)

อ่านเจอใน http://eigenclass.org/
Mauricio Fernandez. เขาเขียน utilities เล็กๆ
ที่ชื่อ Enhance xmp
เพื่อใช้ evaluate ตัวอย่าง code และแสดงค่า ณ ตำแหน่งต่างๆในโปรแกรม

ยกตัวอย่าง
สมมติเรามี snippet code ดังนี้
person1 = "Tim"
person2 = person1.dup
person1[0] = "J"
person1
person2


ถ้าเราต้องการแสดงให้เห็นว่า variable person1 และ person2
มีค่าเป็นอะไร เราก็มักจะเขียน comment ลงไปแบบนี้
person1 # => "Jim"
person2 # => "Tim"


ปกติเวลาเราเขียน comment แบบนี้
ก็มักจะใช้วิธี manual, ก็คือเปิด editor ออกมาใส่ค่าลงไปเอง

utilities ที่ Mauricio เขียน จะช่วย fill-in ค่าพวกนี้ให้เราโดยอัติโนมัติ
โดยเราเพียงแต่เขียนแบบนี้
person1 = "Tim"
person2 = person1.dup
person1[0] = "J"
person1 # =>
person2 # =>

เมื่อ run ผ่านโปรแกรม xmp
xmp ก็จะใส่ค่าลงไปให้เราเอง

ที่นี้ลองมาใล่ดูว่า source code เขาทำงานอย่างไร
technique ที่เขาใช้ก็คือ
เขาจะอ่าน source code ขึ้นมาก่อน
จากนั้นก็แทรก code ไปตัดต่อโปรแกรมบรรทัดที่ต้องการ
โดยสร้างตัวแปรขึ้นมารับผลลัพท์ที่ได้จาก evaluate บรรทัดนั้น
จากนั้นก็ให้พิมพ์ผลลัพท์ออกมาทาง stderr io
code = ARGF.read
idx = 0
newcode = code.gsub(/^(.*) # =>.*/) do |l|
expr = $1
(/^\s*#/ =~ l) ? l :
%!((#{VAR} = (#{expr}); $stderr.puts("#{MARKER}[#{idx+=1}] => " + #{VAR}.inspe
ct) || #{VAR}))!
end

จะเห็นว่าเขาอ่านโปรแกรมขึ้นมา แล้วใช้ gsub
ในการหาบรรทัดที่มี pattern ที่มี # =>
ถ้าเจอบรรทัดนั้น ก็จะทำการแทรก code เพิ่มเข้าไป
ลักษณะของ code ที่ได้ออกมาในตัวแปร newcode จะมีหน้าตาแบบนี้
person1 = "Tim"
person2 = person1.dup
person1[0] = "J"
((_xmp_1133097178_956604 = (person1); $stderr.puts("!XMP1133097178_657069![1] =>
" + _xmp_1133097178_956604.inspect) || _xmp_1133097178_956604))
((_xmp_1133097178_956604 = (person2); $stderr.puts("!XMP1133097178_657069![2] =>
" + _xmp_1133097178_956604.inspect) || _xmp_1133097178_956604))

จะเห็นได้ว่ามีการสร้างตัวแปรมารับ
แล้วก็พิมพ์ตัวแปรนั้นออกมาในรูปของ xxx[idx]=>value

พอแก้ code ไดดังนี้ เขาก็ทำการเรียกใช้งาน code ดังนี้
stdin, stdout, stderr = Open3::popen3("ruby", "-w")
stdin.puts newcode
stdin.close
output = stderr.readlines

เขาใช้ standard library open3 เข้ามาช่วย
(พวกที่ใช้ win32 จะไม่มี library ตัวนี้ให้)
โดยการเรียกโปรแกรม ruby ในอีก subprocess หนึ่ง
โดย subprocess จะรับข้อมูลผ่านทาง stdin
และให้ output กลับมาผ่านทาง stdout, stderr
ในกรณี xmp นี้ ก็จะใช้ข้อมูลจาก stderr เป็นหลัก

ตัวอย่าง stderr ที่ได้จาก program ข้างบน
!XMP1133102283_175558![1] => "Jim"
!XMP1133102283_175558![2] => "Tim"


ขั้นต่อไปก็เป็นการแปลความผลลัพท์ที่ได้จาก stderr
XMPRE = Regexp.new("^" + Regexp.escape(MARKER) + '\[([0-9]+)\] => (.*)')
results = Hash.new{|h,k| h[k] = []}
output.grep(XMPRE).each do |line|
result_id, result = XMPRE.match(line).captures
results[result_id.to_i] << result
end

จะเห็นว่ามีการ new Hash โดยใช้ block
(ที่เคยเขียนไปใน post เรื่อง Cache with Ruby Hash)
ถ้ามีการ access hash เมื่อไร ก็ให้สร้าง empty array เตรียมไว้ให้เลย
จะเห็นว่าใน code ก็จะ grep หาบรรทัดที่มีผลลัพท์
แล้วก็ทำการ put ผลลัพท์ลงไปใน hash (<< คือการ add ลง array)
ที่ต้องใช้ array มาเก็บ ก็เพราะบรรทัดที่อยู่ใน loop
มันจะถูก evaluate ได้หลายครั้ง
เช่น
i = 0
4.times {
i+=1 # => 1, 2, 3, 4
}

จะเห็นว่าบรรทัดใน loop มันจะมีค่าได้หลายค่า

ขั้นถัดไปก็ง่ายแล้ว ก็แค่ อ่าน code ขึ้นมาใหม่
จากนั้นก็ทำการ append ผลลัพท์ที่ได้ลงไป
ส่วนที่เหลือ ก็เป็นกรณี warning กับกรณี error แล้ว
ก็ใช้ technique แบบเดียวกัน เพียงแต่ง่ายกว่า
ตรง pattern ของ warning มันจะมีเลขบรรทัด
บอกมาเสร็จ ก็แค่แทรกลงไปให้ถูกบรรทัด
ส่วนกรณี error ถ้ามีก็ให้ต่อท้าย output ได้เลย

การได้ไล่โปรแกรมอย่างนี้ เป็นการเรียนรู้ที่ดีมาก
ผมว่าการจะเขียนโปรแกรมได้ดีนั้น ส่วนหนึ่งก็คือ
ต้องอ่าน source code คนอื่นเยอะๆ

Related link from Roti

Trip พายเรือ #2

ต่อจาก Trip พายเรือ #1



13/12/2540
วันนี้เจอฝายเพิ่มอีก 3 ฝาย แต่คราวนี้ยุ่งยากขึ้น
เพราะไม่สามารถทำคนเดียวได้
ต้องมีการไหว้วาน และมีค่าตอบแทนให้
(สมัยนั้นเรือแคนนูแบบเบาๆยังไม่เข้ามาในไทย)

2 ฝายแรกความสูงยังไม่มากนัก
ยังใช้วิธีค่อยๆคลายเชือกปล่อยเรือลงไปทีละนิด
และให้มีคนคอยรับเรือด้านล่าง
(ตรงด้านล่างจะมีแท่งคอนกรีตขวางน้ำอยู่
ถ้าปล่อยให้ลอยลงไปตรงๆ ก็จะชนและรั่วได้)
จบจาก 2 ฝายแรก เรือก็ได้รูเล็กๆ เพิ่มมา 1 รู
ส่วนฝายที่ 3 เป็นเขื่อนคอนกรีตขนาดใหญ่
ต้องใช้วิธียกเรือขึ้นเดินแบกไปปล่อยท้ายเขื่อนเอา

ทิวทัศน์วันนี้ บ้านคนเริ่มบางตาแล้ว
แต่ก็ยังมีถนนเลียบไปกับแม่น้ำตลอดทางอยู่

ที่พักของคืนนี้ ทำเพิงนอนที่หาดทรายริมหมู่บ้าน
เช่นเคย ต้องมีการขออนุญาติผู้ใหญ่บ้านก่อน
บ้านนี้ยุ่งขึ้นหน่อย ต้องแสดงบัตรประชาชนให้ดูด้วย

Related link from Roti

Thursday, November 24, 2005

OSCache อีกที

คราวก่อนพูดถึงเรื่อง การ implement cache บน Web Application
ที่ใช้ OSCache เข้ามาช่วย
วันนี้อ่านเจอใน Raible Design เรื่อง Clustering OSCache
เลยรู้ว่าตัวเองเข้าใจผิดในส่วน feature Cluster ไป

ลองอ่านนี่ดู
Caches across a cluster only broadcast messages when flush events
occur. This means that the content of the caches are built up
independently on each server, but whenever content becomes stale on
one server it is made stale on them all. This provides a very high
performing solution since we never have to pass cached objects around
the cluster. And since there is no central server that is in charge of
the cluster, the clustering is very robust


เดิมผมก็เข้าใจว่ามันทำ replicate cache
ระหว่าง node
แต่กลายเป็นว่า มันส่ง event broadcast
เฉพาะกรณีที่เราสั่ง flush (expire) content ใน cache

Related link from Roti

Wednesday, November 23, 2005

DrScheme

ช่วงนี้ผมกำลังเรียนรู้ Lisp อยู่
แต่เผอิญไปเจอ online course
Structure and Interpretation of Computer Programs
ของ MIT เข้า

course นี้เข้าใช้ scheme (ซึ่งเป็น dialect ของ Lisp)
ก็เลยเปลี่ยนมาเรียนรู้ scheme ก่อน

ตอนแรกที่เรียน ก็ใช้ scheme48 เป็น interpreter
run ใน text mode
ซึ่งก็ใช้ได้ดีแหล่ะ
แต่วันนี้ทดลอง load PLT Scheme มาลองเล่นดู
ได้เรื่องเลย มันออกแบบมาสำหรับการเรียนรู้ scheme
ทำให้มี feature ที่น่าสนใจหลายอย่าง

ลองดูในภาพนี้



หน้าต่างครึ่งบนเป็นส่วน file ที่เรา edit
ส่วนครึ่งล่างเป็น REPL ที่ทำให้เราทดลอง evaluate expression ได้

จะเห็นว่าเราสามารถใส่ test case
เข้าไปในส่วน editor ของเราได้
นอกจากนี้ จะเห็นว่าในส่วน function double
จะมีบรรทัดที่เป็นสีแดงอยู่
ส่วนที่เป็นสีแดง ก็คือ บริเวณที่ PLT chekc ว่า
code นี้ยังไม่ได้ถูกเรียกใช้งานเลย (code coverage feature)

นอกจากนี้ยังมี features อื่นๆอีกเพียบเลย
ดูได้จาก A Brief Tour of DrScheme version 299.200

Related link from Roti

Israeli Wall

เมื่อเดือนก่อนได้ดูสารคดี Border ทางช่อง National GeoGraphy
เลยได้รู้ว่า อิสราเอล กำลังสร้างกำแพงแบ่งแยกที่อยู่ระหว่าง palestine
กับ Israel

ดูภาพได้ที่นี่ Apartheid Wall Gallery
Adam เขาบรรยายด้วยว่า ระหว่างการสร้างกำแพง
Palestinian buildings are destroyed but Palestinian olive groves are treated with more respect by the Israelis, who recognise the value of these important resources. Olive trees serve communities for generation after generation. They live for four or five hundred years. So, rather than grub them up with bulldozers, the Israelis dig them up carefully and then move them to Israeli land!

Related link from Roti

การ implement cache ใน Web Application #1

Post นี้จะว่าด้วย solution สำหรับ java application ก่อน
ครั้งต่อไปถึงจะว่าด้วย caching บน RoR (ruby on rails)
Tool ที่ผมเลือกใช้สำหรับ solution นี้ ก็คือ OSCache

OSCache มาพร้อมกับวิธีการใช้ 3 แบบใหญ่ๆคือ
  • ใช้ API ตรงๆ
    อันนี้เหมาะกับ implement caching ในฝั่ง business logic
  • ใช้ JSP Taglib
    อันนี้เหมาะสำหรับ fine-grain cache
    เพราะเราสามารถกำหนดบริเวณที่เราต้องการ cache ได้ตามใจชอบ
    (เลือกเฉพาะบางส่วนใน jsp ได้)
  • ใช้ CacheFilter
    อันนี้เหมาะสำหรับ cache ระดับ Page
    (เพราะว่าสะดวกในการ config ดี)


ขั้นแรกในการใช้ OSCache ก็คือ
การกำหนด Configuration ที่เราต้องการ
  • เลือกกำหนดขนาดของ cache ที่เราต้องการก่อน
    ค่า default จะอยู่ที่ 1000 objects
    (สามารถกำหนดเป็น unlimit ก็ได้)
  • ประเด็นที่ตามมาก็คือ algorithm ในการควบคุมขนาดของ cache
    OSCache มีให้เลือก 2 แบบ
    • LRU (Least-Recently-Used)
    • FIFO

    ประเด็นที่น่าสนใจอันหนึ่งของ LRU
    ก็คือ OSCache implement LRU โดยใช้
    java.util.LinkedHashMap
    ซึ่งถ้าเราตามไปดู javadoc ของ LinkedHashMap จะเห็นคำอธิบายดังนี้
    A special constructor is provided to create a linked hash map whose order of iteration is the order in which its entries were last accessed, from least-recently accessed to most-recently (access-order). This kind of map is well-suited to building LRU caches.

    และถ้าตามไปดู source code ของ OSCache ในส่วน
    LRU ก็จะพบว่า วิธีการที่มัน implement LRU จะทำเป็น 3 step
    ก็คือ
    • ทดลองดูว่าใน classpath มี LinkedHashMap หรือไม่
      ถ้ามีก็ใช้ตัวนี้
    • ถ้าไม่มี ก็จะมองหา org.apache.commons.collections.SequencedHashMap
      ของ jakarta-commons collection แทน
    • ถ้าไม่พบ ก็จะลงเอยด้วยการใช้ LinkedList
      ซึ่งอันนี้อาจจะมีผลเสียต่อ performance แทนในกรณี load สูงๆ

  • เลือกว่าจะทำ Persistent ของ cache ด้วยหรือไม่
    โดย default แรกสุด จะเก็บ cache ใน Memory เท่านั้น
    แต่เราสามารถเลือก persistent ลง Disk ได้ด้วย
    โดย option ที่น่าสนใจก็คือ
    cache.persistence.overflow.only
    ถ้าเรา set ค่านี้เป็น True เวลาที่ Memory Cache เราเต็ม
    entry ที่ถูก remove ออก จะย้ายมาลง Disk แทน
  • กำหนด cache.blocking
    default คือ false นั่นคือ กรณีที่ entry ของเราหมดอายุ
    แล้วมี thread 2 thread เข้ามาพร้อมๆกัน thread แรก
    จะเป็นผู้ได้รับเลือกให้เป็นคนที่ update ค่านั้น ส่วน thread ที่ 2
    จะได้ค่า ณ ขณะนั้น (stale entry) กลับไปแทน
    (ซึ่งก็ถือว่าใช้ได้นะ เพราะถือว่า entry มีอายุเกินกว่าที่เรากำหนดไปไม่กี่ millisec)
    แต่ถ้ากำหนดเป็น true ก็จะมีผลว่า thread ที่ 2
    จะถูก block เพื่อรอให้ thread 1 ทำการ update ให้เสร็จก่อน
    Note: เท่าที่ลองใล่ debug ดู พบว่า
    กรณีที่เป็น new Entry ใหม่ๆเลย ไม่ว่าเราจะเลือก block เป็น true หรือ
    เป็น false ก็ตาม
    OSCache จะเลือกใช้วิธี blocking สำหรับพวก new Entry เสมอ



ตัวอย่าง source code ให้ลองดูที่ document ของ OScache โดยตรง
เพราะเขาเขียนไว้ดีแล้ว

feature ที่น่าสนใจอื่นๆก็คือ
  • การกำหนด cron expression
    ที่ทำให้เรากำหนด schedule ของการ expire ได้
    เช่นกำหนดให้ cache expire ทุกเช้าวันจันทร์
  • การกำหนด group ของ cache
    อันนี้เหมาะสำหรับกำหนดกลุ่ม cache ที่มี content depend ถึงกัน
    เวลาเรามีการ update content จะได้สั่ง flush cache ทีเดียวทั้ง group ได้เลย
  • ใน jsp taglib หรือ cacheFilter สามารถกำหนด
    scope ของ cache ได้
    เช่นถ้าเป็น session, cache ก็จะถูกแยกเป็นของใครของมันเลย
    ไม่ share กัน
    แต่ถ้าเป็น application ก็จะมี cache แค่ instance เดียว
    ให้ทุก session share ใช้กัน


เพิ่มเติม
ประเด็นที่ต้อง design ดีๆ เวลาใช้ cache
ก็คือ กรณีที่ table ใน database เรามีการเปลี่ยนแปลง
แล้วเราต้องการ flush cache (delete cache) ที่เกี่ยวข้องทิ้งทั้งหมด
เราจะทำได้อย่างไร
ทางออกก็อาจจะเช่น design ให้ key ที่มี content เกี่ยวข้องกัน มี prefix เหมือนๆกัน
(เวลาสั่ง flush สามารถสั่ง flush พวกที่มี prefix ตามที่กำหนดได้)
หรือเลือกใช้ group feature เข้ามาช่วย

Related link from Roti

Sunday, November 20, 2005

JS/UIX

JS/UIX คือ Unix ที่เขียนโดยใช้ javascript อย่างเดียว
สามารถ run บน browser ได้เลย

ลองเล่น vi บนนั้นแล้ว เนียนดีมาก
แต่เจอปัญหา cursor มันอยู่ตำแหน่งแปลกๆบ้าง เป็นบางครั้ง

ใน shell มีคำสั่งที่เป็น non-standard command
อยู่หลายตัวเช่น
  • browse เป็นคำสั่งให้เปิด browser window ใหม่ และ navigate ไป url ที่ระบุ
  • fexport export data ที่อยู่ใน home directory ของเรา
  • invaders เล่นเกมส์ space invaders

ถ้าสงสัยว่ามี command อะไรให้ใช้บ้าง
ให้ใช้คำสั่ง help ดูครับ

Related link from Roti