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

เก็บของหลายชิ้น — Array, Hash, Symbol และ map/select

~20 นาที · ควรผ่าน บทเรียน 91 (ตัวแปร · object · string) มาก่อน

บทที่แล้วตัวแปรหนึ่งตัวเก็บของได้ชิ้นเดียว เช่นข้อความหนึ่งข้อความ แต่แอปบล็อกมีบทความเป็นสิบเป็นร้อยชิ้น และบทความหนึ่งชิ้นก็มีหลายช่อง (ชื่อเรื่อง · เผยแพร่หรือยัง · ยอดอ่าน)
อ่านบทนี้จบ คุณจะเก็บบทความหลายชิ้นไว้ในตัวแปรเดียวได้ แล้วสั่ง Ruby ว่า "เอาเฉพาะชิ้นที่เผยแพร่แล้ว แล้วขอแค่ชื่อเรื่อง" ได้ในบรรทัดเดียว โดยไม่ต้องเขียน for loop
ระหว่างทางจะได้รู้จักเครื่องหมายแปลก ๆ ที่เจอในโค้ด Rails ทุกไฟล์ด้วย: :title ที่มีโคลอนนำหน้า และ { |post| … } ที่มีขีดตั้งสองขีด

🔁 ทวนก่อนเริ่ม (จากบท 91)

บทนี้ใช้คำสั่ง p พิมพ์ค่าออกจอทุกตัวอย่าง (p คือคำสั่งพิมพ์ที่โชว์หน้าตาจริงของค่า เช่น string จะมีเครื่องหมายคำพูดติดมาด้วย — เทียบกับ puts ละเอียดในบท 93)

🧰 ใช้ตรวจ: ruby 4.0.9 (สไลด์ใช้ 4.0.6 — เรื่องในบทนี้เหมือนกันทุกตัว) · ทุกกล่อง "ผลรันจริง" มาจากการรัน ruby ชื่อไฟล์.rb บนเครื่องจริงแล้วก็อปมาทั้งก้อน

1 · Array — รายการของที่เรียงเป็นแถว

Array คือตัวแปรหนึ่งตัวที่เก็บของได้หลายชิ้น เรียงตามลำดับ ใช้เมื่อมีของชนิดเดียวกันหลายชิ้น เช่น คะแนนทุกคน หรือบทความทุกชิ้น
ถ้าไม่มี Array คุณต้องตั้งตัวแปร score1 score2 score3 ไปเรื่อย ๆ ซึ่งไม่มีใครทำ

🍽 เทียบให้เห็นภาพ — ชั้นวางจานที่มีช่องเรียงเป็นแถว

หลังครัวมีชั้นวางจานยาว ๆ แบ่งเป็นช่องเรียงกัน · เชฟพูดว่า "ช่องที่ 0" "ช่องที่ 1" แล้วรู้ทันทีว่าหมายถึงจานไหน · มีจานใหม่มาก็วางต่อท้ายแถว
Array ก็คือชั้นวางนั้น: ของแต่ละชิ้นมีเลขช่องประจำตัว และเพิ่มต่อท้ายได้เรื่อย ๆ

ที่ไม่เหมือนกัน: ชั้นวางจริงยาวจำกัด แต่ Array ยาวเท่าไหร่ก็ได้ · คนนับช่องแรกว่า "ช่องที่ 1" แต่ Array นับช่องแรกเป็น 0 · และถ้าขอช่องที่ไม่มีอยู่ ชั้นวางจริงคุณเห็นเองว่าว่าง ส่วน Ruby จะตอบกลับมาเงียบ ๆ ว่า nil (ไม่มีค่า) ไม่ฟ้องอะไร

ดูของจริง — สร้าง Array ของคะแนนสามตัว แล้วลองหยิบ ลองเพิ่ม:

# array.rb — รายการตัวเลข
scores = [10, 20, 30]
p scores[0]
p scores[2]
p scores[-1]
scores << 40
p scores
p scores.length
p scores[10]

ผลรันจริง (ruby array.rb):

10
30
30
[10, 20, 30, 40]
4
nil

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

โค้ดแปลว่า
[10, 20, 30]วงเล็บเหลี่ยม = สร้าง Array · ของแต่ละชิ้นคั่นด้วยจุลภาค
scores[0]หยิบของช่องที่ 0 (ช่องแรก) → 10
scores[-1]เลขติดลบ = นับจากท้าย · -1 คือชิ้นสุดท้าย → 30 (Go ทำแบบนี้ไม่ได้)
scores << 40<< อ่านว่า "ยัดต่อท้าย" — เพิ่ม 40 เข้าไปท้ายแถว
scores.lengthถามว่ามีกี่ชิ้น → 4
scores[10]ขอช่องที่ไม่มี → ได้ nil ไม่ใช่ error

เทียบกับ Go ที่เคยเขียน — Array ของ Ruby คือ slice ของ Go นั่นเอง:

Go — slice

scores := []int{10, 20, 30}
scores = append(scores, 40)
fmt.Println(len(scores))

