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

อะไรนับว่า "จริง" — nil, true/false และ ||=

~20 นาที · ควรผ่าน บทเรียน 93 (method ที่ลงท้ายด้วย ?) มาก่อน

ใน Go เวลาเขียน if ต้องถามเป็นประโยคใช่/ไม่ใช่ชัด ๆ เสมอ เช่น if views > 0 ส่วน Ruby ยอมให้ใส่ค่าอะไรก็ได้ลงไปใน if ตรง ๆ แล้วมันตัดสินเองว่า "ผ่าน" หรือ "ไม่ผ่าน"
อ่านจบบทนี้คุณจะ: รู้ว่าค่าไหนผ่าน if ค่าไหนไม่ผ่าน · เลี่ยงบั๊กที่เจอบ่อยที่สุดคือ บทความที่มีคนอ่าน 0 ครั้ง แต่ขึ้นป้ายว่า "มีคนอ่านแล้ว" · รู้ว่า nil (ค่าว่าง) ทำให้โปรแกรมพังยังไง และใช้เครื่องมือสี่ชิ้นรับมือกับมันได้: &. (เรียกเฉพาะตอนมีค่า) · ||= (ใส่ค่าเริ่มต้น) · blank? กับ presence (เช็คช่องที่ผู้ใช้ปล่อยว่างในฟอร์ม)

🔁 ทวนก่อนเริ่ม — บทนี้ใช้คำจากบทก่อน ๆ ต่อไปนี้

🧰 เวอร์ชันที่ใช้ตรวจ: Ruby 4.0.9 · Active Support 8.1.4 (มากับ Rails 8.1.4) · Go 1.26.5 — สไลด์ใช้ Ruby 4.0.6 · Rails 8.1.3 เรื่องในบทนี้ไม่ต่างกัน · ผลรันทุกกล่องก็อปมาจากเครื่องจริง

1 · if ใน Ruby ถามแค่ว่า "ผ่านไหม"

if มีหน้าที่เดียว: ดูค่าที่วางไว้ข้างหลัง แล้วตัดสินว่า "ทำโค้ดข้างในไหม" แต่ละภาษาตั้งกฎการตัดสินไม่เหมือนกัน

Go เข้มที่สุด — ข้างหลัง if ต้องเป็น bool (true/false) เท่านั้น ลองเขียน if views ทั้งที่ views เป็นตัวเลข Go ไม่ยอม compile เลย:

// main.go
package main

import "fmt"

func main() {
	views := 0
	if views {
		fmt.Println("มีคนอ่านแล้ว")
	}
}
$ go run main.go
# command-line-arguments
./main.go:8:5: non-boolean condition in if statement

แปลว่า "เงื่อนไขของ if ไม่ใช่ bool" · ใน Go คุณจึงต้องเขียนเต็ม ๆ เสมอ เช่น if views > 0 · if title != "" · if err != nil — ยาว แต่ไม่มีทางเข้าใจผิด

Ruby ใจดีกว่า — ใส่อะไรไว้หลัง if ก็ได้ แล้ว Ruby ใช้กฎสั้น ๆ ข้อเดียว:

มีแค่ 2 ค่าที่ "ไม่ผ่าน": nil กับ false · ค่าอื่นทุกค่า "ผ่าน" หมด — รวมถึง 0 และสตริงว่าง "" ที่ดูเหมือนว่างเปล่าด้วย

🚪 เทียบให้เห็นภาพ — ประตูที่ห้ามแค่ 2 คน

นึกถึงประตูหน้าร้านที่มี รปภ. ยืนเฝ้า และได้รับรายชื่อ "ห้ามเข้า" มาแค่ 2 ชื่อ: nil กับ false
ใครไม่อยู่ในรายชื่อนี้ รปภ. ให้เข้าหมด ไม่สนว่าหน้าตาเป็นยังไง — เลข 0 เข้าได้ · กระดาษเปล่า "" เข้าได้ · กล่องเปล่า [] ก็เข้าได้

ที่ไม่เหมือนกัน: รปภ. จริงยังพอดูหน้าตาคนได้ แต่ Ruby ไม่ดูข้างในเลย — แม้แต่สตริง "false" หรือ "0" ที่อ่านแล้วเหมือนแปลว่า "ไม่" ก็ผ่าน เพราะมันเป็นแค่ข้อความ ไม่ใช่ค่า false ตัวจริง

มาลองให้เห็นกับตา · โค้ดข้างล่างเอาค่า 8 ตัวมาผ่านประตู if ทีละตัว:

# truth.rb
[nil, false, 0, "", " ", [], "false", "0"].each do |v|
  if v
    puts "#{v.inspect} → เข้า if"
  else
    puts "#{v.inspect} → ไม่เข้า"
  end
end

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

