🔁 ทวนก่อนเริ่ม (จากบท 91)
- ตัวแปร — ตั้งชื่อแล้วใส่ค่าได้เลย
name = "som"ไม่ต้องบอกชนิดแบบ Go - เรียก method ด้วยจุด —
"som".upcaseแปลว่า "ข้อความนี้ ช่วยทำตัวพิมพ์ใหญ่ให้ที" - string — ข้อความในเครื่องหมายคำพูด
"ต้มยำกุ้ง" - nil — แปลว่า "ไม่มีค่า" (ละเอียดในบท 94)
บทนี้ใช้คำสั่ง 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 สองบรรทัดสุดท้ายเป็นภาษาคน:
hash.rb:10— พังที่ไฟล์hash.rbบรรทัดที่ 10 (บรรทัดfetchตัวสุดท้าย)key not found: :author— "หาช่อง:authorไม่เจอ" ·(KeyError)ในวงเล็บคือชื่อชนิดของ error- บรรทัด
from …บอกเส้นทางที่โค้ดเดินมาถึงจุดนั้น ·'<main>'แปลว่า "ตัวไฟล์หลัก" · ตอนนี้ข้ามได้ (error ละเอียดในบท 96)
เทียบกับ 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· KeyErrorfetchคือการหยิบที่ "ไม่ยอมได้ 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
=>คือลูกศรจับคู่ "key นี้ → ค่านี้" (คนเรียกว่า hash rocket เพราะหน้าตาเหมือนจรวด)title:เป็นทางลัดของ:title =>— สองแบบนี้ได้ Hash เดียวกันเป๊ะ (a == bได้true)- แต่
cใช้ key เป็น string"title"— คนละ Hash กับa(a == cได้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= คนคัดของ เก็บไว้เฉพาะชิ้นที่ผ่านเงื่อนไข → ได้ของน้อยลงหรือเท่าเดิมfind= หยิบชิ้นแรกที่ใช่แล้วหยุดดู → ได้ของชิ้นเดียวsum= เครื่องชั่งท้ายสาย รวมน้ำหนักทั้งหมด ·sort= จัดเรียงจากเล็กไปใหญ่
ที่ไม่เหมือนกัน: สายพานจริง ของชิ้นเดิมถูกราดซอสไปแล้วจริง ๆ ·
แต่ map กับ select ไม่แตะ Array เดิม — มันสร้าง Array ใหม่ให้ ของเดิมยังอยู่ครบ
(ผลรันข้างล่างบรรทัดสุดท้ายพิสูจน์ให้ดู)
ก่อนดูโค้ด ต้องรู้จักของใหม่หนึ่งอย่าง: block คือก้อนโค้ดในปีกกาที่ส่งให้ method เอาไปใช้กับของทีละชิ้น
nums.map { |n| n * 2 }
{ … }— ก้อนโค้ดที่จะทำกับทุกชิ้นบนสายพาน (ปีกกาตรงนี้คือ block ไม่ใช่ Hash · Ruby ดูจากตำแหน่งว่ามันตามหลัง method)|n|— ขีดตั้งสองขีดล้อมชื่อ = "ชิ้นที่ไหลมาถึงตอนนี้ ขอเรียกว่า n" · ตั้งชื่ออะไรก็ได้ · เทียบ Go คือตัวnในfor _, n := range numsนั่นเองn * 2— สิ่งที่ทำกับชิ้นนั้น · ค่าที่ได้จากบรรทัดนี้คือผลของชิ้นนั้น (ไม่ต้องเขียนreturn)
ตอนนี้รู้เท่านี้พอ · 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 } | ทุกชิ้น เอาไปคูณ 2 | Array ใหม่ ยาวเท่าเดิม |
select { |n| n > 2 } | เก็บเฉพาะชิ้นที่มากกว่า 2 | Array ใหม่ (สั้นลง) |
reject { |n| n > 2 } | กลับด้านของ select — ทิ้งชิ้นที่มากกว่า 2 | Array ใหม่ |
find { |n| n > 2 } | ชิ้นแรกที่มากกว่า 2 | ของ 1 ชิ้น (3) |
find ที่ไม่เจอ | ไม่มีชิ้นไหนเกิน 100 | nil |
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}
posts = [ {…}, {…}, {…} ]— Array ที่แต่ละช่องเป็น Hash หนึ่งก้อน (บทความหนึ่งชิ้น) · จุลภาคท้ายชิ้นสุดท้ายใส่ได้ Ruby ไม่ว่า{ |post| post[:published] }— "บทความที่ไหลมา เรียกว่า post · ดูช่อง published" · ถ้าเป็นtrueชิ้นนั้นผ่าน- สองบรรทัดแรกได้ผลเดียวกัน — แบบแรกพักผลไว้ในตัวแปร
published_postsก่อน · แบบที่สองต่อจุดเลย: ผลของselectไหลเข้าmapต่อทันที เหมือนสายพานสองสถานีต่อกัน sum { |post| post[:views] }— รวมยอดอ่าน (120 + 40 + 300 = 460) · ใส่ block บอกว่า "รวมช่องไหน"sort_by { |post| post[:views] }— เรียงโดยใช้ยอดอ่านเป็นเกณฑ์ น้อยไปมาก · ต่อด้วยmapเพื่อดูแค่ชื่อfind— ได้บทความทั้งก้อนชิ้นแรกที่ยอดอ่านเกิน 100 (ไม่ใช่แค่ชื่อ)
เทียบกับ 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]
- สองบรรทัดแรกได้ผลเหมือนกันทุกตัว —
&:upcaseแปลว่า "ทุกชิ้น ให้เรียก.upcase" :upcaseคือ Symbol ที่ใช้เป็นชื่อของ method (เห็นไหม Symbol คือ "ชื่อเรียก" จริง ๆ)&ข้างหน้าบอก Ruby ว่า "เอาชื่อนี้ไปทำเป็น block ให้ที" · เขียนไว้ในวงเล็บ(&:…)แทนปีกกา (เอาไปใส่ในปีกกาจะพัง)
ฝั่ง 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 นาที)
- สร้างไฟล์ชื่อ
task.rbแล้ววางข้อมูลจำลองนี้ลงไป:posts = [ { title: "ต้มยำกุ้ง", published: true, views: 120 }, { title: "ส้มตำ", published: false, views: 40 }, { title: "เที่ยวน่าน", published: true, views: 300 }, { title: "ข้าวซอย", published: true, views: 80 }, ] - เขียนต่อท้ายอีกสามบรรทัด ห้ามใช้
for— แต่ละบรรทัดขึ้นต้นด้วยp:
① ชื่อบทความที่เผยแพร่แล้ว · ② ยอดอ่านรวมทุกบทความ · ③ ชื่อบทความแรกที่ยอดอ่านเกิน 100 - รันใน terminal:
ruby task.rb - ควรเห็นสามบรรทัดนี้:
["ต้มยำกุ้ง", "เที่ยวน่าน", "ข้าวซอย"] 540 "ต้มยำกุ้ง"
ถ้าเจอ error นี้ แปลว่า…
undefined method 'title' for an instance of Hash (NoMethodError)→ คุณใช้&:titleกับ Hash · เปลี่ยนเป็น{ |post| post[:title] }nil can't be coerced into Integer (TypeError)ตอนsum→ คุณหยิบด้วยpost["views"](string) ได้ nil มาบวก · เปลี่ยนเป็นpost[:views]undefined method '[]' for nil (NoMethodError)ที่ข้อ ③ →findหาไม่เจอเลยได้nilแล้วไปต่อ[:title]· เช็คเงื่อนไขในปีกกาว่าพิมพ์ตัวเลขถูกไหม
# 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 คือป้ายชื่อก็พอ |
| 19 | Go v, ok := m["a"] คู่กับ Ruby m[:a] |
Ruby ไม่มี ok · key ไม่มีได้ nil เงียบ ๆ | สนใจ — ใช้ fetch ถ้าอยากรู้ว่าไม่มี |
| 19 | "เข้าถึงด้วย m[:a] ไม่ใช่ m.a" |
ถูก · ลอง m.a จะพัง NoMethodError แบบเดียวกับ &:title ในหัวข้อ 6 | สนใจ |
| 20–21 | posts.select(&:published) · posts.map(&:title) |
ใช้ได้เมื่อ posts เป็น object ที่มี method (เช่น Post ใน Rails) · ถ้าเป็น Hash แบบบทนี้จะพัง | สนใจ — ดูหัวข้อ 6 |
| 21 | nums.min / nums.max · nums.sort / nums.uniq |
/ คือหาร: ได้ 0 และ NoMethodError | สนใจ — แยกพิมพ์ทีละบรรทัด |
| 21 | comment # [6,2,8,2,10] |
Ruby พิมพ์ [6, 2, 8, 2, 10] มีช่องว่างหลังจุลภาค | ไม่ต้อง — ค่าเหมือนกัน |
อ้างอิง
- Ruby From Other Languages (ruby-lang.org) — อ่านหัวข้อย่อย Iteration และ Symbols are not lightweight Strings สั้นมาก อธิบายตรงกับหัวข้อ 3–4 ของบทนี้ว่าทำไมคน Ruby ไม่เขียน for และ Symbol ต่างจาก String ยังไง
- Ruby 4.0 — class Hash —
อ่านแค่ส่วน Hash Data Syntax และ Key Not Found?
ส่วนแรกคือเรื่อง
title:กับ=>· ส่วนหลังคือเรื่อง nil กับfetch - Ruby 4.0 — module Enumerable —
ดูรายการใต้ Methods for Searching and Filtering จะเห็น
findselectrejectอยู่ด้วยกัน พร้อมคำอธิบายหนึ่งบรรทัดของแต่ละตัว - Ruby 4.0 — class Array —
ใช้เปิดดูเมื่ออยากรู้ว่า Array ทำอะไรได้อีก · ลองหา
<<กับsumในหน้านี้ - A Tour of Go — Slices และ
Go maps in action —
ทวนฝั่ง Go ถ้าลืม slice กับ map · ในบทความ maps อ่านส่วนที่พูดถึง zero value กับ
okแล้วเทียบกับ nil ของ Ruby