ต้องบอกชนิด []int · ใส่ได้แต่ตัวเลข

Ruby — Array

scores = [10, 20, 30]
scores << 40
p scores.length

ไม่ต้องบอกชนิด · ความยาวถามจาก Array เองด้วยจุด

⚠️ ต่างจาก Go จุดเดียวที่ต้องระวัง: ใน Go ถ้าขอ scores[10] จากของ 3 ชิ้น โปรแกรมพังทันที (ผมรันจริงได้ panic: runtime error: index out of range [10] with length 3) · แต่ Ruby ส่ง nil กลับมาเงียบ ๆ แล้วเดินต่อ — ถ้าเห็น nil โผล่มาแบบงง ๆ ให้สงสัยเลขช่องก่อน

🗣 ศัพท์ที่เพิ่งเรียนไป
Array · element · index
Array คือรายการ · ของแต่ละชิ้นเรียกว่า element · เลขช่องเรียกว่า index (เริ่มที่ 0) · ถาม AI ได้ว่า "Ruby Array กับ Go slice ต่างกันตรงไหนบ้าง"
<< (shovel operator)
คนเขียน Ruby เรียกเครื่องหมายนี้ว่า "พลั่ว" เพราะมันตักของใส่ท้ายแถว · ทำหน้าที่เหมือน append ของ Go · พิมพ์บอก AI ได้ว่า "use the shovel operator to append"

2 · Hash — แฟ้มที่มีป้ายชื่อทุกช่อง

Array หยิบของด้วยเลขช่อง · แต่บางทีของเรามีหลายช่องที่แต่ละช่องมีความหมายต่างกัน เช่น บทความหนึ่งชิ้นมี "ชื่อเรื่อง" กับ "เผยแพร่หรือยัง" — เรียกว่าช่อง 0 ช่อง 1 คงไม่มีใครจำได้
Hash คือที่เก็บของแบบคู่ชื่อ–ค่า หยิบของด้วยชื่อช่อง ไม่ใช่เลขช่อง

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

เรื่องชื่อที่จะเห็นตลอดบท: ตัวแปร post (ตัวเล็ก ไม่มี s) = บทความหนึ่งชิ้น · posts (มี s) = บทความหลายชิ้น · เป็นแค่ชื่อตัวแปรที่เราตั้งเอง ไม่ใช่คำสั่งพิเศษ
ส่วน Post ตัวใหญ่ที่เห็นในสไลด์คือ "แม่พิมพ์ของบทความ" ในแอป Rails จริง — บทนี้ยังไม่ใช้ (สร้างแม่พิมพ์เองในบท 95 · กฎตั้งชื่อในบท 99)

📁 เทียบให้เห็นภาพ — แฟ้มบทความหนึ่งแผ่นในร้านของเรา

จำภาพร้านอาหารจากบท 90 ได้ไหม — ข้อมูลของร้านเก็บอยู่ในตู้เก็บแฟ้ม มีลิ้นชักบทความ และในลิ้นชักมีแฟ้มทีละแผ่น
แฟ้มหนึ่งแผ่นมีหัวช่องพิมพ์ไว้ "ชื่อเรื่อง: ______" "เผยแพร่: ______" แล้วเรากรอกค่าลงไป · Hash หนึ่งก้อนก็คือแฟ้มหนึ่งแผ่นแบบนั้น: หัวช่อง = key · ที่กรอก = value

ที่ไม่เหมือนกัน: แฟ้มในตู้จริงอยู่ถาวร แต่ Hash อยู่ในหน่วยความจำ ปิดโปรแกรมแล้วหายหมด · การเก็บลงตู้จริง (database) เป็นเรื่องของ Day 2 · อีกข้อ: แฟ้มกระดาษมีหัวช่องตายตัว แต่ Hash เติมช่องใหม่ได้ทุกเมื่อ

ดูของจริง — แฟ้มบทความหนึ่งแผ่น (จำลองด้วย Hash):

# hash.rb — จำลองบทความ 1 ชิ้นด้วย Hash (ยังไม่มี Rails)
post = { title: "ต้มยำกุ้ง", published: true }
p post
p post[:title]
p post[:published]
post[:views] = 120
p post
p post[:author]
p post.fetch(:author, "ไม่ระบุ")
p post.fetch(:author)

ผลรันจริง (ruby hash.rb):

{title: "ต้มยำกุ้ง", published: true}
"ต้มยำกุ้ง"
true
{title: "ต้มยำกุ้ง", published: true, views: 120}
nil
"ไม่ระบุ"
hash.rb:10:in 'Hash#fetch': key not found: :author (KeyError)
	from hash.rb:10:in '<main>'

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

