🔁 ทวนก่อนเริ่ม (คำจากบทก่อน ๆ ที่บทนี้ใช้)
- method — คำสั่งที่ตั้งชื่อไว้เรียกใช้ซ้ำ ประกาศด้วย
def … end(บท 93) - hash — กล่องเก็บของเป็นคู่ "ชื่อ: ค่า" เช่น
{ title: "สวัสดี" }(บท 92) - symbol — ชื่อที่ขึ้นต้นด้วย
:เช่น:titleใช้เป็นป้ายชื่อ (บท 92) - block — ก้อนโค้ด
do … endที่แนบไปกับการเรียก method ·|x|คือช่องรับของ (บท 93) @ตัวแปร— ตัวแปรที่ติดอยู่กับ object (บท 95) · ในบทนี้@postแปลว่า "บทความหนึ่งชิ้น"
แอปตัวอย่างของสไลด์ คือบล็อกชื่อ wongnok_blog · ของหลักคือบทความ (ภาษาอังกฤษ post)
— Post ตัวใหญ่ = แม่พิมพ์ของบทความ · posts ตัวเล็ก = ตารางที่เก็บบทความหลายชิ้น
🧰 เวอร์ชันที่ใช้ตรวจ: Ruby 4.0.9 · Rails 8.1.4 (สไลด์ใช้ Ruby 4.0.6 · Rails 8.1.3 — เรื่องในบทนี้ไม่ต่างกัน) · ผลรันทุกก้อนมาจากการรันจริง
สามบรรทัดที่ดูเหมือนภาษาอังกฤษ
นี่คือโค้ด Rails จริงที่คุณจะเจอในคอร์สนี้ · ยังไม่ต้องเข้าใจ แค่ดูหน้าตาไว้ก่อน:
validates :title, presence: true
redirect_to @post, notice: "saved"
form_with model: @post do |f|
f.text_field :title
end
แต่ละบรรทัดอยู่คนละที่ในแอป · เทียบกับร้านอาหาร (ภาพที่ใช้ทั้งคอร์ส) ได้แบบนี้:
| บรรทัด | อยู่ส่วนไหนของ MVC (บท 90) | ในร้านอาหาร | แปลเป็นภาษาคน |
|---|---|---|---|
validates … | model (Day 2) | คนดูแลห้องเก็บวัตถุดิบ | "แฟ้มบทความทุกแผ่นต้องมีชื่อเรื่อง ไม่มีห้ามเก็บเข้าตู้" |
redirect_to … | controller (บท 102) | หัวหน้าเชฟ | "บันทึกเสร็จแล้ว พาลูกค้าไปหน้าบทความนั้น พร้อมแปะโน้ตว่า saved" |
form_with … | view (บท 103) | การจัดจานก่อนเสิร์ฟ | "วาดแบบฟอร์มให้กรอกบทความ มีช่องชื่อเรื่องหนึ่งช่อง" |
ถ้าคุณมาจาก Go จะรู้สึกแปลก เพราะใน Go ทุกการเรียกฟังก์ชันมีวงเล็บเสมอ
เช่น c.Redirect(http.StatusSeeOther, "/posts/1") ใน Gin
ส่วน Ruby ยอมให้ละของหลายอย่าง จนโค้ดอ่านเหมือนประโยค ·
บทนี้จะถอดทีละชิ้น ทั้งหมด 5 ชิ้น
ชิ้นที่ 1 — วงเล็บละได้
ใน Ruby การเรียก method จะใส่วงเล็บหรือไม่ใส่ก็ได้ ผลเหมือนกันทุกอย่าง
ลองดูด้วย method เล็ก ๆ ที่เราเขียนเอง:
# paren.rb
def greet(name)
puts "สวัสดี #{name}"
end
greet("ต้น") # แบบมีวงเล็บ
greet "ต้น" # แบบละวงเล็บ
$ ruby paren.rb
สวัสดี ต้น
สวัสดี ต้น
def greet(name) … end— ประกาศ method ชื่อgreetที่รับค่าหนึ่งตัว (บท 93)"สวัสดี #{name}"—#{}คือการเสียบค่าตัวแปรลงใน string (บท 91)greet("ต้น")กับgreet "ต้น"— คือการเรียก method ตัวเดียวกัน ต่างกันแค่วงเล็บ
เพราะฉะนั้น validates :title ก็คือ validates(:title) — การเรียก method ชื่อ validates
ไม่ใช่คำสั่งพิเศษของภาษา
Go ทำแบบนี้ไม่ได้เลย: เขียน fmt.Println "hi" แล้ว go build ไม่ผ่านแน่นอน
ลูกค้าประจำเดินเข้าร้านแล้วพูดว่า "กะเพรา ไข่ดาว" แม่ครัวเข้าใจทันที
ไม่ต้องพูดเต็มว่า "ขอสั่งเมนูกะเพรา โดยมีตัวเลือกเพิ่มไข่ดาว"
ใบสั่งที่ไปถึงครัวเป็นใบเดียวกัน — พูดย่อหรือพูดเต็ม ครัวทำเหมือนกัน
ที่ไม่เหมือนกัน: คนฟังในร้านเดาจากบริบทได้ แต่ Ruby เดาไม่ได้
— มันมีกฎตายตัว · บางกรณี (เช่นเรียก method ซ้อนกันหลายชั้นในบรรทัดเดียว) ถ้าไม่ใส่วงเล็บ Ruby จะจับคู่ผิดจากที่คุณตั้งใจ
กฎจำง่าย: อ่านแล้วงงเมื่อไหร่ ใส่วงเล็บเองได้เสมอ ไม่ผิด
- method call · optional parentheses
- การเรียก method · ใน Ruby วงเล็บรอบ argument เป็นของไม่บังคับ · ถาม AI ได้ว่า "rewrite this Ruby line with explicit parentheses" แล้วจะเห็นทันทีว่าอะไรคือ method อะไรคือค่าที่ส่งเข้าไป
- DSL (Domain-Specific Language)
- โค้ดที่ออกแบบให้อ่านเหมือนภาษาเฉพาะเรื่อง · ใน Rails มันคือการเรียก method ธรรมดาที่ละวงเล็บไว้ · บอก senior ได้ว่า "บรรทัดนี้เป็น DSL ของ Rails ใช่ไหมครับ ข้างในคือเรียก method อะไร"
ชิ้นที่ 2 — hash ตัวสุดท้ายละปีกกาได้
ถ้าของชิ้นสุดท้ายที่ส่งเข้า method เป็น hash Ruby ยอมให้ไม่ต้องเขียนปีกกา { }
เวลาเห็น ชื่อ: ค่า ต่อท้ายบรรทัด ให้อ่านว่า "นี่คือ hash ก้อนเดียว ที่ถูกถอดปีกกาออก"
วิธีพิสูจน์ที่ง่ายที่สุด: เขียน validates ปลอมขึ้นมาเอง ให้มันรับของแล้ว p ออกมาดูเฉย ๆ
จะได้เห็นว่า Ruby ส่งอะไรเข้าไปจริง (p คือคำสั่งพิมพ์ค่าแบบเห็นชนิดของมัน · บท 93)
# hash1.rb — validates ปลอม: รับของแล้ว p ออกมาดูเฉย ๆ
def validates(name, options)
p name
p options
end
validates :title, presence: true, length: { minimum: 5 } # แบบที่เขียนใน Rails
validates(:title, { presence: true, length: { minimum: 5 } }) # แบบเขียนเต็ม
สองบรรทัดล่างหน้าตาต่างกันมาก (บรรทัดแรกไม่มีวงเล็บ ไม่มีปีกกาครอบ) — คิดว่า Ruby จะพิมพ์ออกมาเหมือนกันหรือต่างกัน?
$ ruby hash1.rb
:title
{presence: true, length: {minimum: 5}}
:title
{presence: true, length: {minimum: 5}}
เหมือนกันทุกตัวอักษร · ทั้งสองแบบ method ได้ของสองชิ้น:
ชิ้นแรกคือ symbol :title · ชิ้นที่สองคือ hash หนึ่งก้อน
อ่านโค้ดทีละส่วน:
def validates(name, options)— method ปลอมของเรา รับของสองชิ้น ตั้งชื่อว่าnameกับoptionspresence: true— คู่ "ชื่อ: ค่า" หนึ่งคู่ใน hash (ชื่อpresenceค่าtrue)length: { minimum: 5 }— อีกคู่หนึ่ง ที่ค่าของมันเป็น hash อีกก้อน (hash ซ้อน hash) · ปีกกาด้านในห้ามละ เพราะมันไม่ใช่ตัวสุดท้าย- บรรทัดแบบเขียนเต็ม — ใส่วงเล็บ (ชิ้นที่ 1) และปีกกาครอบ hash ตัวสุดท้ายให้ครบ
redirect_to ในสามบรรทัดแรกก็เป็นแบบเดียวกัน · ลองด้วย method ปลอมอีกตัว
(@post ตรงนี้เป็นแค่ string จำลอง · ใน Rails มันคือบทความจริงที่ controller หยิบมา):
# hash2.rb — redirect_to ปลอม
def redirect_to(target, options)
p target
p options
end
@post = "บทความที่ 1 (จำลอง)" # ใน Rails ตัวนี้คือบทความจริงที่ controller หยิบมา
redirect_to @post, notice: "saved"
redirect_to(@post, { notice: "saved" })
$ ruby hash2.rb
"บทความที่ 1 (จำลอง)"
{notice: "saved"}
"บทความที่ 1 (จำลอง)"
{notice: "saved"}
ใน Go ไม่มีท่านี้ · ถ้าอยากส่ง "ก้อนตัวเลือก" เข้าฟังก์ชัน ต้องประกาศ struct แล้วเขียนปีกกาเองทุกครั้ง (โค้ด Go ข้างล่างเป็นการเลียนแบบรูปร่างเพื่อเทียบเท่านั้น ไม่ใช่ library จริง):
Go — ต้องมีวงเล็บและปีกกาเสมอ
Validates("title", Rules{Presence: true})
Ruby — ละได้ทั้งคู่
validates :title, presence: true
ใบสั่งหนึ่งใบมีช่อง เมนู (ของหลัก) กับช่อง หมายเหตุ (ตัวเลือกพ่วงท้าย)
ลูกค้าพูดว่า "กะเพรา, เผ็ด: มาก, ไข่: ดาว" พนักงานจดทุกตัวเลือกลงช่องหมายเหตุช่องเดียวให้เอง
ลูกค้าไม่ต้องพูดว่า "เปิดวงเล็บหมายเหตุ … ปิดวงเล็บ"
validates :title, presence: true ก็เหมือนกัน — :title คือเมนู · presence: true คือหมายเหตุ
· Ruby รวบหมายเหตุทั้งหมดเป็น hash ก้อนเดียวให้
ที่ไม่เหมือนกัน: พนักงานร้านรับหมายเหตุได้ทุกแบบ จะเขียนใส่กระดาษแนบมาก็ยังรับ
แต่ใน Ruby มี method บางตัวที่ "รับหมายเหตุแบบพูดเท่านั้น" (keyword argument · บท 93)
ถ้าคุณใส่ปีกกาเอง มันจะปฏิเสธทันที — ดูชิ้นที่ 3
⚠️ เขียนเต็มไว้ "อ่านให้เข้าใจ" เท่านั้น อย่าเติมปีกกาเองในโค้ด Rails จริง
— validates กับ redirect_to รับได้ แต่ form_with ใส่ปีกกาแล้วพังด้วย ArgumentError
· เขียนแบบไม่มีปีกกา (แบบที่คู่มือ Rails เขียน) ใช้ได้กับทุกตัว
- options hash · trailing hash
- hash ที่ส่งเป็นของชิ้นสุดท้ายเพื่อบอก "ตัวเลือก" · Ruby ให้ละปีกกาได้ · ถาม AI ได้ว่า "in this Rails call, which part is the options hash?"
- keyword argument
- ช่องรับค่าที่จับคู่ด้วยชื่อ เช่น
def form_with(model:)(บท 93) · ตั้งแต่ Ruby 3 ถือว่าเป็นคนละอย่างกับ hash ที่มีปีกกา · บอก senior ได้ว่า "ผมส่ง hash ที่มีปีกกาเข้า method ที่รับ keyword เลยโดน ArgumentError ครับ"
redirect_to @post, notice: "saved", status: :see_other ถ้าเขียนเต็ม (ใส่วงเล็บและปีกกาครบ) จะได้แบบไหน?
(status: :see_other คือตัวเลือกบอกรหัสตอบกลับ · ความหมายไม่สำคัญ ข้อนี้ดูแค่รูปร่าง)
ตัวเลือกที่ 2 ถูก · ของชิ้นแรกคือ @post · ที่เหลือทั้งหมดเป็นคู่ "ชื่อ: ค่า" จึงรวมเป็น hash ก้อนเดียวเป็นชิ้นที่สอง
ตัวเลือกที่ 1 กับ 4 ผิด เพราะเปลี่ยน notice กับ status ให้กลายเป็นการเรียก method
ซึ่งไม่ใช่สิ่งที่บรรทัดเดิมทำ — ในบรรทัดเดิมมันเป็นแค่ชื่อช่องใน hash ·
ตัวเลือกที่ 3 ผิด เพราะยัด @post เข้าไปใน hash ทั้งที่มันไม่มีชื่อช่อง — รันจริง Ruby ไม่ยอมรันเลย (SyntaxError) และบอกว่า
expected a `=>` between the hash key and value
จุดที่ต้องจำ: เห็น ชื่อ: ค่า ต่อท้ายบรรทัดเมื่อไหร่ ให้อ่านว่า "hash ก้อนเดียวที่ถอดปีกกาออก"
ชิ้นที่ 3 — block ที่ได้รับของ |f| และ yield
บรรทัด form_with model: @post do |f| ทำให้คนงงมากที่สุด เพราะ f โผล่มาจากไหนไม่รู้
คำตอบคือ: method form_with เป็นคนสร้างของชิ้นหนึ่ง แล้วยื่นให้ block · ช่อง |f| คือมือที่รับของชิ้นนั้น
คำสั่งที่ใช้ "ยื่นของให้ block" ชื่อว่า yield (แปลตรงตัวว่า "ส่งมอบ")
หัวหน้าห้องจัดจาน (form_with) เตรียมกล่องเครื่องมือไว้หนึ่งกล่อง
แล้วยื่นให้ลูกมือพร้อมบอกว่า "เชิญจัดตามใจเลย" (yield)
ลูกมือ (block) รับกล่องมา เรียกมันว่า f แล้วหยิบเครื่องมือในกล่องมาใช้ (f.text_field :title)
ลูกมือทำเสร็จ ส่งงานคืน · หัวหน้าเก็บงานแล้วยกจานออกไปเสิร์ฟ (ได้ HTML ของฟอร์ม)
ที่ไม่เหมือนกัน: ลูกมือจริงเห็นว่ากล่องมีเครื่องมืออะไรบ้าง
แต่ในโค้ด คุณมองไม่เห็นว่า f มี method อะไร — ต้องไปเปิดคู่มือของ form_with ดู
และถ้าไม่มีลูกมือมารับเลย (เรียก method โดยไม่แนบ block) โปรแกรมไม่ได้รอ แต่พังทันที
มาเขียน form_with จิ๋วที่รันได้จริงกัน (จำลองหลักการ ไม่ใช่โค้ดของ Rails จริง ·
บทความจำลองด้วย hash เพราะยังไม่มี Rails):
# form.rb — form_with จิ๋ว (จำลองหลักการ ไม่ใช่โค้ดของ Rails จริง)
class FormBuilder
def initialize(model)
@model = model
@fields = []
end
def text_field(name)
value = @model[name]
@fields << " <input name=\"post[#{name}]\" value=\"#{value}\">"
end
def to_html
"<form>\n" + @fields.join("\n") + "\n</form>"
end
end
def form_with(model:)
builder = FormBuilder.new(model) # ① เตรียมกล่องเครื่องมือ
yield(builder) # ② ยื่นกล่องให้ block → กลายเป็น f
builder.to_html # ③ เก็บงาน แล้วส่ง HTML คืน
end
post = { title: "สวัสดี Ruby", body: "บทความแรก" } # บทความจำลองด้วย hash
html = form_with model: post do |f|
f.text_field :title
f.text_field :body
end
puts html
$ ruby form.rb
<form>
<input name="post[title]" value="สวัสดี Ruby">
<input name="post[body]" value="บทความแรก">
</form>
อ่านทีละส่วน — ส่วนกล่องเครื่องมือ (FormBuilder):
class FormBuilder— แม่พิมพ์ของ "กล่องเครื่องมือทำฟอร์ม" (class · บท 95)initialize(model)— ทำงานตอนสร้างกล่อง · จำบทความไว้ใน@modelและเตรียม array ว่าง@fieldsไว้เก็บช่องกรอก@model[name]— หยิบค่าจาก hash ด้วยชื่อ เช่น@model[:title]ได้"สวัสดี Ruby"@fields << "…"—<<แปลว่า "ใส่เพิ่มต่อท้าย array" ·\"คือเครื่องหมายคำพูดที่อยู่ข้างใน stringto_html—join("\n")ต่อทุกช่องเข้าด้วยกันโดยคั่นด้วยการขึ้นบรรทัดใหม่ (\n)
ส่วน form_with — หัวใจของเรื่อง:
def form_with(model:)— รับค่าแบบ keyword ชื่อmodel(บท 93) · ตอนเรียกต้องเขียนmodel: …- ① สร้างกล่องเครื่องมือหนึ่งกล่อง เก็บไว้ในตัวแปร
builder - ②
yield(builder)— กระโดดไปทำ block ที่แนบมา และยื่นbuilderให้ · ใน block ของชิ้นนี้จึงมีชื่อว่าf - ③ block ทำเสร็จ กลับมาบรรทัดนี้ · บรรทัดสุดท้ายของ method คือค่าที่ส่งคืนอัตโนมัติ (บท 93)
form_with model: post do |f| … end— ละวงเล็บ (ชิ้นที่ 1) · ส่ง keywordmodel:· แนบ block ที่มีช่องรับของชื่อf
ชื่อ f ไม่ใช่คำพิเศษ · ผมลองเปลี่ยนเป็น |form| แล้วเขียน form.text_field :title ก็ได้ผลเหมือนเดิมทุกตัวอักษร
· คนเขียน Rails แค่นิยมใช้ f เพราะสั้น
เทียบกับ Go ที่คุณเคยเขียน — ใน Gin คุณเขียนแบบนี้ (บท 38):
Go + Gin
r.GET("/posts", func(c *gin.Context) {
c.String(200, "hi")
})
Gin สร้าง c แล้วยื่นให้ฟังก์ชันของคุณ · คุณแค่เรียก c.String
Ruby
form_with model: post do |f|
f.text_field :title
end
form_with สร้าง f แล้วยื่นให้ block ของคุณ · คุณแค่เรียก f.text_field
ท่าเดียวกันเป๊ะ — c ใน func(c *gin.Context) คือ f ใน do |f| ·
Ruby แค่เขียนสั้นกว่า (ไม่ต้องเขียน func ไม่ต้องบอกชนิด)
ทีนี้ลองกลับไปเรื่องคำเตือนในชิ้นที่ 2 — ถ้าเราใส่ปีกกาเองตอนเรียก form_with จิ๋ว:
# form_braces.rb — เหมือน form.rb ทุกบรรทัด ยกเว้นบรรทัดนี้
html = form_with({ model: post }) do |f|
ได้ฟอร์มเหมือนเดิม หรือพัง?
$ ruby form_braces.rb
form_braces.rb:18:in 'Object#form_with': wrong number of arguments (given 1, expected 0; required keyword: model) (ArgumentError)
caller: form_braces.rb:26
| html = form_with({ model: post }) do |f|
^^^^^^^^^
callee: form_braces.rb:18
| def form_with(model:)
^^^^^^^^^
from form_braces.rb:26:in '<main>'
พัง · อ่าน error เป็นภาษาคน: "ส่งของมา 1 ชิ้นแบบไม่มีชื่อ (given 1) แต่ method ไม่รับของแบบนั้นเลย (expected 0)
และยังขาด keyword ที่ชื่อ model"
ปีกกาทำให้ { model: post } กลายเป็น "hash หนึ่งก้อน" แทนที่จะเป็น "keyword ชื่อ model"
· form_with ตัวจริงของ Rails 8.1.4 ก็พังแบบเดียวกัน (ผมลองแล้วได้ wrong number of arguments (given 1, expected 0))
ท่า "method ยื่นของให้ block" นี้ Rails ใช้ทั่วไปหมด ไม่ใช่แค่ฟอร์ม · ยังไม่ต้องจำ แค่จำหน้าตาไว้:
| จะเจอ | ใครยื่นของ | ของที่ยื่นมา | เรียนจริงที่ |
|---|---|---|---|
<%= yield %> ใน layout (ไฟล์ HTML ที่แทรก Ruby ได้ · บท 103) | layout (จานรองของทุกจาน) | ไม่ยื่นของ แค่ "วางเนื้อหาของหน้านั้นตรงนี้" | บท 103 |
create_table :posts do |t| | migration (ใบสั่งช่างต่อเติมตู้) | t = เครื่องมือเพิ่มคอลัมน์ | Day 2 |
respond_to do |format| | controller | format = ตัวเลือกว่าจะตอบเป็นแบบไหน | Day 3 |
- yield
- คำสั่งใน method ที่ "กระโดดไปทำ block ที่แนบมา" และส่งของให้ได้ · ถ้าไม่มี block แนบมาจะพังด้วย
LocalJumpError· ถาม AI ได้ว่า "where is the yield in this method, and what does it pass to the block?" - block parameter (yielded object)
- ของที่ไปโผล่ใน
|f|·|t|·|format|· เจอตัวที่ไม่รู้จัก ถาม AI ได้ว่า "what object does this Rails helper yield to the block, and what methods does it have?" - form builder
- ชื่อเรียกของ
fในform_with· ใน Rails จริงคือ class ชื่อActionView::Helpers::FormBuilder(::อ่านว่า "ที่อยู่ข้างใน" เหมือนบอกที่อยู่ ตึก › ชั้น › ห้อง) · บอก senior ได้ว่า "f คือ form builder ที่ form_with yield ออกมา" ฟังรู้เรื่องกว่า "ตัว f"
ใน form_with model: @post do |f| … end ตัว f มาจากไหน?
ตัวเลือกที่ 3 ถูก · ใน form_with จิ๋วของเรา บรรทัด yield(builder) คือจุดที่ builder ถูกยื่นเข้า block แล้วกลายเป็น f
ตัวเลือกที่ 1 ผิด เพราะ do ไม่ได้สร้างอะไรเอง — ถ้า method ไม่ yield ของมา ช่อง |f| ก็ว่าง ·
ตัวเลือกที่ 2 ผิด เพราะเปลี่ยนเป็น |form| แล้วได้ผลเหมือนเดิม ·
ตัวเลือกที่ 4 ผิด เพราะ f คือกล่องเครื่องมือทำฟอร์ม (มี text_field) ไม่ใช่ตัวบทความ — บทความถูกส่งเข้าไปทาง model:
จุดที่ต้องจำ: เจอ |x| ที่ไม่รู้จัก ให้ถามว่า "method ไหนยื่นของชิ้นนี้มา"
ชิ้นที่ 4 — symbol อยู่ทุกที่
ในสามบรรทัดแรกมี symbol ซ่อนอยู่มากกว่าที่ตาเห็น · ทวนจากบท 92: symbol คือ "ป้ายชื่อ" ที่ Rails ใช้เรียกชื่อของต่าง ๆ เช่นชื่อช่อง ชื่อคอลัมน์ ชื่อตัวเลือก
:title— โคลอนอยู่หน้า = symbol ตัวเดี่ยว ๆpresence: true— โคลอนอยู่หลัง = คู่ใน hash ที่ชื่อช่องเป็น symbol:presencestatus: :see_other— มีทั้งสองแบบในคู่เดียว: ชื่อช่องคือ:statusค่าคือ symbol:see_other
พิสูจน์ว่าชื่อช่องใน hash เป็น symbol จริง:
# sym.rb
options = { presence: true }
p options.keys
p options.keys.first.class
p :title.class
$ ruby sym.rb
[:presence]
Symbol
Symbol
options.keys— ขอรายชื่อช่องทั้งหมดของ hash ได้เป็น array[:presence]· เห็นโคลอนย้ายมาอยู่หน้าแล้ว.first.class— หยิบตัวแรกแล้วถามว่า "เป็นชนิดอะไร" ได้Symbol
ใน Go คุณใช้ string เป็นชื่อช่องแทน เช่น c.JSON(200, gin.H{"title": "สวัสดี"}) ·
Ruby เลือกใช้ symbol เพราะเป็นป้ายชื่อที่ "ตายตัว" เทียบกันได้เร็ว
- symbol key
- ชื่อช่องใน hash ที่เป็น symbol · เขียน
{ title: "x" }ก็คือ{ :title => "x" }(=>คือวิธีเขียน hash แบบเก่า ยังเจอในโค้ดเก่า ๆ) · ถาม AI ได้ว่า "is this hash key a string or a symbol?" — สำคัญตอนอ่านค่าเพราะh[:title]กับh["title"]ไม่ใช่ช่องเดียวกัน
ในบรรทัด validates :title, presence: true อะไรเป็น symbol บ้าง?
ตัวเลือกที่ 2 ถูก · presence: true คือคู่ใน hash ที่ชื่อช่องเป็น symbol — sym.rb พิสูจน์ให้เห็นว่า keys ได้ [:presence]
ตัวเลือกที่ 1 ผิด เพราะ string ต้องมีเครื่องหมายคำพูด · ตัวเลือกที่ 3 ผิด เพราะ symbol เป็นของภาษา Ruby
ใครเรียก method อะไรก็ใช้ได้ · ตัวเลือกที่ 4 ผิด เพราะ true คือค่าจริง/เท็จ ไม่ใช่ป้ายชื่อ
จุดที่ต้องจำ: โคลอนหน้า (:title) หรือโคลอนหลัง (presence:) ก็คือ symbol ทั้งคู่
ชิ้นที่ 5 — method ที่ Rails สร้างให้ตอนรัน
นี่คือคำถามที่ผู้เรียนถามบ่อยที่สุด: "ใน Go ต้องประกาศ struct ชัดเจน แล้วใน Rails post.title มาจากไหน?"
เพราะไฟล์ model ของบทความใน Rails หน้าตาแบบนี้ — ว่างเปล่าจริง ๆ:
# app/models/post.rb — ทั้งไฟล์ (Rails สร้างให้)
class Post < ApplicationRecord
end
(< ApplicationRecord แปลว่า "สืบทอดความสามารถจาก ApplicationRecord" · บท 95)
คำตอบสั้น ๆ: ตอนโปรแกรมรัน Rails ไปอ่านชื่อคอลัมน์จากตาราง posts ในฐานข้อมูล แล้วสร้าง method ชื่อเดียวกันให้
· ตารางมีคอลัมน์ title → ได้ method post.title
คุณเห็นผลของเรื่องนี้มาแล้วใน บท 90 กับ บท 95 (Post.column_names) · ชิ้นนี้จะพาดูอีกด้าน: Rails ทำได้ยังไง — จำลองด้วย Ruby ล้วนที่รันเองได้
Go + GORM — เขียน field เอง
type Post struct {
ID uint
Title string
Body string
}
เริ่มจาก struct → GORM เอา struct ไปสร้างตาราง
Rails — ไม่เขียน field เลย
class Post < ApplicationRecord
end
เริ่มจาก ตาราง → Rails อ่านตารางแล้วสร้าง method ให้ class
สังเกตว่าทิศทางกลับกัน · Go: struct คือต้นฉบับ · Rails: ตารางในฐานข้อมูลคือต้นฉบับ
model Post คือคนดูแลห้องเก็บของ ที่ไม่มีรายการในมือเลยสักแผ่น
พอร้านเปิด เขาเดินไปดูลิ้นชักบทความ (ตาราง posts) อ่านป้ายช่องทุกช่อง: title · body …
จากนั้นใครถาม "ชื่อเรื่องของแฟ้มนี้คืออะไร" (post.title) เขาก็หยิบให้ได้ทันที
· วันไหนช่างมาต่อเติมช่องใหม่ (migration · Day 2) เขาก็รู้เอง โดยไม่ต้องมีใครไปแก้ไฟล์ post.rb
ที่ไม่เหมือนกัน: คนดูแลจริงจดรายการให้คุณดูได้ แต่ไฟล์ post.rb ไม่มีรายการเลย
— เปิดไฟล์แล้วไม่รู้ว่ามีช่องอะไร · editor ก็เดาให้ไม่ได้เต็มที่
· อยากรู้ต้องไปเปิดไฟล์ db/schema.rb ที่ Rails เขียนสรุปตารางไว้ให้
ฟังดูเหมือนเวทมนตร์ แต่ Ruby ล้วนก็ทำได้ด้วย method ชื่อ define_method (แปลว่า "สร้าง method ชื่อนี้ให้หน่อย")
· มาลองจำลองสิ่งที่ Rails ทำ ด้วย Ruby ล้วน (บทความจำลองด้วย hash เหมือนเดิม):
เอารายชื่อคอลัมน์ (สมมติว่าอ่านมาจากตาราง) มาวนสร้าง method ทีละชื่อ
# magic.rb — จำลองหลักการที่ Rails ใช้ (ของจริงซับซ้อนกว่านี้)
class Post
def initialize(data)
@data = data
end
end
# ตอนนี้ class Post ยังไม่มี method title เลย
column_names = [:title, :body] # สมมติว่าอ่านมาจากตาราง posts
column_names.each do |name|
Post.define_method(name) do # สร้าง method ชื่อตามคอลัมน์
@data[name]
end
end
post = Post.new({ title: "สวัสดี Ruby", body: "บทความแรก" })
p post.title
p post.body
p Post.instance_methods(false)
post.author
$ ruby magic.rb
"สวัสดี Ruby"
"บทความแรก"
[:title, :body]
magic.rb:21:in '<main>': undefined method 'author' for an instance of Post (NoMethodError)
post.author
^^^^^^^
column_names.each do |name|— วนทีละชื่อใน array (บท 92–93) · รอบแรกname=:titleรอบสอง =:bodyPost.define_method(name) do … end— สร้าง method ใหม่ให้ classPostโดยชื่อ method มาจากค่าในตัวแปร · ข้างในdo … endคือเนื้อของ method@data[name]— เนื้อของ method: หยิบค่าจาก hash ของบทความชิ้นที่ถูกเรียก (@dataที่จำไว้ตอนinitialize) ด้วยชื่อคอลัมน์Post.instance_methods(false)— ขอรายชื่อ method ที่ classPostมีเอง (false= ไม่รวมที่สืบทอดมา) ได้[:title, :body]post.author— ไม่มีคอลัมน์authorในรายชื่อ จึงไม่ถูกสร้าง → พังด้วยNoMethodError(หา method ชื่อนี้ไม่เจอ · บท 91) · แปลว่า class ไม่ได้มี method ทุกชื่อติดมาเอง ต้องมีคนสร้าง
ต่างจาก def ตรงที่ def title ต้องรู้ชื่อตั้งแต่ตอนพิมพ์โค้ด · define_method รับชื่อตอนโปรแกรมรัน
— ชื่อจะมาจากไหนก็ได้ รวมถึงจากฐานข้อมูล
แล้วใน Rails จริงล่ะ? ผมลองในแอป wongnok_blog ให้ดูแล้ว
(คำสั่งสร้างตารางเป็นเรื่องของ Day 2 · ตอนนี้ยังไม่ต้องทำตาม) — ไฟล์ db/schema.rb สรุปตาราง posts ไว้แบบนี้:
# db/schema.rb — ส่วนของตาราง posts
create_table "posts", force: :cascade do |t|
t.string "title"
t.text "body"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
(สังเกตว่าไฟล์นี้ใช้ชิ้นที่ 1–4 ครบ: ละวงเล็บ · hash ไม่มีปีกกา force: :cascade · symbol · block |t| ·
ความหมายของ force: :cascade กับ null: false ยังไม่ต้องสนใจ รอ Day 2)
แล้วรันโค้ดสั้น ๆ ผ่าน bin/rails runner (คำสั่งที่รันไฟล์ Ruby โดยโหลดแอป Rails ให้ด้วย):
# runner.rb
p Post.column_names
post = Post.new
post.title = "สวัสดี Rails"
p post.title
p post.body
$ bin/rails runner runner.rb
["id", "title", "body", "created_at", "updated_at"]
"สวัสดี Rails"
nil
Post.column_names— ถาม model ว่า "ตารางของคุณมีคอลัมน์อะไร" · Rails ไปอ่านจากฐานข้อมูลมาให้post.title = "สวัสดี Rails"— Rails สร้าง method สำหรับตั้งค่าให้ด้วย (ชื่อtitle=)p post.bodyได้nil(แปลว่า "ไม่มีค่า" · บท 94) เพราะยังไม่ได้ใส่อะไร — แต่ method มีอยู่ ไม่พัง
ต่อจากนั้นผมเพิ่มคอลัมน์ author ในตาราง posts (ด้วย migration — Day 2)
โดยไม่แตะไฟล์ post.rb เลยสักตัวอักษร แล้วรัน:
# runner2.rb
p Post.column_names
post = Post.new
p post.author
post.author จะพังด้วย NoMethodError เหมือนใน magic.rb หรือเปล่า?
$ bin/rails runner runner2.rb
["id", "title", "body", "created_at", "updated_at", "author"]
nil
ไม่พัง · ได้ nil (ยังไม่มีค่า) · และ post.rb หลังเพิ่มคอลัมน์ก็ยังเป็น
class Post < ApplicationRecord / end เหมือนเดิม
นี่คือข้อดีที่สไลด์พูดถึง: เพิ่มคอลัมน์ในฐานข้อมูลแล้วใช้ได้เลย ไม่ต้องไปแก้ struct แบบ Go
- attribute (attribute methods)
- ค่าของแต่ละคอลัมน์ในข้อมูลหนึ่งแถว (หนึ่งแถวเรียกว่า record) · method อย่าง
post.title/post.title =ที่ Rails สร้างจากคอลัมน์เรียกว่า attribute methods · ถาม senior ได้ว่า "method นี้ไม่มีในไฟล์ model มันมาจากคอลัมน์ในตาราง หรือมาจากที่อื่นครับ" - metaprogramming ·
define_method - โค้ดที่สร้างโค้ดอื่นตอนโปรแกรมรัน ·
define_methodคือเครื่องมือพื้นฐาน รับชื่อ method เป็นค่า · พิมพ์บอก AI ได้ว่า "explain how Rails generates attribute methods from database columns, at a beginner level" db/schema.rb- ไฟล์ที่ Rails เขียนสรุปโครงตารางทั้งหมดไว้ (เขียนใหม่ให้เองหลังแก้ตาราง · ไม่ต้องแก้มือ) · ที่แรกที่ต้องเปิดเมื่ออยากรู้ว่า model มี field อะไร
ไฟล์ post.rb มีแค่ class Post < ApplicationRecord กับ end แต่ post.title ใช้ได้ เพราะอะไร?
ตัวเลือกที่ 1 ถูก · หลักฐานคือ เพิ่มคอลัมน์ author แล้ว post.author ใช้ได้ทันทีโดยไม่แตะไฟล์ไหนเลย
ตัวเลือกที่ 2 ผิด เพราะไฟล์ post.rb มีแค่สองบรรทัดจริง ๆ ไม่มีอะไรซ่อน ·
ตัวเลือกที่ 3 ผิด เพราะใน magic.rb class ธรรมดาเรียก post.author แล้วพัง — method ไม่ได้ติดมาเอง ·
ตัวเลือกที่ 4 ผิด เพราะถ้าประกาศไว้ล่วงหน้า มันจะไม่รู้จัก author ที่เพิ่งเพิ่มทีหลัง
จุดที่ต้องจำ: method ที่ไม่มีในไฟล์ model มักมาจากคอลัมน์ในตาราง · อยากรู้ว่ามีอะไร เปิด db/schema.rb
กลับไปอ่านสามบรรทัดแรกอีกรอบ
ตอนนี้เขียนเต็มได้แล้ว — และบอกได้ว่าแต่ละบรรทัดใช้ชิ้นไหนบ้าง:
| แบบที่เห็นใน Rails | แบบเขียนเต็ม (ไว้อ่าน) | ใช้ชิ้นไหน |
|---|---|---|
validates :title, presence: true | validates(:title, { presence: true }) | 1 วงเล็บ · 2 hash · 4 symbol |
redirect_to @post, notice: "saved" | redirect_to(@post, { notice: "saved" }) | 1 วงเล็บ · 2 hash · 4 symbol |
form_with model: @post do |f| | form_with(model: @post) do |f| (ห้ามใส่ปีกกา) | 1 วงเล็บ · 3 block/yield · keyword |
f.text_field :title | f.text_field(:title) | 1 วงเล็บ · 4 symbol |
post.title | (เหมือนเดิม — เป็น method ที่ Rails สร้างจากคอลัมน์ title) | 5 method ตอนรัน |
ภารกิจ — ร้านอาหารจำลองในไฟล์เดียว
🛠 เขียน order.rb ใช้ชิ้นที่ 1–3 ของบทนี้ (~10 นาที)
- สร้างไฟล์
order.rbในโฟลเดอร์ไหนก็ได้ - เขียน method
order(menu, options)ที่p menuแล้วp options(แบบเดียวกับhash1.rb) - เรียกแบบ Rails:
order :kaprao, spicy: "มาก", egg: true - เรียกอีกครั้งแบบเขียนเต็ม (วงเล็บ + ปีกกา) ให้ได้ผลเหมือนข้อ 3
- เขียน method
serveที่puts "หยิบจาน"→yield("จานขาว")→puts "ยกไปเสิร์ฟ" - เรียก
serveพร้อม block ที่รับของชื่อplateแล้วputs "ตักกะเพราลง#{plate}" - รัน
ruby order.rb
ควรเห็น สองบรรทัดแรกซ้ำกันสองรอบ แล้วตามด้วยสามบรรทัดของการเสิร์ฟ:
:kaprao
{spicy: "มาก", egg: true}
:kaprao
{spicy: "มาก", egg: true}
หยิบจาน
ตักกะเพราลงจานขาว
ยกไปเสิร์ฟ
ถ้าเจอ error นี้ แปลว่า…
wrong number of arguments (given 1, expected 2) (ArgumentError)— เรียกorder :kapraoโดยไม่มีตัวเลือกพ่วงท้าย · method ประกาศรับ 2 ชิ้นแต่ได้ 1 · ใส่spicy: "มาก"ต่อท้ายno block given (yield) (LocalJumpError)— เรียกserveเฉย ๆ โดยไม่แนบdo … end·yieldหาลูกมือไม่เจอ
ก่อนกดเฉลย: ในข้อ 6 คำว่า "จานขาว" เดินทางจากบรรทัดไหน ไปโผล่ที่ตัวแปรไหน?
# order.rb — ร้านอาหารจำลอง
def order(menu, options)
p menu
p options
end
def serve
puts "หยิบจาน"
yield("จานขาว")
puts "ยกไปเสิร์ฟ"
end
order :kaprao, spicy: "มาก", egg: true
order(:kaprao, { spicy: "มาก", egg: true })
serve do |plate|
puts "ตักกะเพราลง#{plate}"
end
รันจริงได้ผลตรงกับกล่อง "ควรเห็น" ข้างบนทุกบรรทัด
คำตอบของคำถาม: "จานขาว" ออกจากบรรทัด yield("จานขาว") ใน serve
แล้วไปโผล่ที่ตัวแปร plate ใน |plate| — ท่าเดียวกับที่ form_with ยื่น builder ให้กลายเป็น f
✅ ถ้าตอบสี่ข้อนี้ได้ แปลว่าพร้อมเปิดโค้ด Rails แล้ว
1. validates :title, presence: true เขียนเต็มแล้วหน้าตายังไง?
2. ทำไมไม่ควรเติมปีกกาเองตอนเรียก form_with?
3. f ใน do |f| มาจากบรรทัดไหนของ method?
4. model ว่างเปล่า — จะรู้ได้ยังไงว่ามี field อะไร?
ข้อไหนตอบไม่ได้ ก็อปโค้ดกับผลรันของคุณไปถาม AI หรือถามในกลุ่มคอร์สได้เลย บอกด้วยว่า "ติดที่บทเรียน 97 ชิ้นที่เท่าไหร่"
ถ้าเปิดสไลด์ดูประกอบ
บทนี้เรียงเรื่องใหม่ให้มือใหม่อ่านง่าย · ถ้าเปิดสไลด์หน้า 33–38 คู่กัน มีจุดที่ควรรู้แค่นี้:
| หน้า | สไลด์เขียน | ของจริง | ต้องสนใจไหม |
|---|---|---|---|
| 34 | "กฎเดียว" — Ruby เห็นเป็น validates(:title, { … }) |
จริงกับ validates · redirect_to แต่ form_with ใส่ปีกกาแล้วได้ ArgumentError |
สนใจ — อย่าเติมปีกกาเอง |
| 36 | แนะนำ gem annotate แปะโครงตารางไว้หัวไฟล์ model |
rubygems.org: annotate รุ่นล่าสุด 3.2.0 (ปี 2022) ประกาศรองรับ activerecord < 8.0 → ใช้กับ Rails 8.1 ไม่ได้ ·
ตัวที่ยังพัฒนาต่อชื่อ annotaterb (ผมยังไม่ได้ลองติดตั้ง) |
สนใจ ถ้าจะลงตามสไลด์ |
| 36 | "กลไกคร่าว ๆ" — วน define_method ทุกคอลัมน์ |
ของจริงข้างใน Rails ใช้วิธีที่ซับซ้อนกว่าเพื่อความเร็ว แต่หลักการเดียวกัน | ไม่ต้อง |
| 37 | open class · 3.days.ago · Active Support |
บทนี้ไม่ได้สอน · Active Support (ของที่ Rails เติมให้ Ruby) เจอใน บท 94 (blank?) |
ไม่ต้องตอนนี้ |
| 38 | @ivar คือสะพานจาก controller ไป view · self |
บทนี้ไม่ได้สอน · @ตัวแปร อยู่ บท 95 · การส่งไป view อยู่ บท 102–103 |
ไม่ต้องตอนนี้ |
อ้างอิง
- Ruby docs — Calling Methods — อ่านย่อหน้าแรกสุด (ประโยค "the parenthesis are optional") แล้วข้ามไปหัวข้อ Keyword Arguments และ Block Argument · ครอบคลุมชิ้นที่ 1–3 ของบทนี้
- ruby-lang.org — Separation of positional and keyword arguments in Ruby 3.0 —
อ่านแค่ส่วน tl;dr ด้านบน · เป็นเหตุผลว่าทำไม
form_with({ … })ถึงพัง - Ruby docs — Module#define_method —
ดูตัวอย่างสั้น ๆ ใต้หัวข้อ · คือเครื่องมือที่ใช้ใน
magic.rb - Rails Guides — Active Record Basics — อ่านหัวข้อ 3. Creating Active Record Models ที่บอกว่าคอลัมน์ในตารางกลายเป็น attribute ของ model ให้เอง (ละเอียดรอ Day 2)
- Rails Guides — Action View Form Helpers —
ดูหัวข้อ 2. Creating Forms with Model Objects เพื่อเห็น
form_with model: … do |form|ตัวจริง (คู่มือใช้ชื่อformแทนf) - rubygems.org — annotate · annotaterb — ดูช่อง Runtime Dependencies ของแต่ละตัว เพื่อยืนยันเรื่องรุ่นของ Rails ที่รองรับ