🔁 ทวนก่อนเริ่ม (คำจากบทก่อน ๆ ที่บทนี้ใช้)
- object กับจุด — ทุกอย่างใน Ruby เป็น object ·
"abc".upcaseคือ "สั่งให้ข้อความ abc ทำตัวใหญ่" (บท 91) #{}ใน string — แทรกค่าของตัวแปรลงในข้อความ เช่น"สวัสดี #{name}"(บท 91)nil— แปลว่า "ไม่มีค่า" (บท 91 · ละเอียดบท 94)- Hash กับ symbol —
{ title: "Hello" }คือกล่องที่มีช่องชื่อ:title· ดึงค่าด้วยpost[:title](บท 92) map { |n| n * 2 }— ทำของทุกชิ้นใน Array แล้วได้ Array ใหม่ (บท 92 · บทนี้จะอธิบายว่าก้อน{ |n| ... }คืออะไรกันแน่)
แอปตัวอย่าง: คอร์สนี้สร้างแอปบล็อก — ของหลักคือบทความ ภาษาอังกฤษเรียกว่า post
บทนี้ยังไม่มี Rails เลย ทุกครั้งที่เห็น "บทความ" ในโค้ด เรากำลังจำลองด้วย Hash เช่น { title: "Hello", draft: true }
เวอร์ชันที่ใช้ตรวจ: Ruby 4.0.9 (สไลด์ใช้ 4.0.6 — รุ่นแก้บั๊กสายเดียวกัน ไวยากรณ์ในบทนี้ใช้ได้ทั้งคู่) · ตัวเทียบ Go 1.26.5 · ทุกผลรันในบทนี้ก็อปจากเครื่องจริง
1 · Method คืออะไร — ตั้งชื่อให้คำสั่งชุดหนึ่ง
method คือคำสั่งหลายบรรทัดที่เราจับมัดรวมแล้วตั้งชื่อให้ จะได้เรียกใช้ซ้ำได้ด้วยชื่อเดียว
มันคือของเดียวกับ func ใน Go ทุกประการ แค่ Ruby เรียกว่า method
ลองดู method แรก — ทักทายคนตามชื่อที่ส่งเข้ามา:
# greet.rb
def greet(name)
"สวัสดี #{name}"
end
puts greet("ต้น")
puts greet("มด")
$ ruby greet.rb
สวัสดี ต้น
สวัสดี มด
อ่านทีละบรรทัด:
def= "ต่อไปนี้คือ method ใหม่" (ย่อมาจาก define) — ตรงกับคำว่าfuncใน Gogreetคือชื่อ method ·(name)คือของที่ต้องส่งเข้ามา (เรียกว่า argument) — ไม่ต้องบอกชนิด ว่าเป็น string"สวัสดี #{name}"คือข้อความที่จะส่งกลับไป — ไม่มีคำว่าreturn(หัวข้อ 2 จะอธิบาย)end= ปิด method · Ruby ไม่ใช้ปีกกา{ }ครอบเนื้อ method แบบ Goputs greet("ต้น")= เรียก method แล้วเอาผลไปพิมพ์ออกจอ
วางคู่กับ Go ที่คุณเคยเขียน จะเห็นว่าความหมายเหมือนกันทุกส่วน ต่างแค่หน้าตา:
Go
func Greet(name string) string {
return "สวัสดี " + name
}
ต้องบอกชนิดของ argument · ต้องบอกชนิดของค่าที่คืน · ต้องเขียน return
Ruby
def greet(name)
"สวัสดี #{name}"
end
ไม่ต้องบอกชนิดเลย · ไม่ต้องเขียน return · ชื่อ method ใช้ตัวเล็กคั่นด้วย _ เช่น make_post
2 · บรรทัดสุดท้ายคือของที่ส่งกลับ
ใน Go ฟังก์ชันที่ประกาศว่าจะคืนค่า ต้องเขียน return เสมอ — ลืมแล้ว go build ฟ้อง missing return
Ruby กลับกัน: ค่าของบรรทัดสุดท้ายที่ถูกรัน คือค่าที่ method ส่งกลับเสมอ โดยไม่ต้องบอก
# add.rb
def add(a, b)
a + b # บรรทัดสุดท้าย → ค่านี้คือของที่ส่งกลับ
end
p add(2, 3)
$ ruby add.rb
5
(p คือคำสั่งพิมพ์อีกตัวหนึ่ง คล้าย puts — หัวข้อ 7 จะบอกว่าต่างกันยังไง ตอนนี้อ่านว่า "พิมพ์" ไปก่อน)
คำว่า "บรรทัดสุดท้าย" หมายถึงบรรทัดสุดท้ายที่ได้รันจริง ไม่ใช่บรรทัดล่างสุดในไฟล์
เช่นใน if / else — ถ้าเข้ากิ่งไหน บรรทัดสุดท้ายของกิ่งนั้นคือค่าที่ส่งกลับ:
# grade.rb
def grade(score)
if score >= 50
"pass" # เข้ากิ่งนี้ → ส่ง "pass" กลับ
else
"fail" # เข้ากิ่งนี้ → ส่ง "fail" กลับ
end
end
p grade(70)
p grade(10)
$ ruby grade.rb
"pass"
"fail"
คำว่า return ยังมีอยู่ และใช้ได้เหมือน Go — คนเขียน Ruby ใช้มันตอนอยากออกจาก method กลางทางเท่านั้น
ที่ไม่มีใครทำคือเขียน return ไว้บรรทัดสุดท้าย เพราะมันซ้ำ
สไลด์หน้า 23 ยังโชว์อีกแบบหนึ่งที่สั้นสุด ๆ คือ method บรรทัดเดียว def add(a, b) = a + b
(มีเครื่องหมาย = และไม่มี end) — รันแล้วได้ 5 เหมือนกัน · ใช้ได้เมื่อเนื้อ method เป็นการคำนวณสั้น ๆ อย่างเดียว
# oops.rb — เพิ่ม puts ไว้ท้าย method เพื่อดูว่ามันทำงาน
def add(a, b)
a + b
puts "บวกเสร็จแล้ว"
end
result = add(2, 3)
p result
บรรทัด p result จะพิมพ์อะไร — 5 ใช่ไหม?
$ ruby oops.rb
บวกเสร็จแล้ว
nil
ได้ nil ไม่ใช่ 5 · ไม่มี error สักบรรทัด
เพราะพอเราแทรก puts ไว้ล่างสุด บรรทัดสุดท้ายก็เปลี่ยนเป็น puts "บวกเสร็จแล้ว"
และ puts ส่งกลับ nil เสมอ (พิมพ์ได้ แต่ไม่ส่งอะไรกลับ)
ผลของ a + b ถูกคำนวณแล้ว แต่ถูกทิ้งไปเฉย ๆ เพราะมันไม่ใช่บรรทัดสุดท้ายแล้ว
ใน Go เรื่องนี้เกิดไม่ได้ เพราะ return a + b บอกชัดว่าส่งอะไร
วิธีแก้: ย้าย puts ไปไว้ก่อน a + b ให้ a + b กลับมาอยู่บรรทัดสุดท้าย
method เหมือนชื่อเมนูในร้านกาแฟ — พูดว่า "ลาเต้" คำเดียว บาริสต้ารู้เลยว่าต้องทำขั้นไหนบ้าง (ไม่ต้องอธิบายทุกขั้นทุกครั้ง)
argument คือสิ่งที่ลูกค้าบอกเพิ่ม เช่น "ลาเต้ หวานน้อย" — latte("หวานน้อย")
ทีนี้ดูตอนทำ: บดเมล็ด → ชง → เทนม
พอทำขั้นสุดท้ายเสร็จ ของที่อยู่ในมือตอนนั้นคือของที่ยื่นให้ลูกค้า ไม่ต้องพูดว่า "นี่ครับ" (ไม่ต้องมี return)
ส่วนใน Go บาริสต้าต้องพูด "นี่ครับ" (return) ทุกครั้ง — ลืมพูด ผู้จัดการ (go build) ไม่ยอมให้เปิดร้านเลย
ถ้าเราเพิ่มขั้นสุดท้ายเป็น "เช็ดเคาน์เตอร์" (เหมือนเติม puts ท้าย method)
ของในมือตอนจบก็คือผ้าเช็ดโต๊ะ ไม่ใช่กาแฟ
ที่ไม่เหมือนกัน: บาริสต้าจริงรู้ตัวว่า "เอ๊ะ ผ้าเช็ดโต๊ะไม่ใช่ของที่ลูกค้าสั่ง"
แต่ Ruby ไม่รู้ — มันยื่น nil ให้หน้าตาเฉย ไม่มี error ไม่มีคำเตือน
คนที่ต้องคอยดูว่าบรรทัดสุดท้ายคืออะไรคือคุณ
- method /
def - คำสั่งชุดหนึ่งที่ตั้งชื่อไว้ เทียบเท่า func ของ Go ·
defคือคำที่ใช้ประกาศ · พิมพ์ถาม AI ได้ว่า "write a Ruby method that takes a post hash and returns its title" - argument
- ของที่ส่งเข้าไปให้ method ในวงเล็บ เช่น
"ต้น"ในgreet("ต้น")· ใน Go เรียกเหมือนกัน - return value / implicit return
- return value = ค่าที่ method ส่งกลับ · implicit return = การส่งค่าบรรทัดสุดท้ายกลับเองโดยไม่ต้องเขียน
return· ถาม senior ได้ว่า "method นี้ return อะไรออกมา บรรทัดสุดท้ายคืออะไร" เวลาเจอค่า nil ที่ไม่คาดคิด - endless method
- method บรรทัดเดียวแบบ
def add(a, b) = a + b· ไม่มีendปิดท้าย
3 · Keyword argument — ส่งของโดยบอกชื่อช่อง
argument แบบธรรมดาจับคู่กันตามลำดับ: ตัวแรกไปช่องแรก ตัวที่สองไปช่องที่สอง
ถ้าส่งสลับกัน Ruby ไม่รู้หรอกว่าเราสลับ — ลองสร้างบทความ (จำลองด้วย Hash) แบบนี้ดู:
# post_order.rb
def make_post(title, body)
{ title: title, body: body }
end
p make_post("Hello", "My first post")
p make_post("My first post", "Hello") # เผลอสลับ
$ ruby post_order.rb
{title: "Hello", body: "My first post"}
{title: "My first post", body: "Hello"}
{ title: title, body: body } ในเนื้อ method คือ Hash จากบท 92 —
title: ตัวหน้าคือชื่อช่อง · title ตัวหลังคือตัวแปรที่ส่งเข้ามา (ชื่อซ้ำกันเฉย ๆ)
บรรทัดที่สองได้บทความที่ชื่อเรื่องเป็นเนื้อหา และเนื้อหาเป็นชื่อเรื่อง — ไม่มี error เลย
keyword argument แก้ปัญหานี้: ให้คนเรียกบอกชื่อช่องทุกครั้งที่ส่งของ
# post_kw.rb
def make_post(title:, body: "", draft: false)
{ title: title, body: body, draft: draft }
end
p make_post(title: "Hello")
p make_post(title: "Hello", draft: true)
p make_post(draft: true, title: "Hello") # สลับลำดับ
$ ruby post_kw.rb
{title: "Hello", body: "", draft: false}
{title: "Hello", body: "", draft: true}
{title: "Hello", body: "", draft: true}
อ่านบรรทัด def ทีละช่อง:
title:— มี:ต่อท้ายชื่อ แต่ไม่มีค่าตามหลัง = ช่องบังคับ ต้องส่งทุกครั้งbody: ""— มีค่าตามหลัง = ค่าเริ่มต้น ถ้าไม่ส่งมาก็ใช้""(ข้อความว่าง)draft: false— เหมือนกัน ไม่ส่งก็ได้false- ข้างใน method เรียกใช้ด้วยชื่อเปล่า ๆ
title·body·draft(ไม่มี:)
บรรทัดที่สามสลับลำดับแล้วยังได้ผลเหมือนบรรทัดที่สองทุกตัว เพราะ Ruby จับคู่ด้วยชื่อ ไม่ใช่ลำดับ
ทีนี้ลองลืมส่งช่องบังคับดู — ไฟล์ forgot.rb (บรรทัด 1–3 คือ method เดิม · บรรทัด 6 คือตัวที่ลืมส่ง title):
def make_post(title:, body: "", draft: false)
{ title: title, body: body, draft: draft }
end
puts "start"
make_post(body: "no title")
$ ruby forgot.rb
start
forgot.rb:1:in 'Object#make_post': missing keyword: :title (ArgumentError)
caller: forgot.rb:6
| make_post(body: "no title")
^^^^^^^^^
callee: forgot.rb:1
| def make_post(title:, body: "", draft: false)
^^^^^^^^^
from forgot.rb:6:in '<main>'
วิธีอ่าน error นี้:
forgot.rb:1:in 'Object#make_post'= พังในไฟล์ forgot.rb บรรทัด 1 ข้างใน methodmake_post(คำว่าObject#ข้ามไปก่อน — บท 96 อธิบายว่า # ในข้อความ error คั่นชื่อ class กับชื่อ method)missing keyword: :title= "ขาดช่องชื่อ title" ·ArgumentError= ชนิดของ error คือ "ส่ง argument ผิด"caller= บรรทัดที่เรียก (ตัวที่ลืมส่ง) ·callee= บรรทัดที่ประกาศ method ·^^^ชี้ตำแหน่งให้from forgot.rb:6:in '<main>'= "ถูกเรียกมาจากบรรทัด 6" ·<main>คือตัวไฟล์ชั้นนอกสุด ที่ไม่ได้อยู่ใน method ไหน- สังเกตว่า
startถูกพิมพ์ออกมาก่อน — Ruby เจอปัญหาตอนรันมาถึงบรรทัดนั้น ไม่ได้ตรวจล่วงหน้า
เทียบกับ Go: Go ไม่มี keyword argument · ท่าที่ใกล้สุดคือส่ง struct แล้วระบุชื่อ field ซึ่งถ้าลืม field ไหน Go ก็ใส่ค่าศูนย์ (zero value) ให้เงียบ ๆ:
type Post struct {
Title string
Body string
Draft bool
}
p := Post{Body: "no title"} // ลืม Title
fmt.Printf("%+v\n", p)
// ผลรันจริง: {Title: Body:no title Draft:false} ← Title เป็น "" ไม่มีใครบอก
(%+v = ให้ Go พิมพ์ struct พร้อมชื่อ field · Title: ที่ไม่มีอะไรตามคือข้อความว่าง)
Ruby กลับเข้มกว่าตรงนี้ — ลืมช่องบังคับแล้วพังให้เห็นทันทีที่รันถึง ไม่ปล่อยค่าว่างหลุดไป
argument แบบธรรมดาเหมือนยื่นของใส่กล่องเปล่าที่วางเรียงกัน — กล่องแรกคือชื่อเรื่อง กล่องที่สองคือเนื้อหา
ใส่สลับกล่องเมื่อไหร่ได้ของผิดทันที
keyword argument เหมือนแบบฟอร์มสมัครที่ทุกช่องมีชื่อเขียนไว้ ("ชื่อเรื่อง: ____")
กรอกช่องไหนก่อนก็ได้ · ช่องที่มีดอกจัน (title:) เว้นว่างไม่ได้ ·
ช่องที่พิมพ์ค่าไว้ให้แล้ว (draft: false) ไม่กรอกก็ได้
ที่ไม่เหมือนกัน: แบบฟอร์มจริงมีเจ้าหน้าที่ตรวจตั้งแต่ตอนยื่น แต่ Ruby ตรวจตอนรันมาถึงบรรทัดนั้นเท่านั้น — ถ้าบรรทัดที่ลืมช่องไม่เคยถูกรัน ก็ไม่มีใครรู้ว่าผิด (ต่างจาก Go ที่ compiler ตรวจหลายอย่างให้ก่อนรัน)
ในโค้ด Rails คุณจะเห็นบรรทัดหน้าตาคล้ายกันเยอะมาก เช่น render json: post, status: :created
(render คือคำสั่งของ Rails ที่ส่งผลกลับไปให้ browser — เหมือนยกจานไปเสิร์ฟลูกค้า · บท 102)
ตัวนั้นเป็นอีกท่าหนึ่งที่ดูคล้ายกัน (Hash ที่ละปีกกาได้) เดี๋ยวเจอละเอียดใน
บทเรียน 97
- positional argument
- argument ธรรมดาที่จับคู่ตามลำดับ · แบบที่ Go ใช้
- keyword argument
- argument ที่ส่งด้วยชื่อ
ชื่อ: ค่า· ลำดับไม่สำคัญ · พิมพ์บอก AI ได้ว่า "change this method to use keyword arguments" เวลา method รับของหลายตัวจนสับสนลำดับ - required keyword / default value
- required keyword = ช่องที่ไม่มีค่าเริ่มต้น ต้องส่ง (
title:) · default value = ค่าที่ใช้เมื่อไม่ได้ส่ง (draft: false) - ArgumentError
- error ที่ Ruby โยนเมื่อส่ง argument ไม่ตรงกับที่ method ประกาศ — ขาดช่อง · เกินช่อง · สะกดชื่อผิด · ถาม senior ได้ว่า "ArgumentError ตัวนี้ caller ผิดหรือ callee ผิด"
4 · ? กับ ! ท้ายชื่อ — ป้ายเตือนที่ทุกคนตกลงกัน
Ruby ยอมให้ชื่อ method ลงท้ายด้วย ? หรือ ! ได้ — มันเป็นแค่ตัวอักษรในชื่อ ภาษาไม่ได้บังคับความหมาย
แต่คนเขียน Ruby ทั้งโลกตกลงกันว่า:
? = "method นี้ตอบคำถาม ใช่/ไม่ใช่" · ! = "ระวัง ตัวนี้อันตรายกว่าตัวที่ไม่มี !"
ฝั่ง ? — ลองถามคำถามดู:
# ask.rb
p [].empty? # Array ว่างไหม
p [1, 2].empty?
p "hello".include?("ell") # มีคำว่า ell อยู่ข้างในไหม
p 5.even? # เป็นเลขคู่ไหม
def draft?(post) # เขียนเองก็ได้
post[:draft]
end
post = { title: "Hello", draft: true }
p draft?(post)
$ ruby ask.rb
true
false
true
false
true
พอเห็นชื่อลงท้าย ? ก็เดาได้ทันทีว่ามันตอบคำถาม ใช่/ไม่ใช่ (ส่วนใหญ่คืน true / false) โดยไม่ต้องเปิดดูข้างใน
เทียบกับ Go: Go ใส่ ? ในชื่อไม่ได้ เลยใช้คำว่า Is นำหน้าแทน เช่น
func IsDraft(p Post) bool — หน้าที่เดียวกันเป๊ะ
ในแอป Rails จะเจอแบบนี้บ่อย เช่น post.published? (บทความนี้เผยแพร่แล้วหรือยัง)
— ที่เขียนเป็น post. นำหน้า เพราะใน Rails บทความเป็น object ที่มี method ของตัวเอง (บท 95 กับบท 97)
ฝั่ง ! — ดูคู่ที่สไลด์หน้า 24 ยกมา upcase กับ upcase!:
# bang.rb
name = "abc"
loud = name.upcase # ไม่มี ! → สร้างข้อความใหม่
p loud
p name # ตัวเดิมเปลี่ยนไหม?
name2 = "abc"
name2.upcase! # มี ! → แก้ตัวเดิมเลย
p name2
$ ruby bang.rb
"ABC"
"abc"
"ABC"
name.upcaseสร้างข้อความตัวใหญ่อันใหม่ แล้วส่งกลับ ·nameยังเป็น"abc"เหมือนเดิมname2.upcase!แก้ข้อความเดิมให้เป็นตัวใหญ่ · ไม่ต้องเอาไปใส่ตัวแปรใหม่name2ก็เปลี่ยนแล้ว
# already.rb — ข้อความเป็นตัวใหญ่อยู่แล้ว
s = "ABC"
r = s.upcase!
p r
p s
r กับ s เป็นอะไร?
$ ruby already.rb
nil
"ABC"
r เป็น nil · upcase! ส่งกลับ nil เมื่อไม่มีอะไรต้องเปลี่ยน
(คู่มือของ Ruby เขียนไว้ว่ามันคืน "self or nil")
ถ้าเอาค่าที่มันคืนไปใช้ต่อทันที จะพังเฉพาะตอนข้อมูลเป็นตัวใหญ่อยู่แล้ว:
name = "ADMIN"
puts name.upcase!.length
$ ruby chain.rb
chain.rb:2:in '<main>': undefined method 'length' for nil (NoMethodError)
puts name.upcase!.length
^^^^^^^
undefined method 'length' for nil = "สั่ง length กับของที่เป็น nil ไม่ได้"
กฎง่าย ๆ: method ที่มี ! ให้เรียกเป็นบรรทัดเดี่ยว ๆ อย่าเอาค่าที่มันคืนไปต่อ
upcase (ไม่มี !) คือถ่ายเอกสารก่อน แล้วแก้บนสำเนา — ต้นฉบับในแฟ้มเหมือนเดิม คุณได้แผ่นใหม่กลับมา
upcase! คือเอาปากกาเขียนทับต้นฉบับเลย — ไม่เปลืองกระดาษ แต่แก้แล้วย้อนไม่ได้
เครื่องหมาย ! คือป้าย "ระวัง เขียนทับของจริงนะ"
ที่ไม่เหมือนกัน: คนที่เขียนทับต้นฉบับเสร็จจะยื่นเอกสารคืนให้เสมอ
แต่ upcase! ถ้าไม่มีอะไรต้องแก้ จะยื่นมือเปล่ากลับมา (nil)
และภาพ "เขียนทับ" ใช้ได้กับ method ของ Ruby เองเท่านั้น — ใน Rails ! มีความหมายต่างไปนิด (กล่องข้างล่าง)
💡 ใน Rails จะเจอ save กับ save! (บันทึกบทความลงฐานข้อมูล — เหมือนเก็บแฟ้มเข้าตู้ของร้าน · เจอจริงใน Day 2)
save ถ้าบันทึกไม่สำเร็จจะคืน false แล้วไปต่อเงียบ ๆ ·
save! ถ้าไม่สำเร็จจะหยุดโปรแกรมด้วย error ดัง ๆ
ความหมายร่วมยังเป็นข้อตกลงเดิม: ตัวที่มี ! "อันตรายกว่า" — แต่ใน Rails อันตรายแบบ "พังให้เห็น" ไม่ใช่ "เขียนทับ"
- predicate method (
?) - method ที่ตอบคำถาม ใช่/ไม่ใช่ · ชื่อลงท้าย
?ตามข้อตกลง ไม่ใช่กฎภาษา · พิมพ์บอก AI ได้ว่า "add a predicate method draft? that returns true or false" - bang method (
!) - method เวอร์ชัน "อันตรายกว่า" · ใน Ruby เอง = แก้ของเดิม · ใน Rails (เช่น
save!) = ไม่สำเร็จแล้ว error · คนอ่านออกเสียง!ว่า "bang" - mutate / in-place
- แก้ของตัวเดิมแทนการสร้างอันใหม่ · ถาม senior ได้ว่า "ตรงนี้ควร mutate ของเดิม หรือคืนค่าใหม่ดี"
5 · Block — ฝากก้อนโค้ดให้ method ไปทำให้
ในบท 92 คุณเขียน [1, 2, 3].map { |n| n * 2 } มาแล้ว — ก้อน { |n| n * 2 } ตรงนั้นแหละคือ block
block คือก้อนโค้ดที่เราส่งติดไปกับการเรียก method แล้ว method จะเป็นคนเรียกใช้ก้อนนั้นเองตามจังหวะของมัน
(เช่นเรียกหนึ่งครั้งต่อของหนึ่งชิ้น)
ตัวที่ง่ายที่สุดคือ each — "ของแต่ละชิ้นใน Array ให้ทำตามก้อนนี้":
# each.rb
[10, 20, 30].each { |n| puts n }
$ ruby each.rb
10
20
30
{ ... }= ขอบของ block (ปีกกาตรงนี้ไม่ใช่ Hash — มันอยู่ติดท้ายการเรียก method)|n|= ชื่อที่เราตั้งเอง ไว้รับของที่eachส่งเข้ามาทีละชิ้น · จะตั้งชื่อxหรือpriceก็ได้puts n= สิ่งที่ให้ทำกับของแต่ละชิ้น ·eachเรียก block นี้ 3 ครั้ง ครั้งละหนึ่งตัวเลข
อีกตัวที่เจอบ่อยคือ times — "ทำซ้ำ N ครั้ง":
# times.rb
3.times { |i| puts "จานที่ #{i}" }
$ ruby times.rb
จานที่ 0
จานที่ 1
จานที่ 2
(นับจาก 0 เหมือน index ใน Go) · สังเกตว่าตัวเลข 3 ก็เป็น object ที่มี method ชื่อ times ของตัวเอง
block ยาวหลายบรรทัด ใช้ do ... end แทนปีกกา — ความหมายเดียวกัน:
# each_posts.rb — บทความ 2 ชิ้น (จำลองด้วย Hash)
posts = [
{ title: "Hello", draft: false },
{ title: "Ruby 101", draft: true }
]
posts.each do |post|
puts "บทความ: #{post[:title]}"
puts "เป็นฉบับร่างไหม: #{post[:draft]}"
end
$ ruby each_posts.rb
บทความ: Hello
เป็นฉบับร่างไหม: false
บทความ: Ruby 101
เป็นฉบับร่างไหม: true
do= เปิด block ·end= ปิด block (แทน{กับ})|post|= ชื่อที่รับบทความทีละชิ้น — ชิ้นแรกคือ Hash ของ "Hello" ชิ้นที่สองคือ "Ruby 101"- ธรรมเนียมที่ทีมส่วนใหญ่ใช้ (สไลด์หน้า 22): บรรทัดเดียวใช้
{ }· หลายบรรทัดใช้do ... end
เทียบกับ Go: ใน Go คุณทำแบบเดียวกันได้ด้วยการส่ง func เข้าไปเป็น argument ตัวหนึ่ง:
Go — func เป็น argument ธรรมดา
func Each(nums []int, f func(n int)) {
for _, n := range nums {
f(n)
}
}
Each([]int{10, 20, 30}, func(n int) {
fmt.Println(n)
})
f func(n int) = argument ตัวที่สองเป็น "ฟังก์ชันที่รับ int หนึ่งตัว" · ต้องเขียน Each เอง · ส่ง func อยู่ในวงเล็บ
Ruby — block ห้อยท้าย
[10, 20, 30].each { |n| puts n }
each มีให้แล้ว · ไม่ต้องบอกชนิด · block มีช่องพิเศษของมันเองอยู่นอกวงเล็บ (ส่งได้ก้อนเดียวต่อการเรียกหนึ่งครั้ง)
ทั้งสองฝั่งรันแล้วได้ 10 · 20 · 30 เหมือนกัน (Go ตัวนี้ go build ผ่านจริง)
แอบดูข้างใน: method ที่รับ block เรียกใช้ก้อนนั้นด้วยคำว่า yield ("ถึงตาก้อนที่ฝากมาแล้ว")
— ตอนนี้ยังไม่ต้องเขียนเอง ดูไว้ให้รู้ว่าข้างในหน้าตาแบบนี้:
# yield.rb
def serve_twice
puts "เริ่มเสิร์ฟ"
yield # เรียก block ที่ฝากมา ครั้งที่ 1
yield # ครั้งที่ 2
puts "เสิร์ฟครบแล้ว"
end
serve_twice { puts " ยกจานออกไป 1 จาน" }
$ ruby yield.rb
เริ่มเสิร์ฟ
ยกจานออกไป 1 จาน
ยกจานออกไป 1 จาน
เสิร์ฟครบแล้ว
def serve_twiceไม่มีวงเล็บ = method นี้ไม่รับ argument ธรรมดาเลย (block ไม่ต้องประกาศไว้ในวงเล็บ)yieldแต่ละครั้ง = กระโดดไปรันก้อน{ puts ... }หนึ่งรอบ แล้วกลับมาทำบรรทัดถัดไปserve_twice { ... }= เรียก method แล้วฝาก block ไปด้วย — ไม่มีวงเล็บเพราะไม่มี argument ให้ส่ง
each เหมือนแม่ครัวที่รู้วิธีหยิบวัตถุดิบออกจากตะกร้าทีละชิ้น แต่ไม่รู้ว่าหยิบแล้วต้องทำอะไร
block คือสูตรที่คุณเขียนฝากไว้ ("ได้ชิ้นไหนมา ให้ทอดชิ้นนั้น")
แม่ครัวหยิบชิ้นที่ 1 → ทำตามสูตร · หยิบชิ้นที่ 2 → ทำตามสูตรเดิม · ทำจนหมดตะกร้า
|post| คือคำว่า "ชิ้นนี้" ในสูตร และ yield คือจังหวะที่แม่ครัวหยิบสูตรขึ้นมาอ่าน
ที่ไม่เหมือนกัน: แม่ครัวจริงที่ได้สูตรไปแล้วไม่ทำ จะบอกคุณว่า "ไม่ได้ทำนะ"
แต่ method ที่ไม่ได้ yield จะทิ้งสูตรเงียบ ๆ ไม่มี error —
เช่น method hello ที่แค่ puts "hi" พอเรียก hello { puts "ก้อนที่ฝากมา" } รันจริงได้แค่ hi
(หัวข้อ 6 จะเจอกับดักที่เกิดจากเรื่องนี้)
ในโค้ด Rails จะเจอ do |...| ... end แทบทุกไฟล์ เช่นที่สไลด์หน้า 22 ยกมา
respond_to do |format| ... end
(คำสั่งที่อยู่ใน controller — หัวหน้าเชฟของแผนก — ใช้เลือกว่าจะจัดจานเป็นหน้าเว็บหรือเป็นข้อมูล)
อ่านได้แล้วว่า "เรียก method respond_to แล้วฝาก block หลายบรรทัดไปด้วย ·
format คือของที่ respond_to ส่งเข้ามาให้ใน block" (บท 97 เล่าสั้น ๆ ว่า format คืออะไร · ได้ใช้จริงใน Day 3)
- block
- ก้อนโค้ดที่ห้อยท้ายการเรียก method ด้วย
{ }หรือdo ... end· method เป็นคนเรียกใช้ · พิมพ์ถาม AI ได้ว่า "this method takes a block — what does it pass into the block?" - block parameter (
|x|) - ชื่อที่รับของที่ method ส่งเข้ามาใน block · เทียบได้กับ parameter
n intของ func ที่ส่งเข้าไปใน Go yield- คำสั่งในตัว method ที่แปลว่า "เรียก block ที่ฝากมาตอนนี้" · ถ้าสั่ง yield แต่ไม่มีใครฝาก block มา
จะได้
no block given (yield) (LocalJumpError)· ถาม senior ได้ว่า "method นี้ yield อะไรเข้ามาใน block บ้าง"
6 · กับดัก — puts หน้าบรรทัด + do ... end
หัวข้อที่แล้วบอกว่า { } กับ do ... end "ความหมายเดียวกัน" — เกือบจริง
มีจุดเดียวที่ต่าง และมันโผล่ตอนที่คุณแปะ puts หรือ p ไว้หน้าบรรทัดเพื่อ debug
# trap.rb — ต่างกันแค่ { } กับ do...end
puts [1, 2].map { |n| n * 2 }
puts "---"
puts [1, 2].map do |n| n * 2 end
ส่วนบนกับส่วนล่าง (คั่นด้วย ---) จะพิมพ์เหมือนกันไหม?
$ ruby trap.rb
2
4
---
#<Enumerator:0x000000012457de90>
ไม่เหมือน · ส่วนบนได้ 2 กับ 4 ตามที่หวัง · ส่วนล่างได้ #<Enumerator:0x…>
(เลขฐานสิบหกของคุณจะต่างไป เปลี่ยนทุกครั้งที่รัน) · ไม่มี error · ไม่มีคำเตือน
หน้าตา #<ชื่อชนิด:เลข> คือวิธีที่ Ruby พิมพ์ object ที่ไม่มีหน้าตาเป็นข้อความ — เห็นเมื่อไหร่แปลว่า "ได้ของผิดชนิดมา"
เหตุผล: { } ติดกับ method ที่อยู่ใกล้ที่สุด (map) ·
ส่วน do ... end ติดกับ method ที่อยู่ไกลที่สุด (puts ซ้ายสุด)
Ruby จึงอ่านบรรทัดล่างว่า puts([1, 2].map) do ... end —
map ไม่ได้ block เลยคืน Enumerator (ของที่ "ยังรอ block อยู่") ส่วน block ไปตกที่ puts ซึ่งไม่สนใจมัน
บรรทัดเดียวใช้ { } · หรือใส่วงเล็บให้ puts( ... ) ·
หรือเก็บผลลงตัวแปรก่อนแล้วค่อยพิมพ์ — ผลรันจริงของ puts([1, 2].map do |n| n * 2 end) ได้ 2 กับ 4 ปกติ
บรรทัดแบบ respond_to do |format| ใน Rails ปลอดภัย เพราะมี method ตัวเดียว block ไม่มีที่ให้ติดผิด
7 · puts กับ p — เครื่องมือแอบดูค่าที่ใช้ทุกวัน
ทั้งสองตัวพิมพ์ค่าออกจอ แต่พิมพ์ให้คนละคนอ่าน:
puts พิมพ์ให้ผู้ใช้อ่าน (สวย ๆ) · p พิมพ์ให้โปรแกรมเมอร์อ่าน (บอกชนิดให้เห็น)
# show.rb
puts 1
puts "1"
p 1
p "1"
puts nil
puts ""
p nil
p ""
$ ruby show.rb
1
1
1
"1"
nil
""
puts 1กับputs "1"ออกมาเหมือนกันเป๊ะ — ดูไม่ออกว่าตัวไหนเป็นตัวเลข ตัวไหนเป็นข้อความp "1"มีฟันหนูให้เห็น → รู้ทันทีว่าเป็น stringputs nilกับputs ""ได้บรรทัดว่างเหมือนกัน (สองบรรทัดว่างตรงกลาง) — แยกไม่ออกเลยว่า "ไม่มีค่า" หรือ "ข้อความว่าง"pบอกตรง ๆ ว่าnilหรือ""
อีกความต่างคือค่าที่ส่งกลับ (เชื่อมกับหัวข้อ 2):
# give_back.rb
a = puts "hello"
b = p "hello"
p a
p b
$ ruby give_back.rb
hello
"hello"
nil
"hello"
puts ส่งกลับ nil เสมอ · p ส่งของเดิมกลับมา —
เพราะงั้นแทรก p กลางการคำนวณเพื่อแอบดูค่าได้ เช่น x = p(1 + 2) * 10 พิมพ์ 3 แล้ว x ยังได้ 30 (รันจริงแล้ว)
puts | p | |
|---|---|---|
| พิมพ์ให้ใครอ่าน | ผู้ใช้ — "1" ออกมาเป็น 1 | โปรแกรมเมอร์ — "1" ออกมาเป็น "1" |
พิมพ์ nil | บรรทัดว่าง | nil |
| ส่งกลับ | nil เสมอ | ของเดิม |
| ใช้ตอนไหน | แสดงข้อความให้คนอ่าน | debug — แอบดูค่า |
ในแอป Rails ที่รันจริง คนจะใช้ Rails.logger (ตัวจดบันทึกของแอป — เหมือนสมุดบันทึกประจำครัว) แทนทั้งคู่ — Day 1 ยังไม่ได้ใช้ตัวนี้ · เครื่องมือดูค่าที่ Day 1 ใช้แทน puts คือ debugger ใน
บทเรียน 104
puts เหมือนพนักงานที่อ่านใบสั่งออกเสียงให้ลูกค้าฟัง — อ่านให้ฟังรื่นหู ไม่บอกว่าเป็นเลขหรือเป็นคำ
อ่านเสร็จก็ขยำกระดาษทิ้ง (คืน nil)
p เหมือนพนักงานตรวจของ — ชูของให้ดูพร้อมบอกชัดว่าเป็นอะไร (ข้อความ 1 จะมีฟันหนูครอบให้เห็น ไม่ใช่เลข 1 เฉย ๆ)
แล้วส่งของคืนใส่มือคุณ (คืนของเดิม) จึงแทรกกลางงานได้โดยของไม่หาย
ที่ไม่เหมือนกัน: พนักงานจริงเจอกระดาษเปล่าจะบอกว่า "ไม่มีอะไรให้อ่านครับ"
แต่ puts nil แค่เว้นบรรทัดว่างเงียบ ๆ — ดูไม่ออกเลยว่าไม่มีค่า หรือเป็นข้อความว่าง
puts/pputs= พิมพ์แบบให้คนอ่าน คืน nil ·p= พิมพ์แบบเห็นชนิด คืนของเดิม · กฎจำง่าย: debug ใช้pinspect- method ที่แปลงของเป็นข้อความแบบโปรแกรมเมอร์อ่าน ·
p xก็คือพิมพ์x.inspect· พิมพ์ถาม AI ได้ว่า "why does p show quotes but puts does not?"
ภารกิจ — เขียนเครื่องมือจัดการบทความ 4 ตัว
🎯 ภารกิจ: post_tools.rb (~10 นาที)
- เปิด terminal · สร้างโฟลเดอร์แล้วเข้าไป:
mkdir ruby93 && cd ruby93 - สร้างไฟล์
post_tools.rbในโฟลเดอร์นั้นด้วย editor ที่ใช้อยู่ (เช่น VS Code) ให้มี method 4 ตัว (ห้ามใช้คำว่าreturn):make_postรับ keywordtitle:(บังคับ) ·body:(ค่าเริ่มต้น"") ·draft:(ค่าเริ่มต้นfalse) แล้วคืน Hash สามช่องdraft?(post)คืนค่าช่อง:draftของบทความtitles(posts)รับ Array ของบทความ คืน Array ของชื่อเรื่อง (ใช้mapกับ block)shout(text)คืนข้อความตัวใหญ่ โดยไม่แก้ตัวเดิม
- ก็อปส่วนตรวจนี้ไปวางท้ายไฟล์ (
==คือ "เท่ากันไหม" ได้ true/false เหมือน Go):# ---- ส่วนตรวจ: ทุกบรรทัดต้องพิมพ์ true ---- first = make_post(title: "Hello") second = make_post(title: "Ruby 101", draft: true) p first == { title: "Hello", body: "", draft: false } p draft?(second) == true p titles([first, second]) == ["Hello", "Ruby 101"] name = "abc" p shout(name) == "ABC" p name == "abc" - รัน
ruby post_tools.rb— ควรเห็นtrue5 บรรทัด - ลองทำพังโดยตั้งใจ: แก้
shoutให้ใช้upcase!แล้วรันใหม่ — บรรทัดไหนเปลี่ยนเป็นfalse? เพราะอะไร?
ถ้าเจอ error พวกนี้ แปลว่า…
wrong number of arguments (given 0, expected 1)ชี้ไปที่make_post→ คุณลืม:หลังtitleในบรรทัดdef(Ruby เลยมองว่าเป็น argument ธรรมดาที่ต้องส่งตามลำดับ)syntax errors foundพร้อมexpected an `end` to close the `def` statement→ ลืมendปิด method ตัวที่มันชี้undefined method 'draft' for main→ เรียกdraftโดยลืม?—?เป็นส่วนหนึ่งของชื่อ ขาดไปก็คือคนละชื่อ (main= ตัวไฟล์ชั้นนอกสุด แบบเดียวกับ<main>ในหัวข้อ 3)
# post_tools.rb — จำลองบทความด้วย Hash (ยังไม่มี Rails)
def make_post(title:, body: "", draft: false)
{ title: title, body: body, draft: draft }
end
def draft?(post)
post[:draft]
end
def titles(posts)
posts.map { |post| post[:title] }
end
def shout(text)
text.upcase
end
# ---- ส่วนตรวจ (เหมือนในโจทย์) ----
$ ruby post_tools.rb
true
true
true
true
true
ทุก method ไม่มี return เลย — แต่ละตัวมีบรรทัดเดียว บรรทัดนั้นจึงเป็นค่าที่ส่งกลับ ·
titles ส่งกลับผลของ map ซึ่งเป็น Array ใหม่ของชื่อเรื่อง
ข้อ 5 — เปลี่ยน shout เป็น text.upcase! แล้วรันจริง:
$ ruby post_tools.rb
true
true
true
true
false
บรรทัดสุดท้าย (p name == "abc") เปลี่ยนเป็น false — เพราะ upcase! เขียนทับต้นฉบับ
name จึงกลายเป็น "ABC" ไปด้วย · ส่วนบรรทัดที่สี่ยัง true เพราะ "abc" มีของให้เปลี่ยน
upcase! เลยไม่คืน nil
นี่คือเหตุผลที่ "ส่วนตรวจ" ต้องเช็คทั้งค่าที่ได้และของเดิม — ถ้าเช็คแค่ค่าที่ได้ บั๊กนี้หลุดไปเงียบ ๆ
ทดสอบความเข้าใจ
method นี้ถูกเรียกด้วย x = total([10, 20]) · x มีค่าเท่าไหร่?
def total(prices)
sum = prices.sum
puts "รวม #{sum}"
end
ตัวเลือกที่ 3 ถูก · รันจริงได้ รวม 30 ออกจอ แล้ว p x ได้ nil
— ค่าที่ส่งกลับคือบรรทัดสุดท้าย ซึ่งคือ puts ... และ puts คืน nil เสมอ
ตัวเลือกที่ 1 ผิดเพราะ Ruby ไม่ได้ดูว่าตัวแปรไหน "สำคัญ" ดูแค่บรรทัดสุดท้าย · ตัวเลือกที่ 2 ผิดเพราะ "รวม 30" คือของที่ puts พิมพ์ ไม่ใช่ของที่มันส่งกลับ · ตัวเลือกที่ 4 ผิดเพราะ Ruby ไม่บังคับ return — นั่นคือนิสัยจาก Go
จุดที่ต้องจำ: แปะ puts ท้าย method = เปลี่ยนค่าที่ส่งกลับเป็น nil โดยไม่รู้ตัว
มี def make_post(title:, body: "", draft: false) · ไฟล์หนึ่งเขียน puts "start" แล้วตามด้วย make_post(body: "x") · รันแล้วเกิดอะไร?
ตัวเลือกที่ 1 ถูก · รันจริงได้ start ก่อน แล้วตามด้วย missing keyword: :title (ArgumentError)
— Ruby ตรวจ argument ตอนรันมาถึงบรรทัดนั้น
ตัวเลือกที่ 2 ผิดเพราะ Ruby ไม่ได้ตรวจล่วงหน้าแบบ compiler ของ Go (start จึงถูกพิมพ์ไปแล้ว) ·
ตัวเลือกที่ 3 เป็นพฤติกรรมของ struct ใน Go ไม่ใช่ช่องบังคับของ Ruby ·
ตัวเลือกที่ 4 ผิดเพราะ keyword จับคู่ด้วยชื่อ — "x" ถูกส่งไปที่ช่อง body ไม่ใช่ title
จุดที่ต้องจำ: ช่องที่ไม่มีค่าเริ่มต้น (title:) ต้องส่งเสมอ ไม่ส่ง = ArgumentError ตอนรันถึง
โค้ด name = "abc" · name.upcase · p name จะพิมพ์อะไร?
ตัวเลือกที่ 4 ถูก · รันจริงได้ "abc" · upcase (ไม่มี !) สร้างข้อความตัวใหญ่อันใหม่ส่งกลับมา
แต่เราไม่ได้เก็บไว้ มันจึงหายไป ส่วน name ยังเหมือนเดิม
ตัวเลือกที่ 1 คือพฤติกรรมของ upcase! (มี !) ·
ตัวเลือกที่ 2 ก็เป็นเรื่องของ upcase! ตอนข้อความเป็นตัวใหญ่อยู่แล้ว — และ p name พิมพ์ name ไม่ใช่ผลของ upcase ·
ตัวเลือกที่ 3 ผิดเพราะ Ruby ยอมให้เรียก method แล้วทิ้งผลได้ ไม่มี error
จุดที่ต้องจำ: ไม่มี ! = ได้ของใหม่ (ต้องเก็บเอง) · มี ! = แก้ของเดิม
ใน posts.each do |post| puts post[:title] end คำว่า post ที่อยู่ระหว่าง | | คืออะไร?
ตัวเลือกที่ 2 ถูก · |post| คือ block parameter — each หยิบบทความทีละชิ้นแล้วส่งเข้ามาในชื่อนี้
จะเปลี่ยนเป็น |x| ก็ทำงานเหมือนเดิม (แค่อ่านยากขึ้น)
ตัวเลือกที่ 1 ผิดเพราะชื่อใน | | เกิดขึ้นเองตอน block ถูกเรียก ไม่ต้องประกาศก่อน ·
ตัวเลือกที่ 3 ผิดเพราะสิ่งที่ทำกับแต่ละชิ้นคือเนื้อใน block (puts ...) ไม่ใช่ชื่อใน | | ·
ตัวเลือกที่ 4 ผิดเพราะไม่มีกฎให้ตรงกับชื่อ Array — ตั้ง post ให้ posts เป็นแค่ธรรมเนียมที่อ่านง่าย
จุดที่ต้องจำ: |x| = "ชิ้นนี้" ในสูตรที่ฝากให้ method ทำทีละชิ้น
✅ ถ้าตอบได้สามข้อนี้ บทนี้ผ่านแล้ว:
1. method ที่ไม่มี return ส่งอะไรกลับ — และอะไรทำให้มันกลายเป็น nil โดยไม่รู้ตัว?
2. upcase กับ upcase! ต่างกันยังไง และ "ABC".upcase! คืนอะไร?
3. ทำไม puts [1, 2].map do |n| n * 2 end ถึงไม่ได้ 2 กับ 4?
ติดตรงไหน ถามกลับได้เลย เช่น "ช่วยเขียน method ที่รับ block ให้ดูอีกตัว" หรือ "ใน Go มีอะไรที่ใกล้กับ keyword argument ที่สุด"
ถ้าเปิดสไลด์ดูประกอบ
สไลด์หน้า 22–25 เขียนสั้นและถูกเป็นส่วนใหญ่ · จุดที่ของจริงต่างจากที่สไลด์เขียน หรือสไลด์ไม่ได้บอก:
| หน้า | สไลด์เขียน | ของจริง | ต้องสนใจไหม |
|---|---|---|---|
| 22 | บรรทัดเดียวใช้ { } · หลายบรรทัดใช้ do...end (ฟังเหมือนเรื่องความสวย) |
สองแบบติดกับ method ต่างกัน — มี puts/p นำหน้าแล้วได้ผลต่าง |
✅ สนใจ (หัวข้อ 6) |
| 24 | name.upcase! "แก้ตัวเดิมเลย" |
ถูก แต่ถ้าไม่มีอะไรต้องเปลี่ยนจะคืน nil |
✅ สนใจ (หัวข้อ 4) |
| 24 | ? = "ถามแล้วได้ boolean" |
ส่วนใหญ่ใช่ แต่บางตัวคืนค่าอื่น เช่น 5.nonzero? ได้ 5 · 0.nonzero? ได้ nil |
ยังไม่ต้อง — บท 94 อธิบายว่าทำไมใช้แทนจริง/เท็จได้ |
| 24 | post.save / post.save! |
เป็นของ Rails ไม่ใช่ Ruby ล้วน — รันในไฟล์ .rb เปล่า ๆ ไม่ได้ |
รอ Day 2 |
| 25 | "puts จะไม่โผล่ใน log ของ production" | Rails 8.1 ตั้ง production ให้ log ออกทางหน้าจอ (STDOUT) ซึ่งเป็นทางเดียวกับ puts จึงโผล่ปนได้ — แต่คำแนะนำ "ใช้ Rails.logger" ยังถูก | ยังไม่ต้อง (Day 1 ใช้ p กับ debugger ในบท 104 แทน) |
| ทุกหน้า | Ruby 4.0.6 | เครื่องที่ใช้ตรวจเป็น 4.0.9 — รุ่นแก้บั๊ก ไวยากรณ์เหมือนกัน | ไม่ต้อง |
อ้างอิง
- Ruby 4.0 — Methods —
อ่านหัวข้อ Return Values (method คืนค่าบรรทัดสุดท้าย) · Method Names (ย่อหน้าเรื่องชื่อที่ลงท้าย
?กับ!ว่าเป็นแค่ข้อตกลง) · และ Keyword Arguments (ฝั่งประกาศ) - Ruby 4.0 — Calling Methods — อ่านหัวข้อ Block Argument ย่อหน้าที่เริ่มด้วย "do end has lower precedence than { }" — คำอธิบายทางการของกับดักในหัวข้อ 6
- Ruby 4.0 — String#upcase! —
ดูแค่บรรทัดแรก
upcase!(mapping) → self or nil— คำว่า "or nil" คือที่มาของ predict ในหัวข้อ 4 - Ruby 4.0 — Kernel#p และ Kernel#puts — เทียบกันว่าแต่ละตัวคืนอะไร (ตรงกับตารางในหัวข้อ 7)
- The Go Programming Language Specification — Function literals —
ฝั่ง Go ที่ใช้เทียบกับ block: func ที่เขียนสด ๆ ตรงที่ใช้ แล้วส่งเป็น argument ได้ (แบบ
Eachในหัวข้อ 5)