โค้ดแปลว่า
{ title: "ต้มยำกุ้ง", published: true } ปีกกา = สร้าง Hash · มีสองช่อง: ช่อง title ใส่ข้อความ · ช่อง published ใส่ true (จริง) · โคลอนท้ายชื่อ (title:) แปลว่า "ช่องนี้ชื่อ title" — หัวข้อถัดไปจะอธิบายว่าทำไมเขียนแบบนี้
post[:title]หยิบของจากช่องชื่อ title · สังเกตว่าตอนหยิบ โคลอนย้ายมาอยู่หน้าชื่อ
post[:views] = 120เติมช่องใหม่ชื่อ views ใส่ 120 · ช่องใหม่ไปต่อท้าย
post[:author]ขอช่องที่ไม่มี → nil เงียบ ๆ
post.fetch(:author, "ไม่ระบุ")fetch = หยิบแบบ "ถ้าไม่มี ให้ใช้ค่านี้แทน"
post.fetch(:author)fetch โดยไม่ให้ค่าสำรอง → ไม่มีช่องนั้น = พังดัง ๆ ด้วย KeyError และบอกชื่อช่องที่หาไม่เจอ

อ่านข้อความ error สองบรรทัดสุดท้ายเป็นภาษาคน:

เทียบกับ Go — Hash ของ Ruby คือ map ของ Go:

Go — map

m := map[string]int{"a": 1}
v, ok := m["b"]
fmt.Println(v, ok)
// ผลรันจริง: 0 false

ไม่มี key → ได้ค่าศูนย์ของชนิดนั้น + ok เป็น false บอกให้รู้

Ruby — Hash

m = { a: 1 }
p m[:b]           # nil
p m.fetch(:b, 0)  # 0

ไม่มี ok ให้ · [] ได้ nil · อยากได้ค่าสำรองหรืออยากให้พังชัด ๆ ใช้ fetch

ระวังตาลาย: Go เขียน "a": 1 (key เป็นข้อความในเครื่องหมายคำพูด) · Ruby เขียน a: 1 ไม่มีเครื่องหมายคำพูด — หน้าตาคล้ายกันแต่ key ของ Ruby ตรงนี้ไม่ใช่ข้อความ หัวข้อถัดไปจะบอกว่ามันคืออะไร

แล้วทำไมไม่ใช้ struct แบบ Go? ใน Go คุณคงทำบทความเป็น type Post struct { Title string; Published bool } · Ruby ก็มีแม่พิมพ์แบบนั้น (class ในบท 95) แต่ Hash ไม่ต้องประกาศอะไรก่อน เลยเหมาะกับการลองเล่นตอนนี้ · ข้อเสียคือ struct ของ Go ถ้าพิมพ์ชื่อช่องผิด compile ไม่ผ่าน แต่ Hash พิมพ์ชื่อช่องผิดได้ nil เงียบ ๆ

🗣 ศัพท์ที่เพิ่งเรียนไป
Hash · key · value
Hash คือที่เก็บคู่ชื่อ–ค่า · ชื่อช่องเรียก key · ของในช่องเรียก value · ภาษาอื่นเรียกของแบบนี้ว่า map หรือ dictionary · ถาม AI ได้ว่า "Ruby Hash เทียบกับ Go map ต่างกันตอนหา key ไม่เจอยังไง"
fetch · KeyError
fetch คือการหยิบที่ "ไม่ยอมได้ nil เงียบ ๆ" · ถ้าไม่ให้ค่าสำรอง จะได้ KeyError · ใน code review พูดได้ว่า "ช่องนี้ต้องมีเสมอ ใช้ fetch ดีกว่า จะได้พังตรงจุด"

3 · Symbol — :title คือป้ายชื่อ ไม่ใช่ข้อความ

ตัวที่ขึ้นต้นด้วยโคลอนอย่าง :title เรียกว่า Symbol · มันคือชื่อเรียกของสิ่งหนึ่งในโปรแกรม ไม่ใช่ข้อความที่จะเอาไปโชว์ผู้ใช้
Ruby มีของสองชนิดที่หน้าตาคล้ายกัน: "title" (string = ข้อความ) กับ :title (symbol = ป้ายชื่อ) และ Ruby ถือว่าเป็นคนละอย่างกัน

🏷 เทียบให้เห็นภาพ — ป้ายชื่อลิ้นชัก กับ ข้อความในจดหมาย

ป้ายบนลิ้นชักเขียนว่า "บทความ" — มีไว้บอกว่าลิ้นชักนี้คืออะไร ไม่มีใครแก้ป้ายเล่น ๆ ทุกวัน
ส่วนจดหมายในลิ้นชักมีข้อความ "บทความวันนี้เรื่องต้มยำกุ้ง" — เป็นเนื้อหา อ่านได้ แก้ได้ เอาไปให้คนอื่นอ่านได้
Symbol คือป้าย · String คือข้อความในจดหมาย · Hash จึงใช้ Symbol เป็นหัวช่อง เพราะหัวช่องคือ "ชื่อเรียก" ไม่ใช่เนื้อหา