ก่อนกดเฉลย จดลงกระดาษ: 8 ค่านี้ ตัวไหนเข้า if บ้าง?

$ ruby truth.rb
nil → ไม่เข้า
false → ไม่เข้า
0 → เข้า if
"" → เข้า if
" " → เข้า if
[] → เข้า if
"false" → เข้า if
"0" → เข้า if

ไม่เข้าแค่ 2 บรรทัดแรก ตรงตามกฎ · ถ้าคุณทายว่า 0 หรือ "" ไม่เข้า ไม่ต้องตกใจ — ภาษาอื่นอย่าง JavaScript/TypeScript ถือว่าสองตัวนี้ "ไม่ผ่าน" จริง ๆ คนที่เคยเขียนภาษาพวกนั้นมาจึงพลาดตรงนี้บ่อยที่สุด

🗣 ศัพท์ที่เพิ่งเรียนไป
truthy / falsy
ค่าที่ "ผ่าน" if เรียกว่า truthy · ค่าที่ "ไม่ผ่าน" เรียกว่า falsy · ใน Ruby falsy มีแค่ nil กับ false · ถาม senior ได้ว่า "ทีมนี้ยอมให้เขียน if count ลอย ๆ ไหม หรือต้องเขียน count.positive? ให้ชัด"
truthiness
ชื่อเรียกของ "กฎว่าค่าไหนนับว่าจริงเมื่อเอาไปใส่ใน if" · พิมพ์บอก AI ได้ว่า "Check this Ruby code for places that assume 0 or empty string is falsy" — AI จะรู้ทันทีว่าต้องหาอะไร

2 · บั๊กที่เจอจริง: บทความที่มีคนอ่าน 0 ครั้ง

ทวนแอปตัวอย่างของคอร์สก่อน: สไลด์สร้างแอป บล็อก ชื่อ wongnok_blog · ของหลักในแอปคือ บทความ (ภาษาอังกฤษ post) · สมมติว่าบทความแต่ละชิ้นมีตัวเลข views = จำนวนครั้งที่มีคนเปิดอ่าน

เราอยากโชว์ป้าย "มีคนอ่านแล้ว" เฉพาะบทความที่มีคนอ่าน · ตอนนี้ยังไม่มี Rails เลยจำลองบทความหนึ่งชิ้นด้วย Hash ไปก่อน (ของจริงใน Rails จะเป็น object ชื่อ Post — เจอใน Day 2):

# views.rb
# จำลองบทความ 1 ชิ้นด้วย Hash (ยังไม่มี Rails)
post = { title: "ข้าวมันไก่ประตูน้ำ", views: 0 }

if post[:views]
  puts "มีคนอ่านแล้ว #{post[:views]} ครั้ง"
end
$ ruby views.rb
มีคนอ่านแล้ว 0 ครั้ง

นี่คือบั๊ก — ยังไม่มีใครอ่านเลย แต่ขึ้นป้ายว่ามีคนอ่าน · เหตุผลคือ post[:views] ได้ 0 และ 0 ผ่านประตู (มันไม่ใช่ nil ไม่ใช่ false) · ไม่มี error ไม่มีคำเตือน ทำให้บั๊กแบบนี้รอดไปถึงมือผู้ใช้ได้ง่าย

ทางแก้: ถามให้ตรงกับที่อยากรู้ · เราอยากรู้ว่า "มากกว่า 0 ไหม" ก็ใช้ method positive? ของตัวเลข (ลงท้าย ? = ตอบ true/false ตามบท 93):

# views2.rb
post = { title: "ข้าวมันไก่ประตูน้ำ", views: 0 }

if post[:views].positive?
  puts "มีคนอ่านแล้ว #{post[:views]} ครั้ง"
else
  puts "ยังไม่มีใครอ่าน"
end

p 0.positive?
p 5.positive?
p 0.zero?
$ ruby views2.rb
ยังไม่มีใครอ่าน
false
true
true

⚠️ สไลด์หน้า 26 เขียนตัวอย่างเป็น if @post.views — ถ้าพิมพ์ตามในไฟล์ .rb เปล่า ๆ จะพังทันที ด้วย undefined method 'views' for nil (NoMethodError) เพราะ @post ยังไม่มีค่า (@post คือตัวแปรที่ controller เตรียมไว้ให้หน้าเว็บ — เจอในบท 102) · ฝึกตอนนี้ให้ใช้ Hash แบบข้างบนแทน

3 · nil คือกล่องว่าง — และเรียก method ใส่กล่องว่างแล้วพัง

nil แปลว่า "ไม่มีค่า" · ใน Rails คุณจะเจอมันบ่อยมาก เช่น บทความที่ไม่มีคนใส่ชื่อผู้เขียน · หรือหยิบช่องที่ไม่มีอยู่ใน Hash:

# nil1.rb
post = { title: "ข้าวมันไก่ประตูน้ำ", author: nil }

p post[:author]       # ใส่ไว้ว่า nil
p post[:likes]        # ไม่มีช่องนี้เลย
p post[:author].nil?
p post[:title].nil?
$ ruby nil1.rb
nil
nil
true
false

ปัญหาเกิดตอนเราลืมว่าค่าอาจเป็น nil แล้วสั่งมันทำงาน · เช่น อยากได้ชื่อผู้เขียนเป็นตัวพิมพ์ใหญ่ด้วย .upcase:

# nil2.rb
post = { title: "ข้าวมันไก่ประตูน้ำ", author: nil }

puts post[:author].upcase
$ ruby nil2.rb
nil2.rb:4:in '<main>': undefined method 'upcase' for nil (NoMethodError)

puts post[:author].upcase
                  ^^^^^^^

อ่าน error นี้ให้เป็น: "บรรทัด 4 · ไม่มี method ชื่อ upcase สำหรับ nil" · ลูกศร ^^^^^^^ ชี้ตรงจุดที่พัง · แปลเป็นภาษาคนคือ "คุณสั่งกล่องว่างให้ทำงาน" นี่คือ error ที่คุณจะเห็นบ่อยที่สุดในชีวิตการเขียน Rails

เทียบ Go: คุณเคยเจอเรื่องเดียวกันแล้ว — pointer ที่เป็น nil แล้วไปอ่านช่องข้างใน Go ก็พัง (panic) เหมือนกัน · ในโค้ดข้างล่าง *Author คือ pointer (ตัวชี้) ไปหา struct Author · ถ้ายังไม่ได้ชี้ไปไหน มันเป็น nil:

// main.go
package main

import "fmt"

type Author struct {
	Name string
}

type Post struct {
	Title  string
	Author *Author
}

func main() {
	post := Post{Title: "ก๋วยเตี๋ยวเรือ"} // ไม่ได้ใส่ Author → เป็น nil

	if post.Author != nil {
		fmt.Println("ผู้เขียน:", post.Author.Name)
	} else {
		fmt.Println("ไม่มีผู้เขียน")
	}

	fmt.Println(post.Author.Name) // ไม่เช็คก่อน → พัง
}
$ go run main.go
ไม่มีผู้เขียน
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x2 addr=0x0 pc=0x10306f634]

goroutine 1 [running]:
… (ตามด้วยรายละเอียดอีกหลายบรรทัด · เลข pc=… ในเครื่องคุณจะไม่เหมือนนี้)

ช่วงแรกที่เช็ค != nil ก่อน ทำงานปกติ · บรรทัดสุดท้ายที่ไม่เช็ค พังทันที — Ruby กับ Go เหมือนกันตรงนี้ ต่างกันแค่ชื่อ error

📦 เทียบให้เห็นภาพ — กล่องว่างในห้องเก็บวัตถุดิบ

ในร้านอาหาร พ่อครัวสั่งลูกมือว่า "หยิบกล่องผักชีมา แล้วล้างผักชีในกล่อง" · ถ้ากล่องว่าง (nil) ลูกมือก็ทำต่อไม่ได้ ยืนงงแล้วหยุดงานทั้งครัว — นั่นคือ NoMethodError
.nil? คือการ "เปิดฝาดูก่อนว่ากล่องว่างไหม" ซึ่งทำได้เสมอ ไม่มีวันพัง

ที่ไม่เหมือนกัน: กล่องว่างในครัวมีหลายใบ แต่ nil ใน Ruby มีใบเดียวทั้งโปรแกรม · และมันไม่ได้ "ไม่มีอะไรเลย" จริง ๆ — มันเป็นค่าที่ตอบคำถามง่าย ๆ ได้บางข้อ เช่น nil.nil? ได้ true · สิ่งที่ทำไม่ได้คือคำสั่งของค่าชนิดอื่น เช่น upcase ของสตริง

🗣 ศัพท์ที่เพิ่งเรียนไป
nil
ค่า "ไม่มีค่า" ของ Ruby มีตัวเดียว (เทียบได้กับ nil ของ Go) · ถาม AI ได้ว่า "Where can this variable become nil?" — ได้คำตอบที่ตรงต้นเหตุกว่าถามว่า "แก้ error นี้ยังไง"
NoMethodError
error ที่เกิดเมื่อสั่ง method ที่ค่านั้นไม่มี · แบบที่เจอบ่อยสุดคือ undefined method '…' for nil · บอก senior ได้ว่า "ตัวแปรนี้เป็น nil ตอนเรียก .upcase ครับ กำลังหาว่า nil มาจากไหน"

4 · &. — "ถ้ามีค่อยเรียก"

