Cấu tạo của một hàm
Dạng tổng quát của một hàm (function) trong Zero:
fun name(param1: Type1, param2: Type2) -> ReturnType {
// body
return value
}
Các thành phần:
fun: từ khóa mở đầu một hàm.name: tên của hàm.(param1: Type1, ...): danh sách tham số. Mọi tham số đều có kiểu tường minh.-> ReturnType: kiểu trả về.{ ... }: thân hàm, một khối lệnh.return value: thoát khỏi hàm với giá trịvalue.
Một ví dụ cụ thể nhỏ:
fun double(value: i32) -> i32 {
return value * 2
}
Đó là toàn bộ hàm. Nó nhận một i32, trả về một i32 khác và không làm I/O nào. Bên trong thân hàm, value là một biến kiểu let: bạn dùng nó như mọi biến cục bộ khác.
Gọi hàm
Lời gọi hàm trông đúng như bạn nghĩ:
let result = double(21)
Đối số phải khớp với kiểu tham số. Kết quả được gán cho result, và trình biên dịch suy ra nó là i32 vì double trả về i32.
Một ví dụ hoàn chỉnh kết hợp một hàm phụ và main. Nhấn Chạy để xem nó hoạt động:
Bạn sẽ nhận được math works\n trên stdout.
pub và phạm vi truy cập
Mặc định, một hàm khai báo trong một tệp là riêng tư trong tệp đó (hoặc module, quy tắc phạm vi truy cập chặt hơn khi dự án lớn dần). Để công khai một hàm ra ngoài module, thêm pub phía trước:
pub fun greet() -> String {
return "hello\n"
}
Không có pub, code trong các module khác không thể gọi greet. Runtime cần gọi main từ bên ngoài mọi module do người dùng định nghĩa, đó là lý do main luôn là pub.
Quy tắc mặc định riêng tư là điều nên tận dụng. Chỉ đánh dấu những gì bạn muốn làm giao diện; phần còn lại ở yên trong module.
Kiểu trả về
Mọi hàm đều khai báo kiểu trả về sau ->. Các kiểu trả về thường gặp:
fun answer() -> i32 { return 42 }
fun ok() -> bool { return true }
fun label() -> String { return "ready\n" }
fun nothing() -> Void { }
Void là kiểu trả về của một hàm làm việc thông qua hiệu ứng phụ thay vì tạo ra giá trị. Hàm Void không cần return tường minh: chạy hết thân hàm là đủ.
fun log(world: World, message: String) -> Void raises {
check world.out.write(message)
}
Lời gọi hàm bỏ qua giá trị trả về
Nếu một hàm trả về giá trị mà bạn không quan tâm, bạn vẫn phải thừa nhận giá trị đó. Cách làm quen thuộc là gán nó bằng let:
ignored là một biến mà phần còn lại của hàm không bao giờ đọc. Quy ước đặt tên ignored (hoặc _) cho thấy việc bỏ qua là có chủ đích. Cách này rườm rà hơn so với âm thầm bỏ giá trị trả về, và đó chính là mục đích: trong một ngôn ngữ mà agent đọc và sinh code, một giá trị không được đọc thường là bug đáng để làm lộ ra.
Vai trò của raises
Một hàm có thể thất bại sẽ khai báo điều đó trên chữ ký. Ta đã thấy điều này ở main:
pub fun main(world: World) -> Void raises {
check world.out.write("hello\n")
}
Mệnh đề raises có thể đứng một mình (mọi lỗi) hoặc cụ thể:
fun validate(ok: Bool) -> i32 raises { InvalidInput } {
if ok == false {
raise InvalidInput
}
return 42
}
raises { InvalidInput } nghĩa là "hàm này có thể thất bại với InvalidInput, và không gì khác". Nơi gọi phải dùng check (hoặc một dạng xử lý chi tiết hơn) để lan truyền hoặc xử lý lỗi.
Raises và check đi sâu vào phần này, bao gồm chuyện gì xảy ra khi có nhiều kiểu lỗi và check tương tác thế nào với mệnh đề raises của nơi gọi.
Hàm generic
Khi bạn muốn một hàm hoạt động với nhiều kiểu, hãy khai báo tham số kiểu trong dấu ngoặc nhọn:
fun makePair<T, U>(left: T, right: U) -> Pair<T, U> {
return Pair { left: left, right: right }
}
T và U là tham số kiểu: nơi gọi quyết định chúng là gì. Gọi makePair(40, 2_u8) cho bạn một Pair<i32, u8>. Xem Generic để biết đầy đủ, bao gồm shape generic và ràng buộc.
Hàm nằm ở đâu
Trong một chương trình nhỏ, bạn viết hàm trực tiếp trong tệp .0. Trong một package, bạn chia hàm ra nhiều tệp trong src/ và trình biên dịch tự giải quyết các tham chiếu giữa các tệp. Những điều cơ bản vẫn giữ nguyên, fun, tham số, kiểu trả về, thân hàm, bất kể hàm nằm ở đâu về mặt vật lý.
Ghi chú về phong cách
Vài quy ước bạn sẽ thấy trong các ví dụ chính thức:
- Tên hàm viết thường, các từ viết liền (
makePair) hoặc phân tách theo camelCase. Thư viện chuẩn thiên về camelCase. - Mỗi hàm một giá trị trả về. Nếu cần trả về nhiều thứ, hãy tạo một
shapenhỏ cho chúng: như vậy rõ ràng hơn là trả về một tuple lồng trong cặp lồng trong tuple. - Hàm
Voidchỉ thực hiện các lời gọicheck; hàm tính toán giá trị thì tránh I/O khi có thể. Sự tách biệt này một phần là văn hóa, một phần được bắt buộc: một hàm tính toán thuần không nhậnworldnên theo đúng nghĩa đen không thể làm I/O.
Điểm cuối cùng đáng để suy ngẫm. Vì I/O nằm sau capability World và World được truyền vào một cách tường minh, chữ ký của hàm cho bạn biết nó có thể làm I/O hay không. Những hàm mà chữ ký không nhắc đến World là thuần đối với thế giới bên ngoài. Đó là tính chất mà agent (và con người) có thể dựa vào mà không cần đọc thân hàm.
Tiếp theo: if/else
Bạn đã thấy if xuất hiện thoáng qua. Tài liệu tiếp theo trình bày chi tiết biểu thức if/else, bao gồm cách chúng tương tác với biến và những gì cố tình vắng mặt (không ép kiểu truthy, không toán tử ba ngôi).
Câu hỏi thường gặp
Làm sao để khai báo một hàm trong Zero?
Dùng fun: fun name(param: Type) -> ReturnType { body }. Thêm pub phía trước để hàm nhìn thấy được từ bên ngoài module. Thêm raises sau kiểu trả về nếu hàm có thể thất bại. Ví dụ: pub fun double(value: i32) -> i32 { return value * 2 }.
Từ khóa pub dùng để làm gì?
pub đánh dấu một khai báo là công khai: code bên ngoài module hiện tại nhìn thấy được. Không có pub, hàm là riêng tư trong tệp (hoặc package) nơi nó được khai báo. Điểm vào quen thuộc pub fun main phải là công khai để runtime có thể tìm và gọi nó.
Làm sao để trả về giá trị từ một hàm trong Zero?
Viết return value bên trong thân hàm. Biểu thức phải khớp với kiểu trả về đã khai báo. Một hàm có kiểu trả về Void không trả về gì và không cần câu lệnh return tường minh: chạy hết thân hàm là được.
Hàm trong Zero có nhận được nhiều tham số không?
Có. Liệt kê chúng trong ngoặc tròn, ngăn cách bằng dấu phẩy, mỗi tham số có tên và kiểu: fun add(a: i32, b: i32) -> i32 { return a + b }. Mỗi tham số là một biến kiểu let bên trong thân hàm. Zero yêu cầu kiểu tường minh cho tham số: không có suy luận kiểu tham số khi khai báo hàm.
raises trên chữ ký hàm nghĩa là gì?
raises khai báo rằng hàm có thể thất bại. raises đứng một mình cho phép mọi kiểu lỗi; raises { InvalidInput } giới hạn ở một lỗi có tên cụ thể. Nơi gọi phải dùng check (hoặc một cấu trúc xử lý lỗi khác) để thừa nhận khả năng thất bại: không thể âm thầm bỏ qua nó.