Advanced Functions and Closures
หัวข้อนี้จะสำรวจคุณสมบัติขั้นสูงบางประการที่เกี่ยวข้องกับฟังก์ชันและโคลเชอร์ (closures) ซึ่งรวมถึงพอยน์เตอร์ฟังก์ชัน (function pointers) และการส่งคืนค่าเป็นโคลเชอร์
Function Pointers
เราได้พูดถึงวิธีส่งโคลเชอร์ไปยังฟังก์ชันมาแล้ว คุณยังสามารถส่งฟังก์ชันธรรมดาไปยังฟังก์ชันได้อีกด้วย! เทคนิคนี้มีประโยชน์เมื่อคุณต้องการส่งฟังก์ชันที่คุณได้นิยามไว้แล้ว แทนที่จะนิยามโคลเชอร์ขึ้นมาใหม่ ฟังก์ชันจะถูกบีบบังคับแปลงประเภท (coerce) ไปเป็นประเภท fn (ตัว f พิมพ์เล็ก) ซึ่งไม่ควรสับสนกับ Fn closure trait ประเภท fn ถูกเรียกว่า พอยน์เตอร์ฟังก์ชัน (function pointer) การส่งฟังก์ชันด้วยพอยน์เตอร์ฟังก์ชันจะทำให้คุณสามารถใช้ฟังก์ชันเป็นอาร์กิวเมนต์ให้กับฟังก์ชันอื่นได้
ไวยากรณ์สำหรับระบุว่าพารามิเตอร์เป็นพอยน์เตอร์ฟังก์ชันนั้นคล้ายกับของโคลเชอร์ ดังแสดงในโค้ดตัวอย่างที่ 20-28 ซึ่งเรานิยามฟังก์ชัน add_one ที่บวก 1 เข้ากับพารามิเตอร์ของมัน ฟังก์ชัน do_twice รับพารามิเตอร์สองตัว: พอยน์เตอร์ฟังก์ชันที่ไปยังฟังก์ชันใดๆ ที่รับพารามิเตอร์ i32 และคืนค่าเป็น i32 และค่า i32 อีกหนึ่งค่า ฟังก์ชัน do_twice จะเรียกฟังก์ชัน f สองครั้ง โดยส่งค่า arg ไปให้ จากนั้นบวกผลลัพธ์ของการเรียกฟังก์ชันทั้งสองเข้าด้วยกัน ฟังก์ชัน main จะเรียก do_twice ด้วยอาร์กิวเมนต์ add_one และ 5
fn add_one(x: i32) -> i32 {
x + 1
}
fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 {
f(arg) + f(arg)
}
fn main() {
let answer = do_twice(add_one, 5);
println!("The answer is: {answer}");
}
โค้ดนี้จะพิมพ์ The answer is: 12 ระบุว่าพารามิเตอร์ f ใน do_twice เป็น fn ที่รับพารามิเตอร์หนึ่งตัวประเภท i32 และคืนค่าเป็น i32 จากนั้นเราสามารถเรียก f ภายในบอดี้ของ do_twice ได้ ใน main เราสามารถส่งชื่อฟังก์ชัน add_one เป็นอาร์กิวเมนต์แรกให้กับ do_twice ได้
แตกต่างจากโคลเชอร์ fn เป็นประเภท (type) ไม่ใช่เทรต (trait) ดังนั้นเราจึงระบุ fn เป็นประเภทพารามิเตอร์โดยตรง แทนที่จะประกาศพารามิเตอร์ประเภทเจเนอริกที่มีเทรต Fn ตัวใดตัวหนึ่งเป็นข้อจำกัดเทรต (trait bound)
พอยน์เตอร์ฟังก์ชันอิมพลีเมนต์ทั้งสาม closure traits (Fn, FnMut, และ FnOnce) ซึ่งหมายความว่าคุณสามารถส่งพอยน์เตอร์ฟังก์ชันเป็นอาร์กิวเมนต์ให้กับฟังก์ชันที่คาดหวังโคลเชอร์ได้เสมอ ทางที่ดีที่สุดคือควรเขียนฟังก์ชันโดยใช้ประเภทเจเนอริกและ closure traits ตัวใดตัวหนึ่ง เพื่อให้ฟังก์ชันของคุณสามารถรับได้ทั้งฟังก์ชันและโคลเชอร์
อย่างไรก็ตาม ตัวอย่างหนึ่งที่คุณต้องการรับเฉพาะ fn และไม่รับโคลเชอร์คือ เมื่อต้องเชื่อมต่อกับโค้ดภายนอกที่ไม่มีโคลเชอร์: ฟังก์ชันในภาษา C สามารถรับฟังก์ชันเป็นอาร์กิวเมนต์ได้ แต่ภาษา C ไม่มีโคลเชอร์
เพื่อเป็นตัวอย่างที่คุณสามารถใช้ได้ทั้งโคลเชอร์ที่นิยามแบบอินไลน์ (inline) หรือฟังก์ชันที่มีชื่อ ให้ดูการใช้งานเมธอด map ที่มีให้โดย Iterator trait ในไลบรารีมาตรฐาน หากต้องการใช้เมธอด map เปลี่ยนเวกเตอร์ของตัวเลขให้เป็นเวกเตอร์ของสตริง เราสามารถใช้โคลเชอร์ได้ ดังแสดงในโค้ดตัวอย่างที่ 20-29
fn main() {
let list_of_numbers = vec![1, 2, 3];
let list_of_strings: Vec<String> =
list_of_numbers.iter().map(|i| i.to_string()).collect();
}
หรือเราสามารถระบุชื่อฟังก์ชันเป็นอาร์กิวเมนต์ให้กับ map แทนโคลเชอร์ได้ โค้ดตัวอย่างที่ 20-30 แสดงให้เห็นว่ารูปแบบนี้จะออกมาเป็นอย่างไร
fn main() {
let list_of_numbers = vec![1, 2, 3];
let list_of_strings: Vec<String> =
list_of_numbers.iter().map(ToString::to_string).collect();
}
โปรดสังเกตว่าเราต้องใช้ไวยากรณ์แบบระบุชื่อเต็มสมบูรณ์ (fully qualified syntax) ที่เราพูดถึงในหัวข้อ “Advanced Traits” เนื่องจากมีหลายฟังก์ชันที่มีชื่อว่า to_string
ในที่นี้ เราใช้ฟังก์ชัน to_string ที่นิยามไว้ใน ToString trait ซึ่งไลบรารีมาตรฐานอิมพลีเมนต์ไว้ให้สำหรับประเภทใดๆ ที่อิมพลีเมนต์ Display
จำได้ไหมจากหัวข้อ “ค่านิวเมอเรชัน (Enum Values)” ในบทที่ 6 ว่าชื่อของแต่ละตัวแปรผันของอีนัม (enum variant) ที่เรานิยามจะกลายเป็นฟังก์ชันสร้างค่าเริ่มต้น (initializer function) ด้วยเช่นกัน เราสามารถใช้ฟังก์ชันสร้างค่าเริ่มต้นเหล่านี้เป็นพอยน์เตอร์ฟังก์ชันที่อิมพลีเมนต์ closure traits ได้ ซึ่งหมายความว่าเราสามารถระบุฟังก์ชันสร้างค่าเริ่มต้นเป็นอาร์กิวเมนต์สำหรับเมธอดที่รับโคลเชอร์ได้ ดังที่เห็นในโค้ดตัวอย่างที่ 20-31
fn main() {
// ANCHOR: here
enum Status {
Value(u32),
Stop,
}
let list_of_statuses: Vec<Status> = (0u32..20).map(Status::Value).collect();
// ANCHOR_END: here
}
ในที่นี้ เราสร้างอินสแตนซ์ Status::Value โดยใช้ค่า u32 แต่ละตัวในช่วงที่ map ถูกเรียกใช้ โดยผ่านฟังก์ชันสร้างค่าเริ่มต้นของ Status::Value บางคนชอบสไตล์นี้และบางคนชอบใช้โคลเชอร์ ทั้งสองแบบจะถูกคอมไพล์เป็นโค้ดเดียวกัน ดังนั้นเลือกใช้สไตล์ใดก็ได้ที่คุณรู้สึกว่าชัดเจนกว่าสำหรับคุณ
Returning Closures
โคลเชอร์ถูกนำเสนอผ่านเทรต ซึ่งหมายความว่าคุณไม่สามารถส่งคืนโคลเชอร์โดยตรงได้ ในกรณีส่วนใหญ่ที่คุณอาจต้องการส่งคืนเทรต คุณสามารถใช้ประเภทคอนกรีตที่อิมพลีเมนต์เทรตนั้นเป็นค่าส่งคืนของฟังก์ชันแทนได้ อย่างไรก็ตาม ปกติคุณไม่สามารถทำเช่นนั้นกับโคลเชอร์ได้เนื่องจากพวกมันไม่มีประเภทคอนกรีตที่ส่งคืนได้ ตัวอย่างเช่น คุณไม่ได้รับอนุญาตให้ใช้พอยน์เตอร์ฟังก์ชัน fn เป็นประเภทส่งคืน หากโคลเชอร์นั้นมีการจับ (capture) ค่าใดๆ จากขอบเขตของมัน
แต่โดยปกติคุณจะใช้ไวยากรณ์ impl Trait ที่เราเรียนรู้กันในบทที่ 10 คุณสามารถส่งคืนประเภทฟังก์ชันใดก็ได้ โดยใช้ Fn, FnOnce, และ FnMut ตัวอย่างเช่น โค้ดในโค้ดตัวอย่างที่ 20-32 จะคอมไพล์ผ่านได้ด้วยดี
#![allow(unused)]
fn main() {
fn returns_closure() -> impl Fn(i32) -> i32 {
|x| x + 1
}
}
อย่างไรก็ตาม ดังที่เราได้ตั้งข้อสังเกตไว้ในหัวข้อ “การอนุมานและการระบุประเภทโคลเชอร์” ในบทที่ 13 โคลเชอร์แต่ละอันก็เป็นประเภทเฉพาะที่แตกต่างกันในตัวเองเช่นกัน หากคุณต้องการทำงานกับหลายฟังก์ชันที่มีลายเซ็นเดียวกันแต่มีการอิมพลีเมนต์ต่างกัน คุณจะต้องใช้ trait object สำหรับพวกมัน พิจารณาสิ่งที่จะเกิดขึ้นหากคุณเขียนโค้ดลักษณะดังแสดงในโค้ดตัวอย่างที่ 20-33
fn main() {
let handlers = vec![returns_closure(), returns_initialized_closure(123)];
for handler in handlers {
let output = handler(5);
println!("{output}");
}
}
fn returns_closure() -> impl Fn(i32) -> i32 {
|x| x + 1
}
fn returns_initialized_closure(init: i32) -> impl Fn(i32) -> i32 {
move |x| x + init
}
ในที่นี้ เรามีฟังก์ชันสองอันคือ returns_closure และ returns_initialized_closure ซึ่งทั้งคู่ส่งคืน impl Fn(i32) -> i32 ข้อสังเกตคือโคลเชอร์ที่พวกมันส่งคืนนั้นแตกต่างกัน แม้ว่าจะอิมพลีเมนต์ประเภทเดียวกันก็ตาม หากเราพยายามคอมไพล์สิ่งนี้ Rust จะแจ้งให้เราทราบว่าไม่สามารถทำงานได้:
$ cargo build
Compiling functions-example v0.1.0 (file:///projects/functions-example)
error[E0308]: mismatched types
--> src/main.rs:2:44
|
2 | let handlers = vec![returns_closure(), returns_initialized_closure(123)];
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected opaque type, found a different opaque type
...
9 | fn returns_closure() -> impl Fn(i32) -> i32 {
| ------------------- the expected opaque type
...
13 | fn returns_initialized_closure(init: i32) -> impl Fn(i32) -> i32 {
| ------------------- the found opaque type
|
= note: expected opaque type `impl Fn(i32) -> i32`
found opaque type `impl Fn(i32) -> i32`
= note: distinct uses of `impl Trait` result in different opaque types
For more information about this error, try `rustc --explain E0308`.
error: could not compile `functions-example` (bin "functions-example") due to 1 previous error
ข้อความแสดงข้อผิดพลาดบอกเราว่าเมื่อใดก็ตามที่เราส่งคืน impl Trait Rust จะสร้าง opaque type (ประเภททึบแสง) ที่ไม่ซ้ำกันขึ้นมา ซึ่งเป็นประเภทที่เราไม่สามารถมองเห็นรายละเอียดของสิ่งที่ Rust สร้างขึ้นมาให้เราได้ และไม่สามารถเดาประเภทที่ Rust จะสร้างขึ้นเพื่อเขียนเองได้ ดังนั้น แม้ว่าฟังก์ชันเหล่านี้จะส่งคืนโคลเชอร์ที่อิมพลีเมนต์เทรตเดียวกันคือ Fn(i32) -> i32 แต่ opaque types ที่ Rust สร้างขึ้นสำหรับแต่ละอันก็ยังคงแตกต่างกัน (สิ่งนี้คล้ายกับวิธีที่ Rust สร้างประเภทคอนกรีตที่แตกต่างกันสำหรับบล็อก async ที่แยกจากกัน แม้ว่าจะมียิวด์ประเภทเดียวกัน ดังที่เราเห็นในหัวข้อ “ประเภท Pin และ Unpin Trait” ในบทที่ 17) เราได้เห็นแนวทางแก้ไขปัญหานี้มาแล้วสองสามครั้ง: เราสามารถใช้ trait object ดังแสดงในโค้ดตัวอย่างที่ 20-34
fn main() {
let handlers = vec![returns_closure(), returns_initialized_closure(123)];
for handler in handlers {
let output = handler(5);
println!("{output}");
}
}
fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
Box::new(|x| x + 1)
}
fn returns_initialized_closure(init: i32) -> Box<dyn Fn(i32) -> i32> {
Box::new(move |x| x + init)
}
โค้ดนี้จะคอมไพล์ได้สำเร็จ สำหรับข้อมูลเพิ่มเติมเกี่ยวกับ trait objects โปรดอ้างอิงหัวข้อ “การใช้ Trait Objects เพื่อเป็นตัวแทนนามธรรมสำหรับพฤติกรรมร่วม” ในบทที่ 18
ถัดไป เราจะมาดูเรื่องแมโคร (macros) กันครับ!