ใน Go เราป้องกัน nil ด้วย if post.Author != nil { … } ทุกครั้ง · Ruby มีทางลัดให้ คือเขียน &. แทนจุดธรรมดา:

# safe.rb
post1 = { title: "ข้าวมันไก่ประตูน้ำ", author: "somchai" }
post2 = { title: "ก๋วยเตี๋ยวเรือ", author: nil }

p post1[:author]&.upcase
p post2[:author]&.upcase
$ ruby safe.rb
"SOMCHAI"
nil
🧑‍🍳 เทียบให้เห็นภาพ — ลูกมือที่เปิดฝาดูก่อนลงมือ

จุดธรรมดา . คือลูกมือที่ได้ยินว่า "ล้างผักชีในกล่อง" แล้วลงมือทันทีโดยไม่ดู — กล่องว่างก็ยืนงง หยุดทั้งครัว · &. คือลูกมือที่เปิดฝาดูก่อน: มีผักชีก็ล้างให้ · กล่องว่างก็วางคืนแล้วบอกว่า "ว่างครับ" (ได้ nil) งานไม่สะดุด

ที่ไม่เหมือนกัน: ลูกมือจริงอาจวิ่งไปหาผักชีมาเติม หรือบอกว่าทำไมกล่องถึงว่าง · แต่ &. ไม่เติมของให้และไม่บอกเหตุผลเลย — ส่งแค่ nil กลับมาเงียบ ๆ

วางโค้ด Go กับ Ruby ที่ทำเรื่องเดียวกันไว้ข้างกัน — "ถ้ามีผู้เขียน ค่อยเอาชื่อมาทำเป็นตัวพิมพ์ใหญ่":

Go — เช็คเองทุกครั้ง

name := ""
if post.Author != nil {
	name = strings.ToUpper(post.Author.Name)
}

Ruby — ใช้ &.

name = post[:author]&.upcase

ความคิดเดียวกัน แต่ Ruby เหลือบรรทัดเดียว · ต่างกันตรงที่ถ้าไม่มีผู้เขียน ฝั่ง Go name ยังเป็น "" ที่เราตั้งไว้ ส่วนฝั่ง Ruby name ได้ nil

ข้อควรระวัง: &. ไม่ได้ทำให้ค่าว่างหายไป แค่เลื่อนปัญหาไปข้างหน้า · ได้ nil มาแล้วถ้าบรรทัดถัดไปเอาไปใช้ต่อโดยไม่เช็ค ก็พังอยู่ดี · ถ้าค่านั้น "ไม่ควรเป็น nil ตั้งแต่แรก" ให้ไปแก้ที่ต้นทางดีกว่าโรย &. ทุกจุด

🗣 ศัพท์ที่เพิ่งเรียนไป
safe navigation operator (&.)
ชื่อทางการในเอกสาร Ruby ของ "จุดที่เปิดฝาดูก่อน" · ถาม senior ได้ว่า "ตรงนี้ควรใช้ &. หรือควรทำให้ค่าไม่มีวันเป็น nil ตั้งแต่ต้นครับ"

5 · ||= — "ถ้ายังว่างค่อยใส่"

บ่อยครั้งเราอยากใส่ค่าเริ่มต้นให้ตัวแปร แต่เฉพาะตอนที่มันยังว่างอยู่ · ใน Go คุณเขียนแบบนี้ (slice ที่ยังไม่ได้ใส่อะไรจะเป็น nil):

// main.go
package main

import "fmt"

func main() {
	var tags []string // ยังไม่ได้ใส่อะไร = nil

	if tags == nil {
		tags = []string{"ทั่วไป"}
	}

	fmt.Println(tags)
}
$ go run main.go
[ทั่วไป]

Ruby ย่อทั้งก้อน if นั้นเหลือเครื่องหมายเดียว ||=:

# orq.rb
tags = nil
tags ||= ["general"]
p tags

tags = ["food"]
tags ||= ["general"]
p tags
$ ruby orq.rb
["general"]
["food"]
🥤 เทียบให้เห็นภาพ — พนักงานเติมน้ำเฉพาะแก้วที่ว่าง

พนักงานเดินรอบร้าน ถือเหยือกน้ำ กฎคือ "แก้วไหนว่าง เติมให้ · แก้วไหนมีน้ำอยู่ ไม่ต้องยุ่ง" — นี่คือ ||= เป๊ะ ๆ

ที่ไม่เหมือนกัน: พนักงานจริงเห็นแก้วที่ลูกค้าคว่ำไว้ (แปลว่า "ไม่เอาแล้ว") ก็จะไม่เติม · แต่ ||= มองไม่ออก — ค่า false ที่ตั้งใจใส่ไว้ว่า "ไม่" ถูกนับเป็น "แก้วว่าง" แล้วถูกเติมทับ · ส่วนแก้วที่มีน้ำแข็งก้อนเดียว (0) พนักงานจริงอาจเติม แต่ ||= ไม่เติม เพราะ 0 ผ่านประตู

