Gần đây, các đặc tính đã được giới thiệu trong Mojo nên tôi nghĩ mình sẽ thử chúng. Hiện tại, các đặc điểm tích hợp bao gồm CollectionElement , Copyable , Destructable , Intable , Movable , Sized và Stringable (có vẻ như hậu tố "-able" là một thứ trong các quy ước đặt tên này!).
Đặc điểm hoạt động trên cấu trúc. Để triển khai một đặc điểm, bạn chỉ cần thêm một phương thức vào cấu trúc phù hợp với đặc điểm đó; sau đó chuyển tên đặc điểm làm tham số:
@value struct Duck(Quackable): fn quack(self): print("Quack!")
Trình trang trí @value
trong Mojo chèn các phương thức vòng đời như __init__()
, __copyinit__()
và __moveinit__()
vào cấu trúc, đơn giản hóa cuộc sống của chúng ta một chút vì chúng ta không phải tự thêm chúng.
Các đặc điểm trong Mojo chưa hỗ trợ triển khai mặc định cho các phương thức, do đó, ...
nằm trong phần nội dung của phương thức Quackable
ở trên. Bạn cũng có thể sử dụng pass
, điều này sẽ có tác dụng tương tự như trong Python.
Mặc dù có tên khác nhưng cách tiếp cận cơ bản về các đặc điểm trong Mojo khiến tôi nhớ đến giao diện Go. Trong Go, bạn sẽ xác định cấu trúc Duck
và triển khai giao diện Quackable
như thế này:
type Quackable interface { quack() }
Và để tạo một cấu trúc thỏa mãn giao diện này:
type Duck struct {} func (d Duck) quack() { fmt.Println("Quack!") }
So sánh nó với việc triển khai Mojo:
trait Quackable: fn quack(self): ... @value struct Duck(Quackable): fn quack(self): print("Quack!")
Tôi nghĩ phiên bản Mojo thậm chí còn ngắn gọn hơn, lồng định nghĩa phương thức quack()
bên trong kiểu cấu trúc Duck
. Ngoài việc làm việc trên các cấu trúc, các đặc điểm trong Mojo không yêu cầu từ khóa implements
, giống như trong Go.
Điều thú vị là, tài liệu Mojo đã nêu rõ rằng việc triển khai mặc định chưa được phép, nghĩa là chúng có thể được cho phép trong tương lai. Điều này có nghĩa là Mojo có thể theo đuổi một cách tiếp cận khác với Go, tập trung vào các cấu trúc thỏa mãn giao diện chứ không phải triển khai nó.
Việc triển khai phương thức mặc định là không cần thiết trong Go và đạt được hiệu quả tương tự khi nhúng, vì việc triển khai giao diện/đặc điểm đơn giản là một khái niệm không tồn tại trong Go. Mọi thứ có thể khác ở Mojo.
Giá trị của các đặc điểm và giao diện là làm cho mã có thể tái sử dụng được. Ví dụ: trong Mojo, bạn có thể viết các hàm chấp nhận các loại đặc điểm…
fn make_it_quack[T: Quackable](could_be_duck: T): could_be_duck.quack()
Và sau đó chuyển các cấu trúc khác nhau triển khai đặc điểm giống như đầu vào - mọi thứ sẽ hoạt động bình thường! Ví dụ: đây là cấu trúc Goose
triển khai Quackable
:
@value struct Goose(Quackable): fn quack(self): print("Honk!")
Và ở đây, để gọi make_it_quack
trên cả Goose
và Duck
(hãy nhớ rằng, bạn cần hàm main
trong Mojo làm điểm vào chương trình của mình):
def main(): make_it_quack(Duck()) make_it_quack(Goose())
Đầu ra của điều này sẽ là
Quack! Honk!
Nếu tôi cố gắng chuyển thứ gì đó không triển khai đặc điểm Quackable
vào hàm make_it_quack
, giả sử StealthCow
, chương trình sẽ không biên dịch:
@value struct StealthCow(): pass
make_it_quack(StealthCow())
Thông báo lỗi bên dưới có thể mang tính mô tả nhiều hơn; có lẽ nhóm Mojo sẽ cải thiện nó?
error: invalid call to 'make_it_quack': callee expects 1 input parameter, but 0 were specified
Tương tự nếu tôi loại bỏ phương thức quack
khỏi Goose
; ở đây, chúng tôi nhận được một lỗi mô tả hay:
struct 'Goose' does not implement all requirements for 'Quackable' required function 'quack' is not implemented
Ưu điểm của loại kiểm tra kiểu tĩnh này so với phương pháp Python thuần túy là chúng ta có thể dễ dàng phát hiện lỗi và tránh chuyển bất kỳ lỗi nào sang quá trình sản xuất vì mã sẽ không được biên dịch.
Các đặc điểm trong Mojo đã hỗ trợ tính kế thừa, vì vậy đặc điểm `Quackable` của chúng tôi có thể mở rộng đặc điểm `Audible` như sau:
trait Audible: fn make_sound(self): ...
trait Quackable(Audible): fn quack(self): ...
Điều này có nghĩa là cấu trúc Duck
sẽ phải triển khai cả quack
và make_sound
để phù hợp với đặc điểm Quackable
.
Điều này tương tự như khái niệm "Nhúng giao diện" trong Go, để tạo giao diện mới kế thừa từ các giao diện khác, bạn sẽ nhúng giao diện chính như thế này:
type Quackable interface { Audible // includes methods of Audible in Quackable's method set }
Các đặc điểm cũng chấp nhận các phương thức tĩnh hoạt động mà không cần tạo phiên bản của cấu trúc:
trait Swims: @staticmethod fn swim(): ... @value struct Duck(Quackable, Swims): fn quack(self): print("Quack!") @staticmethod fn swim(): print("Swimming")
fn make_it_swim[T: Swims](): T.swim()
Bạn gọi một phương thức tĩnh như thế này:
def main(): make_it_quack(Duck()) make_it_quack(Goose()) Duck.swim()
Cái nào sẽ xuất ra:
Quack! Honk! Swimming
Lưu ý rằng trong lệnh gọi phương thức cuối cùng, phiên bản Duck không được tạo. Đây là cách các phương thức tĩnh trong Python hoạt động, khác xa với lập trình hướng đối tượng. Mojo được xây dựng dựa trên chức năng Python này.
Điều thú vị là thủ thuật cờ vây có interface{}
cho phép vượt qua bất kỳ loại nào và phổ biến với cộng đồng cờ vây trước khi Go Generics được giới thiệu, sẽ không hoạt động với hàm được gõ Mojo fn
.
Cấu trúc của bạn phải triển khai ít nhất một trong các phương thức vòng đời như __len__
hoặc __str__
, trong trường hợp này, sẽ làm cho nó tuân theo Sized
hoặc Stringable
, để được sử dụng với các hàm chấp nhận các loại đặc điểm.
Đây thực sự không phải là một hạn chế thực sự và rất có ý nghĩa trong Mojo, vì với Mojo, nếu bạn cần gõ động, bạn có thể quay lại hàm def
linh hoạt hơn, sau đó áp dụng phép thuật Python thông thường để làm việc với ẩn số. các loại.
Các hàm Mojo fn
nghiêm ngặt hơn cũng hoạt động trên các cấu trúc chung sử dụng các kiểu như DynamicVector
; đọc thêm về phần đó ở đay .
Một hạn chế khác mà tôi thấy liên quan đến các cấu trúc và phương thức của chúng hơn là các đặc điểm và là một trở ngại tiềm ẩn trong việc triển khai Kiến trúc sạch/tách mã thành các phần khác nhau.
Hãy xem xét một trong những ví dụ trước đây với định nghĩa phương thức cấu trúc Go và Mojo:
type Duck struct {} func (d Duck) quack() { fmt.Println("Quack!") }
@value struct Duck(Quackable): fn quack(self): print("Quack!")
Ví dụ Mojo, theo cú pháp giống Python hơn, lồng định nghĩa phương thức trực tiếp bên trong cấu trúc, trong khi phiên bản Go cho phép nó tách nó khỏi chính cấu trúc đó. Trong phiên bản này, nếu tôi có một cấu trúc rất dài với nhiều loại và phương thức thì sẽ khó đọc hơn một chút.
Tuy nhiên, đó không phải là sự khác biệt quan trọng mà chỉ là điều cần lưu ý.
Các đặc điểm Mojo đã khá hữu ích, mặc dù ngôn ngữ này mới ở giai đoạn đầu. Mặc dù triết lý của Go là sự đơn giản và cố gắng giữ các tính năng ở mức tối thiểu, nhưng có khả năng chúng ta sẽ thấy nhiều chức năng hơn được thêm vào các đặc điểm của Mojo trong tương lai, khiến chúng thậm chí còn mạnh mẽ hơn và cho phép thực hiện nhiều trường hợp sử dụng khác nhau.