ที่ไม่เหมือนกัน: คนเห็นป้าย "บทความ" กับจดหมายที่เขียนว่า "บทความ" ก็รู้ว่าคำเดียวกัน แต่ Ruby ไม่รู้ — :title กับ "title" สำหรับ Ruby คือของคนละชิ้น หยิบด้วยตัวหนึ่ง จะไม่เจอของที่เก็บด้วยอีกตัว (หัวข้อนี้จะรันให้ดู) · และป้ายแปลงเป็นข้อความได้ถ้าจำเป็น (:title.to_s ได้ "title" · to_s ย่อมาจาก "to string" = แปลงเป็นข้อความ)

ใน Go ไม่มีของแบบนี้ · ที่ใกล้ที่สุดคือการตั้งค่าคงที่เป็น string ไว้ใช้เป็นชื่อ เช่น const StatusActive = "active" · Ruby มีชนิดข้อมูลแยกไว้ให้เลย ไม่ต้องประกาศที่ไหนก่อน เขียน :active ได้ทันที

ทีนี้ไขปริศนาจากหัวข้อที่แล้ว — ทำไมตอนสร้างโคลอนอยู่ท้าย แต่ตอนหยิบโคลอนอยู่หน้า?

# rocket.rb — เขียน Hash สามแบบ
a = { title: "ต้มยำกุ้ง" }        # แบบสั้น (เจอบ่อยที่สุด)
b = { :title => "ต้มยำกุ้ง" }    # แบบเต็ม
c = { "title" => "ต้มยำกุ้ง" }   # key เป็น string
p a
p b
p c
p a == b
p a == c

ผลรันจริง (ruby rocket.rb):

{title: "ต้มยำกุ้ง"}
{title: "ต้มยำกุ้ง"}
{"title" => "ต้มยำกุ้ง"}
true
false

สรุปเป็นภาษาคน: ตอนสร้าง title: กับตอนหยิบ [:title] คือ Symbol ตัวเดียวกัน แค่วิธีเขียนต่างกัน

post = { title: "ต้มยำกุ้ง" }
p post[:title]
p post["title"]

บรรทัดล่าง หยิบด้วย "title" (string) — คิดว่าได้อะไร? ได้ "ต้มยำกุ้ง" · ได้ nil · หรือพัง?

ผลรันจริง:

"ต้มยำกุ้ง"
nil

ได้ nil เงียบ ๆ ไม่มี error — เพราะของถูกเก็บไว้ใต้ป้าย :title (symbol) แต่เราไปหาลิ้นชักที่ติดป้าย "title" (string) ซึ่งไม่มี · นี่คือ bug ที่มือใหม่เจอบ่อยที่สุดในบทนี้ เพราะตาคนเห็นว่าเป็นคำเดียวกัน

หมายเหตุสั้น ๆ: params ของ Rails (ค่าที่ผู้ใช้ส่งมากับหน้าเว็บ เดี๋ยวเจอละเอียดในบท 102) เป็นข้อยกเว้น — Rails ทำให้มันหยิบได้ทั้ง [:title] และ ["title"] (ผมลองใน Rails 8.1.4 แล้วได้ค่าเดียวกันทั้งสองแบบ) · แต่ Hash ธรรมดาที่คุณสร้างเองไม่ใช่

ใน Rails คุณจะเห็น Symbol แทบทุกบรรทัด เช่นบรรทัดนี้ที่สไลด์ยกมา (จะได้เขียนจริงใน Day 2):

validates :title, presence: true

ตอนนี้ยังไม่ต้องเข้าใจว่ามันทำอะไร แค่ให้อ่านออกว่า validates คือชื่อ method ตัวหนึ่งของ Rails · :title คือ "ป้ายชื่อของช่อง title" และ presence: true คือ Hash แบบสั้นที่มีช่องเดียว (ทำไมไม่มีวงเล็บและไม่มีปีกกา อธิบายในบท 97)

🗣 ศัพท์ที่เพิ่งเรียนไป
Symbol
ค่าที่ขึ้นต้นด้วยโคลอน เช่น :title · ใช้เป็นชื่อเรียก (ชื่อช่อง ชื่อ option) ไม่ใช่ข้อความโชว์ผู้ใช้ · ถาม AI ได้ว่า "เมื่อไหร่ควรใช้ Symbol เมื่อไหร่ควรใช้ String ใน Ruby"
symbol key · string key
Hash ที่ key เป็น :title กับ "title" หยิบข้ามกันไม่ได้ · เวลาเจอ nil แปลก ๆ บอก senior ได้ว่า "สงสัยว่า key เป็น string แต่ผมหยิบด้วย symbol" — ประโยคนี้แก้ bug ได้บ่อยมาก
=> (hash rocket)
ลูกศรจับคู่ key กับ value แบบเต็ม · title: "x" คือแบบย่อของ :title => "x" · จะเห็นแบบเต็มตอนที่ key ไม่ใช่ symbol เช่น "title" => "x"

4 · map · select · find · sum · sort — สายพานแทน for loop