ลองทายสองกรณีในท่อน "ที่ไม่เหมือนกัน" ข้างบน:

# orq2.rb
views = 0
views ||= 99
p views

published = false     # ตั้งใจว่า "ยังไม่เผยแพร่"
published ||= true
p published

สองบรรทัดที่ p ออกมาคืออะไร?

$ ruby orq2.rb
0
true

views ยังเป็น 0 เพราะ 0 ผ่านประตู → ||= ถือว่ามีค่าแล้ว ไม่แตะ ✅
published กลายเป็น true — นี่คือกับดัก · เราตั้งใจใส่ false แต่ ||= เห็นว่า false ไม่ผ่านประตู เลยเขียนทับ · ในเว็บจริงหน้าตาจะเป็น "ผู้ใช้ปิดการแจ้งเตือนไว้ แต่ระบบเปิดกลับให้เองทุกครั้ง"

ทางแก้คือถามให้ตรง ๆ ว่า "ยังเป็น nil อยู่ไหม" แทน:

# orq3.rb
published = false
published = true if published.nil?
p published

draft = nil
draft = true if draft.nil?
p draft
$ ruby orq3.rb
false
true

คำสั่ง if เงื่อนไข คือการเขียน if ไว้ท้ายบรรทัด แปลว่า "ทำคำสั่งนี้ ถ้าเงื่อนไขจริง" — สั้นกว่าเขียน if … end สามบรรทัด · false.nil? ได้ false จึงไม่เขียนทับ · ส่วน nil.nil? ได้ true จึงใส่ค่าเริ่มต้นให้

ในโค้ด Rails คุณจะเจอ ||= บ่อยมากในรูป @current_user ||= … (@ คือตัวแปรที่ติดอยู่กับ object — เจอละเอียดในบท 95) · ความหมายเดิมทุกอย่าง: "ถ้ายังไม่เคยหาผู้ใช้ ค่อยไปหา · ถ้าหาไว้แล้วก็ใช้ของเดิม"

🗣 ศัพท์ที่เพิ่งเรียนไป
or-equals (||=)
ชื่อเล่นที่คนในวงการเรียก · ในเอกสาร Ruby อยู่หัวข้อ abbreviated assignment (การกำหนดค่าแบบย่อ) · ถาม senior ได้ว่า "ตัวแปรนี้เป็น false ได้ไหมครับ ถ้าได้ ||= ตรงนี้จะเขียนทับมัน" — เป็นคำถามรีวิวโค้ดที่ดีมาก
default value
ค่าเริ่มต้นที่ใส่ให้เมื่อยังไม่มีค่า · พิมพ์บอก AI ได้ว่า "Set a default only when the value is nil — it can be false, so don't use ||="

6 · blank? · present? · presence — ของที่ Rails เติมให้

ทีนี้มาดูเรื่องที่เจอทุกวันในเว็บ: ฟอร์ม · ในภาพร้านอาหาร ค่าที่ผู้ใช้กรอกมาคือ "รายละเอียดในใบสั่ง" ซึ่ง Rails เรียกว่า params (Hash ที่เก็บค่าจากผู้ใช้ — เจอละเอียดในบท 102)

จุดสำคัญ: ถ้าผู้ใช้เว้นช่องว่างไว้แล้วกดส่ง ฟอร์มส่งมาเป็นสตริงว่าง "" ไม่ใช่ nil · ถ้าเผลอเคาะ space ก็ได้ " " (ตรวจกับ Rails 8.1.4 แล้วว่าได้แบบนี้จริง)
และจากหัวข้อ 1 — ทั้งสองค่านี้ผ่านประตู if · ถ้าใช้ || ใส่ค่าเริ่มต้นจะพลาด:

# params.rb — รันด้วย ruby -ractive_support/all params.rb
# จำลอง params ด้วย Hash — ผู้ใช้เคาะ space 3 ที แล้วกดค้นหา
params = { q: "   " }

p params[:q] || "all"
p params[:q].presence || "all"
$ ruby -ractive_support/all params.rb
"   "
"all"

บรรทัดแรก || ธรรมดาได้คำค้นเป็นช่องว่างสามตัว — ค้นหาแล้วไม่เจออะไร · บรรทัดที่สองใช้ presence ได้ "all" ตามที่ตั้งใจ · มาดูว่า presence และเพื่อน ๆ คืออะไร

ข่าวร้ายก่อน: Ruby เปล่า ๆ แยก "ว่างจริง" กับ "มีแต่ช่องว่าง" ไม่ได้ · มีแค่ empty? (ยาว 0 ตัวไหม) ส่วน blank? ไม่มีเลย:

# blank_plain.rb
p "".empty?
p " ".empty?
p " ".blank?
$ ruby blank_plain.rb
true
false
blank_plain.rb:4:in '<main>': undefined method 'blank?' for an instance of String (NoMethodError)

p " ".blank?
     ^^^^^^^

blank? มาจาก Active Support — ส่วนเสริมที่ติดมากับ Rails ซึ่ง "เติม method ใหม่" ให้ของพื้นฐานของ Ruby (ตามที่เล่าในบท 90) · ในแอป Rails ใช้ได้เลยทุกไฟล์ · นอก Rails ต้องสั่งโหลดเอง ด้วย ruby -ractive_support/all ไฟล์.rb (-r = "โหลดของชิ้นนี้ก่อนรันไฟล์")

# blank_as.rb — รันด้วย ruby -ractive_support/all blank_as.rb
p "".blank?
p "   ".blank?
p nil.blank?
p [].blank?
p 0.blank?
p false.blank?
p "ramen".present?
p "   ".present?
$ ruby -ractive_support/all blank_as.rb
true
true
true
true
false
true
true
false
methodถามว่าจำแค่นี้
blank?"ว่างไหม" ในความหมายของคนทั่วไปnil · false · "" · " " · [] นับว่าว่าง · 0 ไม่ว่าง
present?"มีของจริงไหม"ตรงข้ามกับ blank? เป๊ะ
presence"ถ้ามีของจริง ส่งตัวมันมา · ถ้าว่าง ส่ง nil มา"ใช้คู่กับ || เพื่อใส่ค่าเริ่มต้น
# presence.rb — รันด้วย ruby -ractive_support/all presence.rb
p "ramen".presence
p "".presence
p "   ".presence
p nil.presence
$ ruby -ractive_support/all presence.rb
"ramen"
nil
nil
nil

ทีนี้ย้อนกลับไปอ่าน params[:q].presence || "all" จะเข้าใจแล้ว: " ".presence ได้ nil → nil ไม่ผ่านประตู → || หยิบ "all" ทางขวามาแทน

🧾 เทียบให้เห็นภาพ — พนักงานอ่านช่อง "หมายเหตุ" ในใบสั่ง

ใบสั่งอาหารมีช่อง "หมายเหตุ" · ลูกค้าบางคนเว้นว่าง ("") บางคนขีด ๆ เคาะ ๆ ไว้แต่ไม่มีตัวหนังสือ (" ") · พนักงานที่ใช้ blank? มองทั้งสองแบบว่า "ไม่ได้เขียนอะไร" เหมือนที่คนทั่วไปมอง · ส่วน presence คือพนักงานที่ "ถ้ามีหมายเหตุก็ส่งต่อให้เชฟตามนั้น · ถ้าไม่มี ก็ส่งมือเปล่า (nil) ให้เชฟใช้สูตรมาตรฐาน"

ที่ไม่เหมือนกัน: ถ้าลูกค้าเขียนว่า "พริก 0 เม็ด" คนอาจคิดว่า "ไม่มีหมายเหตุ" แต่ 0.blank? ได้ false — สำหรับ Active Support ตัวเลขไม่เคยว่าง เพราะ 0 คือค่าที่ผู้ใช้ตั้งใจกรอก · และพนักงานคนนี้ไม่ได้ "เกลาลายมือ" ให้ — " ramen ".presence ยังได้ " ramen " ที่มีช่องว่างหัวท้ายติดมา

เทียบ Go: Go ไม่มี blank? ให้ · คุณต้องตัดช่องว่างเองด้วย strings.TrimSpace แล้วเทียบกับ "" เอง — งานเดียวกับ params[:q].presence || "all":

// main.go
package main

import (
	"fmt"
	"strings"
)

func main() {
	q := "   " // ผู้ใช้เคาะ space 3 ที

	if strings.TrimSpace(q) == "" {
		q = "all"
	}

	fmt.Println(q)
}
$ go run main.go
all
🗣 ศัพท์ที่เพิ่งเรียนไป
Active Support
ส่วนหนึ่งของ Rails ที่เติม method ให้ของพื้นฐานของ Ruby (blank? · present? · presence และอีกเยอะ) · ถาม AI ได้ว่า "Is this method plain Ruby or Active Support?" ทุกครั้งที่ไม่แน่ใจ — สำคัญมากถ้าโค้ดนั้นจะไปรันนอก Rails
blank / present
blank = ว่างในความหมายของคน (nil · false · สตริงที่มีแต่ช่องว่าง · กล่องเปล่า) · present = ตรงข้าม · พิมพ์บอก AI ได้ว่า "Treat whitespace-only input as missing — use presence"
presence
คืนตัวเองถ้า present · คืน nil ถ้า blank · ถาม senior ได้ว่า "ช่องนี้ควรตัดช่องว่างหัวท้ายก่อนเรียก presence ไหมครับ"

