บทเรียน 93 · Rails · Day 1

Method และ Block — เขียนคำสั่งของตัวเอง และส่งโค้ดเป็นก้อน

~20 นาที · ควรผ่าน บทเรียน 91 (ตัวแปร · string) และ บทเรียน 92 (Array · Hash · map) มาก่อน

สองบทที่แล้วคุณเรียกใช้คำสั่งที่ Ruby มีให้อยู่แล้ว เช่น "hi".upcase หรือ [1, 2].sum
บทนี้คุณจะเขียนคำสั่งของตัวเอง (เรียกว่า method) แบบเดียวกับที่เคยเขียน func ใน Go และจะเข้าใจของสามอย่างที่ทำให้โค้ด Ruby ดูแปลกตาที่สุด: method ที่ไม่มีคำว่า return · ชื่อที่ลงท้ายด้วย ? กับ ! · และก้อน do ... end ที่ห้อยท้ายคำสั่ง (เรียกว่า block)
อ่านจบแล้วคุณจะเปิดโค้ด Rails แล้วอ่านออกว่าบรรทัดพวกนี้กำลังทำอะไร

🔁 ทวนก่อนเริ่ม (คำจากบทก่อน ๆ ที่บทนี้ใช้)

แอปตัวอย่าง: คอร์สนี้สร้างแอปบล็อก — ของหลักคือบทความ ภาษาอังกฤษเรียกว่า 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
สวัสดี ต้น
สวัสดี มด

อ่านทีละบรรทัด:

วางคู่กับ 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 ทีละช่อง:

บรรทัดที่สามสลับลำดับแล้วยังได้ผลเหมือนบรรทัดที่สองทุกตัว เพราะ 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 นี้:

เทียบกับ 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"
# 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 ที่มี ! ให้เรียกเป็นบรรทัดเดี่ยว ๆ อย่าเอาค่าที่มันคืนไปต่อ

📄 เทียบให้เห็นภาพ — ถ่ายเอกสารแล้วแก้บนสำเนา vs เขียนทับต้นฉบับ

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

อีกตัวที่เจอบ่อยคือ 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

เทียบกับ 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 จาน
เสิร์ฟครบแล้ว
👩‍🍳 เทียบให้เห็นภาพ — ฝากสูตรไว้กับแม่ครัว

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
""

อีกความต่างคือค่าที่ส่งกลับ (เชื่อมกับหัวข้อ 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 (รันจริงแล้ว)

putsp
พิมพ์ให้ใครอ่านผู้ใช้ — "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 / p
puts = พิมพ์แบบให้คนอ่าน คืน nil · p = พิมพ์แบบเห็นชนิด คืนของเดิม · กฎจำง่าย: debug ใช้ p
inspect
method ที่แปลงของเป็นข้อความแบบโปรแกรมเมอร์อ่าน · p x ก็คือพิมพ์ x.inspect · พิมพ์ถาม AI ได้ว่า "why does p show quotes but puts does not?"

ภารกิจ — เขียนเครื่องมือจัดการบทความ 4 ตัว

🎯 ภารกิจ: post_tools.rb (~10 นาที)

  1. เปิด terminal · สร้างโฟลเดอร์แล้วเข้าไป: mkdir ruby93 && cd ruby93
  2. สร้างไฟล์ post_tools.rb ในโฟลเดอร์นั้นด้วย editor ที่ใช้อยู่ (เช่น VS Code) ให้มี method 4 ตัว (ห้ามใช้คำว่า return):
    • make_post รับ keyword title: (บังคับ) · body: (ค่าเริ่มต้น "") · draft: (ค่าเริ่มต้น false) แล้วคืน Hash สามช่อง
    • draft?(post) คืนค่าช่อง :draft ของบทความ
    • titles(posts) รับ Array ของบทความ คืน Array ของชื่อเรื่อง (ใช้ map กับ block)
    • shout(text) คืนข้อความตัวใหญ่ โดยไม่แก้ตัวเดิม
  3. ก็อปส่วนตรวจนี้ไปวางท้ายไฟล์ (== คือ "เท่ากันไหม" ได้ 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"
  4. รัน ruby post_tools.rb — ควรเห็น true 5 บรรทัด
  5. ลองทำพังโดยตั้งใจ: แก้ shout ให้ใช้ upcase! แล้วรันใหม่ — บรรทัดไหนเปลี่ยนเป็น false? เพราะอะไร?

ถ้าเจอ error พวกนี้ แปลว่า…

# 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 — รุ่นแก้บั๊ก ไวยากรณ์เหมือนกัน ไม่ต้อง

อ้างอิง