ใน Go ถ้าอยากทำอะไรกับของทุกชิ้นใน slice คุณต้องเขียน for เดินหยิบทีละชิ้นเอง · Ruby ให้คุณบอกแค่ว่าอยากได้อะไร แล้ว Array เดินให้เอง ผ่าน method อย่าง map และ select
คนเขียน Ruby แทบไม่เขียน for เลย

🏭 เทียบให้เห็นภาพ — สายพานในครัว

นึกถึงสายพานลำเลียงในครัวใหญ่ ของไหลมาทีละชิ้น แล้วผ่าน "สถานี" ต่าง ๆ:

ที่ไม่เหมือนกัน: สายพานจริง ของชิ้นเดิมถูกราดซอสไปแล้วจริง ๆ · แต่ map กับ select ไม่แตะ Array เดิม — มันสร้าง Array ใหม่ให้ ของเดิมยังอยู่ครบ (ผลรันข้างล่างบรรทัดสุดท้ายพิสูจน์ให้ดู)

ก่อนดูโค้ด ต้องรู้จักของใหม่หนึ่งอย่าง: block คือก้อนโค้ดในปีกกาที่ส่งให้ method เอาไปใช้กับของทีละชิ้น

nums.map { |n| n * 2 }

ตอนนี้รู้เท่านี้พอ · block ละเอียดกว่านี้ (เขียนหลายบรรทัด · do … end) อยู่ในบท 93

ดูของจริงกับตัวเลขห้าตัว (ชุดเดียวกับสไลด์หน้า 21):

# belt.rb — สายพานของตัวเลข
nums = [3, 1, 4, 1, 5]
p nums.map { |n| n * 2 }
p nums.select { |n| n > 2 }
p nums.reject { |n| n > 2 }
p nums.find { |n| n > 2 }
p nums.find { |n| n > 100 }
p nums.select { |n| n > 100 }
p nums.sum
p nums.sort
p nums.min
p nums.max
p nums

ผลรันจริง (ruby belt.rb):

[6, 2, 8, 2, 10]
[3, 4, 5]
[1, 1]
3
nil
[]
14
[1, 1, 3, 4, 5]
1
5
[3, 1, 4, 1, 5]
โค้ดภาษาคนได้
map { |n| n * 2 }ทุกชิ้น เอาไปคูณ 2Array ใหม่ ยาวเท่าเดิม
select { |n| n > 2 }เก็บเฉพาะชิ้นที่มากกว่า 2Array ใหม่ (สั้นลง)
reject { |n| n > 2 }กลับด้านของ select — ทิ้งชิ้นที่มากกว่า 2Array ใหม่
find { |n| n > 2 }ชิ้นแรกที่มากกว่า 2ของ 1 ชิ้น (3)
find ที่ไม่เจอไม่มีชิ้นไหนเกิน 100nil
select ที่ไม่เจอไม่มีชิ้นไหนเกิน 100[] (Array ว่าง ไม่ใช่ nil)
sum · sort · min · maxรวม · เรียง · น้อยสุด · มากสุดไม่ต้องมี block
p nums บรรทัดสุดท้ายของเดิมยังอยู่ครบ ไม่ถูกแก้[3, 1, 4, 1, 5]

เทียบกับ Go — "คูณ 2 ทุกตัว" แบบเดินเองกับแบบสายพาน:

Go — เดินหยิบเองทีละชิ้น

out := []int{}
for _, n := range nums {
    out = append(out, n*2)
}

ต้องเตรียมถังเปล่า · เดินเอง · ใส่เอง

Ruby — บอกแค่ว่าทำอะไรกับแต่ละชิ้น

out = nums.map { |n| n * 2 }

ถังใหม่กับการเดิน map จัดให้

⚠️ สไลด์หน้า 21 เขียน nums.min / nums.max และ nums.sort / nums.uniq — เครื่องหมาย / ตรงนั้นสไลด์ตั้งใจให้แปลว่า "และ" · แต่ใน Ruby / คือการหาร: พิมพ์บรรทัดแรกตามได้ 0 (1 หาร 5 แบบจำนวนเต็ม) ส่วนบรรทัดที่สองพัง undefined method '/' for an instance of Array (NoMethodError) · ให้แยกพิมพ์ทีละบรรทัด

🗣 ศัพท์ที่เพิ่งเรียนไป
block
ก้อนโค้ดใน { |n| … } ที่ส่งให้ method เอาไปใช้กับของทีละชิ้น · |n| เรียกว่า block parameter · ถาม AI ได้ว่า "อธิบาย block ใน Ruby โดยเทียบกับ func ที่ส่งเป็น argument ใน Go"
Enumerable · map / select / find
ชุด method สำหรับเดินผ่านของหลายชิ้น เรียกรวมว่า Enumerable · select คือตัวเดียวกับ filter ใน TypeScript · ใน code review ได้ยินบ่อยว่า "loop นี้เปลี่ยนเป็น map/select ได้ไหม"

5 · รวมร่าง — เอาเฉพาะบทความที่เผยแพร่แล้ว แล้วขอแค่ชื่อเรื่อง