7 · สรุป: อยากรู้อะไร ถามให้ตรง

อยากรู้ว่า…เขียนแบบนี้อย่าเขียน
ตัวเลขมากกว่า 0 ไหมviews.positive?if views (0 ก็ผ่าน)
ค่านี้ว่างเปล่า (nil) ไหมx.nil?if !x (เข้าทั้งตอน x เป็น nil และ false · แยกสองตัวนี้ไม่ออก)
เรียก method ถ้ามีค่าauthor&.upcaseauthor.upcase เมื่อ author อาจเป็น nil
ใส่ค่าเริ่มต้นให้ตัวแปรที่อาจเป็น niltags ||= [...]—
ใส่ค่าเริ่มต้นให้ค่า true/falsex = true if x.nil?x ||= true (ทับ false)
ผู้ใช้กรอกฟอร์มมาจริงไหมq.present? · q.presence || "all"if q · q || "all"

💡 สับสนว่าใช้ nil? หรือ blank?? ถามตัวเองว่า "ค่านี้มาจากไหน" — มาจากฟอร์มที่ผู้ใช้พิมพ์ ใช้ blank? / presence · มาจากที่อื่นที่อาจไม่มีค่า ใช้ nil? / &.

8 · ภารกิจ: แก้บั๊ก 2 จุด (~10 นาที)

🎯 ภารกิจ

  1. สร้างไฟล์ task.rb แล้ววางโค้ดนี้ทั้งก้อน · มีบั๊ก 2 จุด ทั้งคู่เป็นเรื่องในบทนี้
    # task.rb — มีบั๊ก 2 จุด
    
    def views_label(views)
      if views
        "มีคนอ่าน #{views} ครั้ง"
      else
        "ยังไม่มีใครอ่าน"
      end
    end
    
    def title_or_default(title)
      title || "untitled"
    end
    
    puts views_label(12)        # ควรได้: มีคนอ่าน 12 ครั้ง
    puts views_label(0)         # ควรได้: ยังไม่มีใครอ่าน
    p title_or_default("Ramen") # ควรได้: "Ramen"
    p title_or_default("")      # ควรได้: "untitled"
    p title_or_default("   ")   # ควรได้: "untitled"
    p title_or_default(nil)     # ควรได้: "untitled"
  2. รัน ruby task.rb ก่อนแก้ · ดูว่าบรรทัดไหนไม่ตรงกับ comment "ควรได้" แล้วอธิบายให้ตัวเองฟังว่าเพราะอะไร
  3. แก้ method ละ 1 บรรทัด
  4. รันใหม่ด้วย ruby -ractive_support/all task.rb จนทุกบรรทัดตรงกับ comment

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

ก่อนแก้ — ผลรันจริง:

$ ruby task.rb
มีคนอ่าน 12 ครั้ง
มีคนอ่าน 0 ครั้ง
"Ramen"
""
"   "
"untitled"

บรรทัด 2 ผิดเพราะ 0 ผ่านประตู · บรรทัด 4–5 ผิดเพราะ "" กับ " " ผ่านประตู || เลยไม่หยิบ "untitled" · มีแค่ nil บรรทัดสุดท้ายที่ได้ค่าเริ่มต้น

แก้สองบรรทัด:

def views_label(views)
  if views.positive?              # ← ถามให้ตรงว่า "มากกว่า 0 ไหม"
    "มีคนอ่าน #{views} ครั้ง"
  else
    "ยังไม่มีใครอ่าน"
  end
end

def title_or_default(title)
  title.presence || "untitled"    # ← "" กับ "   " กลายเป็น nil ก่อน
end
$ ruby -ractive_support/all task.rb
มีคนอ่าน 12 ครั้ง
ยังไม่มีใครอ่าน
"Ramen"
"untitled"
"untitled"
"untitled"

nil.presence ก็ได้ nil (Active Support เติม presence ให้ nil ด้วย) บรรทัดสุดท้ายจึงยังถูก · ถ้าลองรันเฉลยนี้ด้วย ruby task.rb เฉย ๆ จะเห็น error presence ตัวแรกในรายการ "ถ้าเจอ error" ข้างบน

ลองตอบดู

บทความมี views เป็น 0 แต่ if post[:views] ขึ้นป้าย "มีคนอ่านแล้ว 0 ครั้ง" — แก้แบบไหนถึงถูก?

