Vì sao cần hiệu ứng đại số
(antelang.org)- Hiệu ứng đại số là một tính năng ngôn ngữ bắt và xử lý luồng điều khiển giống như ngoại lệ có thể tiếp tục, là tính năng cốt lõi của Ante và cũng được dùng làm trung tâm trong các ngôn ngữ nghiên cứu như Koka, Effekt, Eff, Flix
- Với cùng một cơ chế, có thể xây dựng generator, exception, async, coroutine, tự động vi phân ở cấp thư viện; nhờ đa hình hiệu ứng, các hàm như
mapcũng chỉ cần viết một lần bất kể loại hiệu ứng - Nếu biến dependency injection và truyền context như truy cập cơ sở dữ liệu, xuất dữ liệu, logging, truyền trạng thái thành hiệu ứng, có thể xử lý mock khi test, thu thập output, lọc log bằng cách thay handler
- Khi các hiệu ứng như
can IO,can Print,can Failxuất hiện trong chữ ký hàm, điều đó có lợi cho bảo đảm tính thuần, ghi/phát lại và kiểm toán bảo mật, nhưng các hiệu ứng đã được cho phép có thể vô tình lan truyền đến handler hiện có - Điểm yếu truyền thống là lo ngại về hiệu năng, nhưng các ngôn ngữ gần đây đang giảm chi phí bằng tối ưu hóa hiệu ứng tail-resumptive, evidence passing, giới hạn một lần
resume, và chuyên biệt hóa handler
Mô hình cơ bản của hiệu ứng đại số
- Hiệu ứng đại số còn được gọi là effect handlers, và có thể hiểu theo mô hình “ngoại lệ có thể tiếp tục”
- Trong mã giả Ante, ta khai báo hàm hiệu ứng và đánh dấu bằng
cantrong chữ ký hàm để cho biết có thể sử dụng hiệu ứng đó- Khi gọi một hàm hiệu ứng như
say_message: Unit -> Unit, nó trở thành hình thức “ném” một hiệu ứng - Hàm gọi sẽ thể hiện khả năng dùng hiệu ứng đó trong chữ ký, như
foo () can SayMessage
- Khi gọi một hàm hiệu ứng như
- Biểu thức
handlebắt hiệu ứng tương tựtry/catch, rồi tiếp tục phép tính đang bị tạm dừng bằng lời gọiresume- Nếu handler của
say_messagethực thiprint "Hello World!"rồi gọiresume (), phép tính ban đầu tiếp tục và trả về42
- Nếu handler của
- Tên gọi “algebraic” phần lớn là thuật ngữ còn sót lại; trên thực tế, effect handlers gần với cách diễn đạt chính xác hơn, nhưng bài viết dùng tên hiệu ứng đại số vì đây là tên quen thuộc với người dùng
Luồng điều khiển do người dùng định nghĩa
- Hiệu ứng đại số cho phép triển khai nhiều tính năng ngôn ngữ bằng một cơ chế duy nhất
- generator
- exception
- async
- coroutine
- tự động vi phân
- Đa hình hiệu ứng giúp giảm vấn đề what color is your function
map (input: Vec a) (f: a -> b can e): Vec b can ediễn đạt rằng dù hàm đầu vàofthực hiện hiệu ứngenào,mapcũng thực hiện cùng hiệu ứng đó- Có thể dùng cùng một
mapvới việc in ra stdout, gọi hàm bất đồng bộ, stream yield, v.v. - Nhiều ngôn ngữ effect handler cho phép lược bỏ biến hiệu ứng
e, để viết theo dạng quen thuộcmap (input: Vec a) (f: a -> b): Vec b
- Có thể triển khai ngoại lệ bằng cách không gọi
resumekhi xử lý hiệu ứng- Định nghĩa
throw: a -> never_returnscủa hiệu ứngThrow a - Khi chia cho 0, gọi
throw "error: Division by zero!"; handler in thông báo rồi không tiếp tục phép tính
- Định nghĩa
- Generator có thể được triển khai bằng
yield: a -> Unitcủa hiệu ứngYield a- Duyệt qua các phần tử vector và gọi
yield elem - Handler
filtergọi lạiyield xnếu giá trị được yield thỏa điều kiện, rồi tiếp tục sang phần tử kế tiếp bằngresume () - Handler
my_for_eachthực thi hàmfcho từng giá trị được yield và tiếp tục bằngresume ()
- Duyệt qua các phần tử vector và gọi
- Bộ lập lịch hợp tác cũng có thể được tạo bằng hiệu ứng
yield: Unit -> Unit, trong đó handler nhận quyền điều khiển và chuyển sang thực thi hàm khác- Ví dụ scheduler của Effekt cho thấy mẫu này
- Nhiều hiệu ứng kết hợp với nhau tốt, và đây được xem là ưu điểm giúp tăng tính dễ dùng so với các trừu tượng hóa hiệu ứng khác
Dependency injection và khả năng kiểm thử
- Hiệu ứng cũng có thể được dùng cho dependency injection trong các ứng dụng nghiệp vụ thông thường
- Thay vì truyền trực tiếp đối tượng cơ sở dữ liệu dưới dạng tham số hàm, có thể định nghĩa hiệu ứng
Database- Dạng hiện có nhận đối tượng DB làm tham số, như
business_logic (db: Database) (x: I32) - Dạng dựa trên hiệu ứng trở thành
business_logic (x: I32) can Database, và bên trong gọiquery "..."
- Dạng hiện có nhận đối tượng DB làm tham số, như
- Việc chọn cơ sở dữ liệu cụ thể do handler ở phía trên call stack đảm nhiệm
- Có thể đổi DB vận hành sang DB khác hoặc thay bằng mock DB cho kiểm thử
- Handler
mock_databasecó thể bỏ qua thông điệpqueryvàresumeđể luôn trả vềDbResponse.Ok
- Nếu cũng xử lý output như hiệu ứng, trong lúc test có thể thu thập thành chuỗi thay vì ghi trực tiếp ra stdout
- Handler
print_to_stringbắt lời gọiprint msgvà tích lũy vào chuỗiall_messageskèm ký tự xuống dòng output_messagescó thể kiểm chứng giá trị trả về1234và chuỗi thông điệp mà không xuất thật
- Handler
- Logging có thể được chuyển thành output có điều kiện bằng hiệu ứng
LogvàLogLevellog_handlergọiprint msgnếu level của thông điệp bằng hoặc cao hơn ngưỡng đã đặtfoo () with log_handler Errorchỉ in log lỗi
API gọn hơn và truyền context
- Hiệu ứng đại số có thể diễn đạt mẫu đối tượng Context được truyền xuyên suốt chương trình hoặc thư viện dưới dạng hiệu ứng
- Hiệu ứng
Use acó thể xem là hiệu ứng trạng thái, cung cấpget: Unit -> a,set: a -> Unit- Handler
statelưu trạng thái ban đầu, trả về context hiện tại choget, và cập nhật sang context mới choset - Định nghĩa
statetrong ví dụ bỏ qua quy tắc ownership; trong triển khai thực tế có thể cần ràng buộcCopy a
- Handler
- Ví dụ lưu chuỗi trong vector và truyền index như khóa cho thấy chi phí của việc truyền context
- Nếu không dùng hiệu ứng,
push_string,get_string,append_with_separator,example, v.v. phải liên tục nhậnstringslàm tham số - Trong triển khai dựa trên hiệu ứng, các phép toán nguyên thủy
push_string,get_stringgọiget/set, còn mã cấp trên không cần truyền trực tiếpstrings
- Nếu không dùng hiệu ứng,
- Cách này phù hợp khi thư viện bọc việc truyền context nội bộ
- Người dùng thư viện không cần bận tâm đến chi tiết bên trong của cách truyền context
- Nếu không muốn bị ràng buộc vào một kiểu context cụ thể, có thể trừu tượng hóa các hàm cần thiết thành interface
Thay thế biến toàn cục và direct style
- Những API nhìn bề ngoài như không trạng thái nhưng thực ra cần trạng thái, chẳng hạn sinh số ngẫu nhiên hoặc cấp phát bộ nhớ, có thể được diễn đạt bằng hiệu ứng thay vì biến toàn cục
- Ví dụ sinh số ngẫu nhiên cho thấy gánh nặng khi phải truyền trực tiếp đối tượng
Prngxuyên suốt chương trìnhPrngtoàn cục thì tiện, nhưng phát sinh nhược điểm của giá trị toàn cục, như cần đảm bảo an toàn luồng- Nếu dùng
random: Unit -> U8của hiệu ứngRandom, người dùng chỉ cần chỉ rõ việc khởi tạo bằng handler ở đâu đó trong call stack phía trên - Sau đó, nếu muốn đổi sang
/dev/urandomhoặc nguồn số ngẫu nhiên khác, chỉ cần thay handler, phần mã còn lại trong call stack không cần đổi
- Cấp phát bộ nhớ cũng có thể được diễn đạt bằng hiệu ứng
Allocateallocate: (size: Usz) -> Alignment -> Ptr afree: Ptr a -> Unit- Phần lớn lời gọi dùng allocator toàn cục; trong tight loop, có thể thêm handler vào thân vòng lặp để đổi sang arena allocator
- Hiệu ứng cho phép direct style thay vì truyền kết quả được bọc trong giá trị chuyên dụng
- Nếu dùng
Maybe t, phải nối tiếp đường thành công bằngand_then,map - Cú pháp sugar như
?của Rust là cơ chế để tập trung vào đường đi tốt get_line_from_stdin (): String can Fail, IOvàparse (s: String): U32 can Faildựa trên hiệu ứng được viết như mã tuần tự thông thường:line = ...,x = ...,x * 2
- Nếu dùng
- Xử lý thất bại có thể được thực hiện bằng cách áp dụng handler để rời khỏi đường đi tốt
get_line_from_stdin () with default "42"xử lý hiệu ứngFailbằng giá trị mặc định
- Các kiểu lỗi khác nhau cũng được kết hợp tự nhiên bằng danh sách hiệu ứng
LibraryA.foo (): U32 can Throw LibraryA.ErrorLibraryB.bar (): U32 can Throw LibraryB.Errormy_functioncó thể khai báo cùng lúcThrow LibraryA.Error,Throw LibraryB.Error,Throw MyError- Nếu việc lặp lại trở nên dài, có thể tạo type alias như
AllErrors = can Throw ... - Cùng hiệu ứng
Throw Stringsẽ được gộp thành một; nếu muốn tách riêng, cần một kiểu wrapper nhưMyError
Tính thuần, khả năng chạy lại, kiểm toán bảo mật
- Hầu hết ngôn ngữ effect handler, ngoại trừ cỡ OCaml, dùng hiệu ứng ở những nơi có thể phát sinh side effect
- Trong Ante, nếu không đánh dấu như
can Print,can IOthì không thể dùng side effect - Định nghĩa
externkhông thể được trình biên dịch kiểm tra, nên phải tin vào định nghĩa kiểu - Việc chỉ thực hiện hiệu ứng
IOtrong debug mode để duy trì an toàn hiệu ứng ở release mode là một tính năng đã được lên kế hoạch
- Trong Ante, nếu không đánh dấu như
- Một số hàm yêu cầu hàm thuần làm đầu vào
- Khi tạo thread, thread được tạo không được phép gọi vào handler thuộc sở hữu của thread hiện tại
spawn_all (functions: Vec (Unit -> a pure)): Vec a can IOlà dạng chỉ nhận hàm thuần, chạy tất cả hàm trong các thread rồi chờ hoàn tất
- Software Transactional Memory(STM) là kỹ thuật đồng thời cần hàm thuần
- Khi chạy nhiều hàm đồng thời, nếu một giá trị trong transaction bị thread khác thay đổi, transaction đó sẽ được bắt đầu lại
- Triển khai proof-of-concept của Effekt nằm ở effekt-stm
- Tính thuần có thể đem lại khả năng ghi/phát lại tương tự tiện ích gỡ lỗi
rr- Hai handler
recordvàreplayxử lý hiệu ứng cấp cao nhất màmainphát ra, thường làIO recordghi lại sự phát sinh hiệu ứng và kết quả, rồi nâng tiếp lên handlerIOtích hợp để xử lý thậtreplaykhông thực hiệnIOthật mà dùng kết quả trong log hiệu ứng- Nếu mặc định ghi lại trong debug build, có thể có được debugging mang tính xác định
- Hai handler
- Danh sách hiệu ứng trong chữ ký hàm giúp kiểm toán bảo mật theo cách tương tự Capability Based Security
get_pi: Unit -> F64cho biết hàm không âm thầm thực hiệnIOở nền- Nếu sau khi cập nhật thư viện, nó trở thành
get_pi: Unit -> F64 can IO, mã sẽ báo lỗi trừ khi hàm phía gọi đã yêu cầuIO - Nên khai báo chỉ các hiệu ứng tối thiểu; ví dụ chỉ khai báo
Printthay vì toàn bộIO - Việc thêm hiệu ứng mới được xem là thay đổi phá vỡ semantic versioning
- Tài liệu liên quan gồm Capability Based Security và Designing with Static Capabilities and Effects
Hạn chế và chiến lược triển khai
- Một hạn chế của cách tiếp cận hiệu ứng là có thể xảy ra xử lý ngoài ý muốn
- Ngay cả khi một hàm mới yêu cầu
IO, có thể sẽ không có lỗi nếu hàm gọi đã cho phépIO - Hiệu ứng
Failcũng tương tự: nếu một hàm thư viện trước đây không thất bại nhưng sau này có thểFail, nó có thể lan truyền tới handlerFailhiện có - Hành vi này có thể ổn tùy tình huống, nhưng nếu bạn muốn xử lý riêng, chẳng hạn cung cấp giá trị mặc định, thì nó có thể không đúng ý
- Ngay cả khi một hàm mới yêu cầu
- Nhược điểm lớn truyền thống là lo ngại về hiệu năng, nhưng output biên dịch của các hiệu ứng gần đây đã được cải thiện đáng kể
- Nhiều ngôn ngữ hiệu ứng đại số tối ưu hóa hiệu ứng tail-resumptive thành lời gọi closure thông thường
- Hiệu ứng tail-resumptive là hiệu ứng mà handler gọi
resumeở cuối - Phần lớn hiệu ứng thực tế thuộc loại này, và hầu hết ví dụ trong bài cũng nằm trong nhóm này
- Ngoại lệ được phân loại là trường hợp ngoại lệ vì hoàn toàn không gọi
resume
- Hiệu ứng tail-resumptive là hiệu ứng mà handler gọi
- Chiến lược tối ưu hóa cũng khác nhau theo từng ngôn ngữ
- Koka dùng evidence passing, đẩy hiệu ứng lên tới handler và biên dịch sang C không cần runtime
- Ante và OCaml giới hạn
resumechỉ được gọi tối đa một lần- Giới hạn này loại trừ một số hiệu ứng như tính phi tất định
- Đổi lại, nó đơn giản hóa xử lý tài nguyên và cho phép triển khai continuation nội bộ hiệu quả hơn bằng các cách như segmented stacks
- Effekt chuyên biệt hóa và loại bỏ hoàn toàn handler khỏi chương trình
- Cách này có hạn chế là biến hầu hết hàm thành second-class
- Có thể dùng dạng boxed để có first-class function và chuyển sang mô hình pay-as-you-go
- Tài liệu liên quan gồm tài liệu captures của Effekt và bài báo
Chưa có bình luận nào.