ทีนี้เอาสามเรื่องมาต่อกัน: Array ที่ข้างในเป็น Hash (บทความหลายชิ้น) แล้วใช้สายพานคัดและแปลง · ในร้านของเรา นี่ก็คือ "ลิ้นชักบทความ" ที่มีแฟ้มหลายแผ่น — ยังเป็นของจำลอง ของจริงจะอยู่ใน database ตอน Day 2

# posts.rb — จำลองบทความหลายชิ้น (ยังไม่มี Rails: ใช้ Array ของ Hash)
posts = [
  { title: "ต้มยำกุ้ง",  published: true,  views: 120 },
  { title: "ส้มตำ",      published: false, views: 40 },
  { title: "เที่ยวน่าน", published: true,  views: 300 },
]

published_posts = posts.select { |post| post[:published] }
titles = published_posts.map { |post| post[:title] }
p titles

p posts.select { |post| post[:published] }.map { |post| post[:title] }
p posts.sum { |post| post[:views] }
p posts.sort_by { |post| post[:views] }.map { |post| post[:title] }
p posts.find { |post| post[:views] > 100 }

ผลรันจริง (ruby posts.rb):

["ต้มยำกุ้ง", "เที่ยวน่าน"]
["ต้มยำกุ้ง", "เที่ยวน่าน"]
460
["ส้มตำ", "ต้มยำกุ้ง", "เที่ยวน่าน"]
{title: "ต้มยำกุ้ง", published: true, views: 120}

เทียบกับ Go ที่ทำงานเดียวกัน (สไลด์หน้า 20 วางไว้ให้เทียบ) · ฝั่ง Go บทความเป็น struct ที่ประกาศไว้ก่อน · ผมรันโค้ด Go นี้จริง (ใส่ package main กับข้อมูลสามชิ้นแบบเดียวกัน) ได้ [ต้มยำกุ้ง เที่ยวน่าน]:

Go — struct + for + if + append

type Post struct {
    Title     string
    Published bool
}
// posts คือ []Post ที่ใส่ข้อมูลไว้แล้ว

titles := []string{}
for _, p := range posts {
    if p.Published {
        titles = append(titles, p.Title)
    }
}

Ruby — สองสถานีต่อกัน

titles = posts
  .select { |post| post[:published] }
  .map    { |post| post[:title] }

ฝั่ง Ruby ขึ้นบรรทัดใหม่แล้วเริ่มด้วยจุดได้ — Ruby รู้ว่าเป็นการต่อจากบรรทัดบน · อ่านจากบนลงล่างได้ตรงกับประโยคภาษาคน: "บทความทั้งหมด → เลือกที่เผยแพร่แล้ว → เอาแต่ชื่อ"

6 · &:upcase — ทางลัดเมื่อ block แค่ "เรียก method ตัวเดียว"

ในโค้ด Rails คุณจะเห็นท่านี้บ่อยมาก: .map(&:title) · มันคือทางลัดของ block ที่ทำแค่อย่างเดียว คือ "เรียก method ชื่อนี้กับทุกชิ้น"

# amp.rb — &:ชื่อmethod เป็นทางลัด
names = ["som", "nok", "ploy"]
p names.map { |name| name.upcase }
p names.map(&:upcase)
p names.map(&:length)

ผลรันจริง (ruby amp.rb):

["SOM", "NOK", "PLOY"]
["SOM", "NOK", "PLOY"]
[3, 3, 4]

ฝั่ง Go ไม่มีทางลัดแบบนี้ · ทุกครั้งต้องเขียน for _, n := range … เต็ม ๆ เหมือนตัวอย่างในหัวข้อ 4

posts = [
  { title: "ต้มยำกุ้ง", published: true },
  { title: "ส้มตำ",     published: false },
]
p posts.map { |post| post[:title] }
p posts.map(&:title)

บรรทัดแรกได้ชื่อสองชื่อแน่ ๆ · แล้วบรรทัดล่างที่ใช้ทางลัด &:title ได้ผลเดียวกันไหม?

ผลรันจริง (ผมบันทึกโค้ดนี้เป็นไฟล์ amp_hash.rb แล้วสั่ง ruby amp_hash.rb):

["ต้มยำกุ้ง", "ส้มตำ"]
amp_hash.rb:6:in 'Array#map': undefined method 'title' for an instance of Hash (NoMethodError)

p posts.map(&:title)
       ^^^^
	from amp_hash.rb:6:in '<main>'

บรรทัดล่างพัง — เพราะ &:title แปลว่า "เรียก method ชื่อ title" · แต่ Hash ไม่มี method ชื่อนั้น มันมีแค่ช่องชื่อ :title ซึ่งต้องหยิบด้วย post[:title]
error อ่านเป็นภาษาคนว่า: "หา method ชื่อ title ไม่เจอ ในของชนิด Hash" · บรรทัด ^^^^ คือ Ruby ชี้ให้ดูว่าพังตรงคำไหน