ตัวเลือกที่ 2 ถูก · 0.positive? ได้ false จึงไม่ขึ้นป้าย · ตัวเลือกที่ 1 กับ 4 ผิดเพราะ 0 ไม่ใช่ nil — รันจริงทั้งคู่ได้ true ป้ายยังขึ้นเหมือนเดิม · ตัวเลือกที่ 3 ผิดเพราะตัวเลขไม่เคย blank — 0.present? รันจริงได้ true

จุดที่ต้องจำ: อยากรู้เรื่องตัวเลข ให้ถามด้วย method ของตัวเลข (positive? · zero?) อย่าโยนตัวเลขเข้า if ลอย ๆ

ใน Ruby ค่ากลุ่มไหน "ไม่ผ่าน" if ครบทุกตัว และไม่มีตัวอื่นอีกแล้ว?

ตัวเลือกที่ 1 ถูก · ผลรัน truth.rb ในหัวข้อ 1 มีแค่ nil กับ false ที่ "ไม่เข้า" · ตัวเลือกที่ 2 กับ 3 เป็นกฎของ JavaScript/TypeScript ที่ถือว่า 0 และ "" เป็นเท็จ — ใน Ruby สองตัวนี้ผ่าน · ตัวเลือกที่ 4 ยังเพิ่ม [] (กล่องเปล่า) เข้ามาอีก ซึ่งรันจริงก็ "เข้า if" เหมือนกัน

จุดที่ต้องจำ: ประตูของ Ruby ห้ามแค่ 2 คน — nil กับ false

โค้ด published = false แล้วบรรทัดถัดไป published ||= true · สุดท้าย published เป็นอะไร?

ตัวเลือกที่ 2 ถูก · รันจริง (orq2.rb) ได้ true · ตัวเลือกที่ 1 คือสิ่งที่คนส่วนใหญ่คาด แต่ ||= ไม่ได้ดูว่า "มีค่าไหม" มันดูว่า "ผ่านประตูไหม" และ false ไม่ผ่าน · ตัวเลือกที่ 3 ผิดเพราะ ||= ไม่เคยใส่ nil ให้ใคร · ตัวเลือกที่ 4 ผิดเพราะ ||= ไม่ใช่ method ของค่า แต่เป็นไวยากรณ์ของภาษา ใช้กับค่าไหนก็ได้

จุดที่ต้องจำ: ค่า true/false ห้ามใส่ค่าเริ่มต้นด้วย ||= · ใช้ x = true if x.nil?

ผู้ใช้เคาะ space 3 ทีในช่องค้นหาแล้วกดค้น · params[:q] ได้ " " · อยากให้ได้ "all" แทน ต้องเขียนแบบไหน? (strip = method ของสตริงที่ตัดช่องว่างหัวท้ายทิ้ง)

ตัวเลือกที่ 3 ถูก · " ".presence ได้ nil แล้ว || หยิบ "all" มาแทน (รันจริงได้ "all") · ตัวเลือกที่ 1 กับ 2 ได้ " " เหมือนเดิม เพราะสตริงที่มีช่องว่างผ่านประตู · ตัวเลือกที่ 4 ใกล้แต่พลาด — strip ตัดช่องว่างทิ้งเหลือ "" ซึ่งก็ยังผ่านประตู รันจริงได้ ""

จุดที่ต้องจำ: ค่าจากฟอร์มที่ว่างมาเป็น "" ไม่ใช่ nil · ใส่ค่าเริ่มต้นด้วย .presence || …

ถ้าเปิดสไลด์ดูประกอบ

หน้าสไลด์เขียนของจริงต้องสนใจไหม
26if @post.views # จริงเสมอ จริงเสมอยกเว้นตอน views เป็น nil · และพิมพ์ตามในไฟล์เปล่า ๆ จะพังเพราะ @post ยังไม่มีค่า สนใจ — ฝึกด้วย Hash แบบหัวข้อ 2
26TypeScript "falsy 6 ตัว" ลิสต์มาแค่ 5 ตัว (ขาด false) · ของจริงมีมากกว่า 6 ไม่ต้อง — บทนี้เทียบกับ Go
27// Go: if x == 0 { x = 0 } บรรทัดนี้ไม่เปลี่ยนอะไรเลย (เป็น 0 ก็ตั้งเป็น 0) · ตัวอย่าง Go ที่ใกล้ ||= จริงอยู่ในหัวข้อ 5 ไม่ต้อง
27||= เทียบกับ x ??= 0 ของ TS เหมือนกันเกือบหมด ยกเว้นกรณี false — TS ??= ไม่ทับ แต่ Ruby ||= ทับ สนใจถ้าเคยเขียน TS
27@current_user ||= User.find_by(...) ยังไม่ต้องเข้าใจทั้งบรรทัด (@ = บท 95 · User.find_by = Day 2) · จำแค่ว่าเป็น ||= แบบเดียวกับหัวข้อ 5 ไม่ต้องตอนนี้

อ้างอิง