แล้วทำไมสไลด์ (หน้า 20–21) เขียน posts.map(&:title) ได้? เพราะในแอป Rails จริง บทความแต่ละชิ้นไม่ใช่ Hash แต่เป็น object ชนิด Post ที่มี method .title ให้เรียก (Post ตัวใหญ่ = แม่พิมพ์ของบทความ · สร้างแม่พิมพ์เองได้ในบท 95 · Rails สร้าง .title ให้เองยังไงดูบท 97)
สรุป: กับ Hash ใช้ { |post| post[:title] } · กับ object ที่มี method title ใช้ &:title ได้

🗣 ศัพท์ที่เพิ่งเรียนไป
&:method (symbol to proc)
ทางลัดของ block ที่แค่เรียก method เดียว · คนเรียกท่านี้ว่า symbol to proc (proc คือก้อนโค้ดที่เก็บไว้เป็นของชิ้นหนึ่งได้ — ตอนนี้ไม่ต้องรู้ลึก) · พิมพ์ถาม AI ได้ว่า "ทำไม posts.map(&:title) พังเมื่อ posts เป็น Array ของ Hash"
method chaining
การเอาจุดต่อ method กันเป็นทอด ๆ .select { … }.map { … } ผลของตัวหน้าไหลเข้าตัวหลัง · บอก senior ได้ว่า "ผมแตก chain นี้เป็นตัวแปรพักทีละขั้นเพื่อ debug" — ทำแบบ published_posts ข้างบน

7 · ภารกิจ — ลิ้นชักบทความ 4 แผ่น

🎯 ภารกิจ (~10 นาที)

  1. สร้างไฟล์ชื่อ task.rb แล้ววางข้อมูลจำลองนี้ลงไป:
    posts = [
      { title: "ต้มยำกุ้ง",    published: true,  views: 120 },
      { title: "ส้มตำ",        published: false, views: 40 },
      { title: "เที่ยวน่าน",   published: true,  views: 300 },
      { title: "ข้าวซอย",      published: true,  views: 80 },
    ]
  2. เขียนต่อท้ายอีกสามบรรทัด ห้ามใช้ for — แต่ละบรรทัดขึ้นต้นด้วย p:
    ① ชื่อบทความที่เผยแพร่แล้ว · ② ยอดอ่านรวมทุกบทความ · ③ ชื่อบทความแรกที่ยอดอ่านเกิน 100
  3. รันใน terminal: ruby task.rb
  4. ควรเห็นสามบรรทัดนี้:
    ["ต้มยำกุ้ง", "เที่ยวน่าน", "ข้าวซอย"]
    540
    "ต้มยำกุ้ง"

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

# 1) ชื่อบทความที่เผยแพร่แล้ว
p posts.select { |post| post[:published] }.map { |post| post[:title] }

# 2) ยอดอ่านรวมทุกบทความ
p posts.sum { |post| post[:views] }

# 3) ชื่อบทความแรกที่ยอดอ่านเกิน 100
p posts.find { |post| post[:views] > 100 }[:title]

ผลรันจริง (ruby task.rb):

["ต้มยำกุ้ง", "เที่ยวน่าน", "ข้าวซอย"]
540
"ต้มยำกุ้ง"
  • ข้อ ③ find ได้บทความทั้งก้อน แล้ว [:title] ต่อท้ายเพื่อหยิบแค่ชื่อ · "เที่ยวน่าน" ก็เกิน 100 แต่ find หยุดที่ชิ้นแรกที่เจอ
  • ถ้าเปลี่ยนเงื่อนไขเป็น > 1000 จะไม่มีชิ้นไหนผ่าน → ได้ error ตัวที่สามในรายการข้างบน (ผมลองแล้ว ได้ undefined method '[]' for nil จริง) — วิธีกันพังแบบนี้อยู่ในบท 94 (&.)

แบบทดสอบ

post = { title: "ส้มตำ", views: 40 } แล้วสั่ง p post["views"] จะได้อะไร?

ตัวเลือกที่ 2 ถูก (ผมรันจริงได้ nil) · views: 40 เก็บไว้ใต้ป้าย :views (symbol) แต่เราหาด้วย "views" (string) ซึ่งเป็นคนละป้าย · ตัวเลือกที่ 1 ผิดเพราะ Ruby ไม่ถือว่าสองตัวนี้เหมือนกัน · ตัวเลือกที่ 3 ผิดเพราะชนิดของ key ไม่ได้เปลี่ยนชนิดของค่า · ตัวเลือกที่ 4 ผิดเพราะ [] หาไม่เจอจะได้ nil เงียบ ๆ — KeyError มาจาก fetch ที่ไม่ได้ให้ค่าสำรอง

จุดที่ต้องจำ: สร้างด้วย views: ต้องหยิบด้วย [:views] · เจอ nil แปลก ๆ ให้เช็คชนิดของ key ก่อน

[2, 5, 8].select { |n| n > 4 } ได้อะไร?

ตัวเลือกที่ 1 ถูก (ผลรันจริง [5, 8]) · select คือคนคัดของบนสายพาน เก็บชิ้นที่ผ่านเงื่อนไขไว้ · ตัวเลือกที่ 2 คือผลของ map (ทำกับทุกชิ้นแล้วเก็บคำตอบ จริง/เท็จ) — ผมรันแล้วได้แบบนั้นจริง · ตัวเลือกที่ 3 คือผลของ find ที่หยุดที่ชิ้นแรก · ตัวเลือกที่ 4 คือผลของ reject ซึ่งกลับด้านกับ select

จุดที่ต้องจำ: map = แปลงทุกชิ้น · select = เก็บชิ้นที่ผ่าน · reject = ทิ้งชิ้นที่ผ่าน · find = ชิ้นแรกชิ้นเดียว

อยากได้ "บทความแรกที่ยอดอ่านเกิน 100" ชิ้นเดียว จาก posts ควรใช้ method ไหน?

ตัวเลือกที่ 3 ถูก · find คืนของชิ้นเดียว (หรือ nil ถ้าไม่เจอ) · ตัวเลือกที่ 2 ได้ผลเป็น Array ของทุกชิ้นที่ผ่าน ไม่ใช่ชิ้นเดียว · ตัวเลือกที่ 1 ให้ผลออกมาครบทุกชิ้นโดยไม่คัดอะไรเลย · ตัวเลือกที่ 4 ให้ตัวเลขรวม ไม่ใช่บทความ

จุดที่ต้องจำ: อยากได้ "หลายชิ้น" → select · อยากได้ "ชิ้นเดียว" → find (แล้วคิดเผื่อว่าได้ nil)

posts เป็น Array ของ Hash แบบในหัวข้อ 5 · ทำไม posts.map(&:title) ถึงพัง?

ตัวเลือกที่ 4 ถูก · error จริงคือ undefined method 'title' for an instance of Hash · &:title แปลว่า "เรียก .title กับทุกชิ้น" แต่ Hash มีแค่ช่อง :title ต้องหยิบด้วย post[:title] · ตัวเลือกที่ 1 ผิดเพราะ names.map(&:upcase) ในหัวข้อ 6 รันด้วย Ruby ล้วนได้ปกติ · ตัวเลือกที่ 2 ผิดเพราะทางลัดนี้ใช้ symbol · ตัวเลือกที่ 3 ผิดเพราะ (&:upcase) ในวงเล็บทำงานได้จริง

จุดที่ต้องจำ: &:ชื่อ = เรียก method ชื่อนั้น · Hash ต้องใช้ { |post| post[:ชื่อ] } · object อย่าง Post ใน Rails ถึงใช้ &:title ได้

✅ สรุปบทนี้ในสี่บรรทัด
1. Array [ ] = รายการเรียงลำดับ หยิบด้วยเลขช่องเริ่ม 0 (เหมือน slice ของ Go)
2. Hash { title: … } = คู่ชื่อ–ค่า หยิบด้วยชื่อช่อง (เหมือน map ของ Go) · ใช้จำลองบทความหนึ่งชิ้น
3. :title = Symbol = ป้ายชื่อ · "title" = ข้อความ · หยิบข้ามกันไม่ได้
4. map / select / find / sum / sort แทน for loop · { |post| … } คือสิ่งที่ทำกับแต่ละชิ้น

ติดตรงไหน ลองพิมพ์โค้ดของคุณพร้อม error ไปถาม AI หรือ senior ได้เลย — ใช้ศัพท์ในกล่อง 🗣 จะได้คำตอบตรงขึ้น

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

หน้าสไลด์เขียนของจริงต้องสนใจไหม
18:a.object_id == :a.object_id # true จริง แต่เป็นเรื่องการจัดการหน่วยความจำข้างใน · บทนี้ข้ามไปไม่ต้อง — รู้แค่ว่า Symbol คือป้ายชื่อก็พอ
19Go v, ok := m["a"] คู่กับ Ruby m[:a] Ruby ไม่มี ok · key ไม่มีได้ nil เงียบ ๆสนใจ — ใช้ fetch ถ้าอยากรู้ว่าไม่มี
19"เข้าถึงด้วย m[:a] ไม่ใช่ m.a" ถูก · ลอง m.a จะพัง NoMethodError แบบเดียวกับ &:title ในหัวข้อ 6สนใจ
20–21posts.select(&:published) · posts.map(&:title) ใช้ได้เมื่อ posts เป็น object ที่มี method (เช่น Post ใน Rails) · ถ้าเป็น Hash แบบบทนี้จะพังสนใจ — ดูหัวข้อ 6
21nums.min / nums.max · nums.sort / nums.uniq / คือหาร: ได้ 0 และ NoMethodErrorสนใจ — แยกพิมพ์ทีละบรรทัด
21comment # [6,2,8,2,10] Ruby พิมพ์ [6, 2, 8, 2, 10] มีช่องว่างหลังจุลภาคไม่ต้อง — ค่าเหมือนกัน

อ้างอิง