6 điểm bởi GN⁺ 2024-02-26 | 1 bình luận | Chia sẻ qua WhatsApp
  • Khi mã producer và consumer trao đổi dữ liệu với nhau, nếu viết đảo một phía thành dạng callee thì cấu trúc thuật toán vốn nhìn thấy ban đầu rất dễ bị chôn vùi trong các chuyển trạng thái
  • Coroutine kiểu Knuth là mô hình trong đó hai routine lưu vị trí thực thi và chuyền quyền điều khiển cho nhau, nhưng trong cấu trúc gọi hàm dựa trên stack của C thì rất khó hiện thực trực tiếp theo cách có tính khả chuyển
  • Mẹo cốt lõi của bài viết này là dùng cú pháp C cho phép đặt case bên trong khối con của switch cùng với macro __LINE__ để tạo ra một máy trạng thái ngầm tái nhập tại vị trí sau return
  • Với các macro crBegin, crReturn, crFinish, có thể giữ nguyên cấu trúc vòng lặp gốc của bộ giải nén và parser, nhưng các biến cục bộ cần được bảo toàn phải là static và không được đặt crReturn trong switch tường minh hoặc trên cùng một dòng
  • Trong mã thực tế, do ràng buộc về tính tái nhập và đa luồng, cần một biến thể cải tiến truyền vào struct ngữ cảnh, và coroutine.h cung cấp cả macro scr đơn giản lẫn macro ccr có khả năng tái nhập

Vấn đề về cấu trúc khi nối producer và consumer

  • Trong các chương trình lớn, thường có trường hợp một đoạn mã tạo dữ liệu và đoạn khác tiêu thụ dữ liệu; khi đó việc bên nào là caller và bên nào là callee làm thiết kế trở nên khó khăn
  • Ví dụ gồm hai routine nhỏ
    • Mã giải nén run-length đọc đầu vào bằng getchar() và xuất từng ký tự bằng emit()
    • Mã parser đọc ký tự bằng getchar(), xử lý các đoạn chữ cái liên tiếp là WORD, còn các ký tự khác là PUNCT
  • Nếu nhìn riêng thì cả hai routine đều tự nhiên, nhưng để đầu ra emit() của bộ giải nén nối trực tiếp vào đầu vào getchar() của parser thì cần một cấu trúc liên kết giữa chúng
  • Có thể giải bằng pipe giữa hai process hoặc hai thread
    • emit() của bộ giải nén ghi vào pipe, còn getchar() của parser đọc ở đầu kia
    • Cách này đơn giản và vững chắc nhưng nặng nề, tính khả chuyển thấp, nên nhiều khi không muốn tách thread chỉ cho một tác vụ đơn giản

Mất tính dễ đọc khi viết lại hàm

  • Cách giải truyền thống là viết lại một đầu của kênh giao tiếp thành dạng hàm có thể gọi được
  • Nếu đổi bộ giải nén thành hàm trả về một ký tự mỗi lần gọi, parser hiện có có thể gọi decompressor() thay cho getchar()
  • Ngược lại, nếu đổi parser thành hàm được gọi mỗi khi nhận một ký tự, mã giải nén hiện có chỉ cần gọi parser() thay cho emit()
  • Không cần đổi cả hai; chỉ một phía là đủ để nối được, nhưng mã sau khi viết lại sẽ khó đọc hơn nhiều so với bản gốc
    • Ở bộ giải nén và parser nguyên bản, luồng thuật toán được bộc lộ tự nhiên trong vòng lặp
    • Dạng viết lại phụ thuộc vào biến trạng thái static và chuyển trạng thái bằng switch, khiến khó đọc ra định dạng nén hay ngữ pháp parser ngay từ mã nguồn
  • Mục tiêu là nối chúng lại mà không phải lật một bên thành máy trạng thái tường minh

Coroutine kiểu Knuth và giới hạn của C

  • Cách giải coroutine của Donald Knuth bỏ qua phân biệt caller và callee, xem hai process là các thực thể ngang hàng cùng hợp tác
  • Nguyên lý gọi trong mô hình này khác với lời gọi hàm thông thường
    • Vị trí thực thi hiện tại được lưu ở nơi riêng, không phải trên stack
    • Chương trình nhảy tới vị trí thực thi mà routine kia đã lưu lần cuối
    • Khi bộ giải nén phát ra ký tự, nó lưu program counter của mình rồi chuyển tới vị trí đã lưu của parser
    • Khi parser cần ký tự tiếp theo, nó lưu program counter của mình rồi chuyển tới vị trí đã lưu của bộ giải nén
  • Quyền điều khiển qua lại giữa hai routine nhiều lần tùy nhu cầu
  • Về lý thuyết cách này rất tốt, nhưng trên thực tế chỉ khả thi ở assembly
  • Các ngôn ngữ bậc cao như C phụ thuộc vào cấu trúc dựa trên stack, nên khi chuyển điều khiển giữa các hàm thì một bên phải là caller, bên kia là callee
  • Trong mã C có tính khả chuyển, coroutine thuần túy thực tế không hữu dụng hơn mấy so với cách dùng pipe của Unix

Mô phỏng “return and continue” trong C

  • Hành vi cần có trong C là hàm callee sau khi return sẽ tiếp tục chạy từ ngay sau return đó ở lần gọi tiếp theo: return and continue
  • Ví dụ, sẽ lý tưởng nếu một hàm dạng for (i = 0; i < 10; i++) return i; khi được gọi 10 lần thì lần lượt trả về từ 0 đến 9
  • Cách hiện thực đầu tiên dùng biến trạng thái và goto
    • Đặt label ở đầu hàm và sau mỗi return
    • Biến state được giữ qua các lần gọi sẽ trỏ tới label cần tiếp tục
    • Khi vào hàm, switch(state) sẽ chuyển tới label phù hợp
    • Ngay trước return, lưu vào state label để quay lại ở lần gọi sau
  • Cách này chạy được nhưng việc quản lý label bằng tay gây gánh nặng bảo trì
    • Mỗi lần thêm return phải tạo label mới và thêm vào switch ban đầu
    • Khi bỏ return thì cũng phải bỏ label tương ứng
    • Luôn phải giữ cho thân hàm và danh sách trong switch nhất quán với nhau

Máy trạng thái được giấu bằng Duff’s device

  • Duff’s device nổi tiếng trong C tận dụng cú pháp cho phép đặt case tương ứng của switch bên trong khối con của nó
  • Áp dụng tính chất này vào mẹo coroutine thì thay vì switch chọn goto nào để thực thi, chính switch sẽ hoạt động như cú nhảy tái nhập
  • Dạng cơ bản như sau
    • static int state lưu điểm tiếp tục ở lần sau
    • Đầu hàm vào bằng switch(state) { case 0: ... }
    • Trước return, gán cho state giá trị case kế tiếp
    • Ngay sau return đặt label case tương ứng
  • Bọc nó trong macro sẽ tạo ra giao diện trông giống coroutine
    • crBegin: giấu static int state=0; switch(state) { case 0:
    • crReturn: lưu state, trả về giá trị, rồi đặt label case ngay tại vị trí đó
    • crFinish: đóng khối đang mở
  • crReturn được bọc trong do ... while(0) nên vẫn hợp lệ về cú pháp khi dùng giữa ifelse mà không cần ngoặc nhọn
  • Ban đầu phải truyền số trạng thái trực tiếp như crReturn(1, i), nhưng dùng macro __LINE__ của ANSI C thì có thể lấy ngay số dòng mã nguồn hiện tại làm giá trị trạng thái
  • Sau cải tiến này chỉ cần viết crReturn(x), nhưng phát sinh quy tắc mới là không được đặt hai crReturn trên cùng một dòng

Quy tắc dùng macro và ví dụ

  • Coroutine dựa trên macro này giả định một số quy tắc
    • Bọc thân hàm bằng crBegincrFinish
    • Các biến cục bộ cần được bảo toàn qua crReturn phải khai báo là static
    • Tuyệt đối không đặt crReturn bên trong câu lệnh switch tường minh
    • Với bản hiện thực dựa trên __LINE__, không đặt hai crReturn trên cùng một dòng
  • Ví dụ bộ giải nén giữ nguyên cấu trúc vòng lặp ban đầu, chỉ thay emit(c) bằng crReturn(c) tại nơi phát ký tự
  • Ví dụ parser sẽ quay lại caller bằng crReturn() mỗi khi cần ký tự mới, rồi ở lần gọi tiếp theo tiếp tục chạy với ký tự mới nằm trong tham số c
  • Parser có một thay đổi cấu trúc nhỏ
    • Vì ký tự đầu tiên đã nằm trong c ngay khi vào hàm, nên crReturn tương ứng với getchar() ở đầu vòng lặp gốc được chuyển xuống cuối vòng lặp
    • Nếu muốn, cũng có thể quy định parser cần một lời gọi khởi tạo ban đầu
  • Không cần chuyển cả hai routine sang macro coroutine; chỉ đổi một bên và để bên kia làm caller cũng được
  • Kết quả là có thể kết hợp ANSI C, preprocessor và cú pháp ít dùng của switch để truyền dữ liệu giữa producer và consumer mà không cần viết lại thành máy trạng thái tường minh

Xung đột giữa chuẩn mã hóa và độ rõ ràng của thuật toán

  • Kỹ thuật này vi phạm khá nặng các chuẩn mã hóa thông thường
    • Trong macro có chứa dấu ngoặc nhọn không cân đối
    • Dùng case bên trong khối con
    • crReturn giấu cả switch, returncase trong một macro
  • Theo chuẩn mã hóa, macro che giấu cấu trúc cú pháp có thể bị xem là làm giảm tính rõ ràng
  • Tuy nhiên, hàm bị viết lại thành máy trạng thái tường minh cũng gồm những khối case STATE nhỏ và các chuyển trạng thái, nên cấu trúc trực quan không khác mấy so với hàm liệt kê các khối label goto
  • Hàm càng dài thì việc viết lại thành máy trạng thái càng phá vỡ cấu trúc thuật toán ban đầu nhiều hơn
  • Kỹ thuật này là một sự đánh đổi: che đi một phần cấu trúc cú pháp để bộc lộ cấu trúc thuật toán rõ hơn

Biến thể cải tiến có tính tái nhập và mã được cung cấp

  • Bản hiện thực đồ chơi đơn giản phụ thuộc vào biến static, nên không có tính tái nhập và cũng không phù hợp với đa luồng
  • Trong ứng dụng thực tế, cần có khả năng gọi cùng một hàm trong nhiều context khác nhau, và mỗi context đều có thể tiếp tục từ sau return cuối cùng của riêng nó
  • Cách cải tiến là truyền thêm con trỏ tới struct context như một tham số bổ sung
    • Cả trạng thái cục bộ lẫn biến trạng thái coroutine đều đặt trong các thành viên của struct
    • Các biến như bộ đếm vòng lặp cũng phải truy cập theo kiểu ctx->i thay vì i
    • Mã sẽ xấu hơn đôi chút, nhưng giải quyết được vấn đề tái nhập mà vẫn giữ nguyên cấu trúc tổng thể của routine
  • Người dùng C++ có thể đặt coroutine thành thành viên lớp và để trạng thái tương ứng với biến cục bộ nằm trong lớp, giúp xử lý scope tự nhiên hơn
  • coroutine.h được cung cấp để hiện thực sẵn mẹo coroutine này dưới dạng một bộ macro định nghĩa trước
    • Các macro có tiền tố scr là bản đơn giản dùng biến static
    • Các macro có tiền tố ccr là bản nâng cao có tính tái nhập
    • Tài liệu chi tiết nằm trong phần chú thích của file header
  • Visual C++ 6 xử lý macro __LINE__ một cách kỳ lạ trong thiết lập debug mặc định “Program Database for Edit and Continue”, nên không hợp với mẹo này
    • Muốn biên dịch chương trình dùng coroutine trên VC++ 6 thì phải tắt Edit and Continue
    • Trong thiết lập dự án, ở tab “C/C++”, mục “General”, phần “Debug info”, cần chọn tùy chọn khác thay vì “Program Database for Edit and Continue”
  • File header được phát hành theo giấy phép MIT

Tài liệu liên quan và sử dụng thực tế

  • The Art of Computer Programming của Donald Knuth, Volume 1, Section 1.4.2, trình bày coroutine ở dạng thuần túy
  • Trong phần thảo luận về Duff’s device của Tom Duff có nội dung cho thấy ông có thể đã độc lập nghĩ ra một mẹo coroutine tương tự, và ở bản cập nhật 2005-03-07 Tom Duff đã xác nhận điều này qua bình luận blog
  • Mã giao thức SSH của PuTTY thực sự sử dụng mẹo coroutine này
  • Trường hợp của PuTTY cho thấy đây là một mức hack C rất mạnh, hiếm gặp trong mã production nghiêm túc

1 bình luận

 
GN⁺ 2024-02-26
Ý kiến trên Hacker News
  • Khi cố giảm độ phức tạp API trong một dự án C, tôi đã nhiều lần quay lại trang này, và thấy phần giải thích về luồng điều khiển rất xuất sắc.
    Nó cũng giúp tôi suy nghĩ rõ hơn về việc lưu trạng thái trong và ngoài stack, cũng như sự khác biệt về tính dễ đọc giữa các cách tiếp cận.
    Kết luận hiện tại của tôi là tốt hơn hết nên để người dùng thư viện quyết định có dùng coroutine C hay không. Ví dụ, Mongoose(https://github.com/cesanta/mongoose) xử lý bất đồng bộ bằng callback sự kiện; với những thư viện kiểu này, bọc chúng bằng các nguyên thủy thread/task gốc của từng hệ thống sẽ dễ chịu hơn nhiều so với việc cố port sang một kiểu coroutine C đa nền tảng mang tính thần thoại, hay tệ hơn là std::thread

  • Coroutine là một khái niệm thực sự tuyệt vời, và tôi cũng thích xem các video về coroutine C++ ở CppCon, chủ yếu do phía Microsoft trình bày. Cái móc “abstraction chi phí âm” cũng khá hay.
    Vài năm trước, mấy người bạn ở Meta bắt đầu dùng coroutine C++, nhưng rồi nói với tôi rằng đó rốt cuộc là một sai lầm lớn. Họ phải đối mặt với bug trong phần triển khai của compiler, và chắc hẳn việc lần vết rất lộn xộn. Ở Google, tôi đang chờ những người giỏi xuất sắc tích hợp nó đúng cách vào google3/ thông báo rằng giờ đã có thể dùng được.
    Bài viết này giải thích chiến lược triển khai coroutine C bằng goto có cấu trúc dựa trên macro thông qua thiết bị Duff [1]. Điểm cốt lõi là có thể đặt câu lệnh case gần như ở bất kỳ đâu trong khối switch; sau đó bọc toàn bộ hàm trong một switch, dùng biến static để lưu vị trí trả về coroutine gần nhất, và gắn nhãn mỗi coReturn bằng một case.
    Bài viết về coroutine C của Sustrik cũng có thể đáng xem [2].
    [1] https://en.wikipedia.org/wiki/Duff%27s_device
    [2] https://250bpm.com/blog:48/index.html

    • Sau khi chuyển từ google3 sang fbcode trong vài năm gần đây, tôi thấy việc mã bất đồng bộ với co_yield, co_return, co_await rải rác khắp mã C++ vừa có ưu điểm vừa có nhược điểm.
      So với cách làm nội bộ của google3, ưu điểm là khi đọc mã, tính bất đồng bộ của từng phần hiện ra rất rõ. Một số lập trình viên ở Google từng không biết mô hình luồng ngoài mức từng nhánh, rồi về sau tạo ra các bug nghiêm trọng.
      Nhược điểm thì đơn giản hơn. Vì có quá nhiều đoạn mã “có thể bất đồng bộ”, theo thời gian toàn bộ hệ thống dần trở nên bất đồng bộ chỉ vì lập trình viên viết theo chế độ đó. Việc dùng spinlock hay mutex có nhường quyền nên được quyết định dựa trên kích thước vùng tới hạn và tình trạng luồng tại thời điểm đó, nhưng khi cố giữ tính dễ đọc và nhất quán, cả dự án rất dễ nghiêng hẳn về một phía.
      Tôi muốn biết thêm về các triển khai ngôn ngữ luồng có thể tránh đặt mặc định nghiêng về một bên, thay vào đó dùng profile của lần chạy trước để tối ưu lần chạy sau tốt hơn mà không cần sửa mã hay tạo bug.
    • Một phương án khác là dùng tính năng labels as values của GCC. Bạn có thể lấy địa chỉ của một nhãn rồi nhảy về đó sau này. Tôi đã đóng góp đoạn mã hiện nằm trong lc-addrlabels.h từ năm 2005.
      Tôi cũng dùng tính năng local labels của GCC để tránh hoàn toàn việc dùng __LINE__, và thậm chí có thể đặt nhiều coReturn trên cùng một dòng.
    • Có lẽ đúng là Duff đã nhận ra có thể đặt câu lệnh case gần như ở bất kỳ đâu trong khối switch, nhưng bản thân khả năng đó gần như chắc chắn là một tính năng được chủ ý thiết kế.
      Như phần cuối bài viết có nói, Duff cũng nhận ra rằng có thể xây coroutine trên đó, nhưng ông xem ý tưởng ấy là “ghê tởm”.
      Nếu coi switch trong C như một dạng pattern matching ít biểu đạt thì “fallthrough” rất dễ trông như bug, nhưng thực ra không phải vậy. Nó cùng họ với computed goto trong Fortran, và còn tiện hơn vì giá trị không cần liên tiếp, cũng không cần liệt kê toàn bộ nhãn ở đầu. Viết ra rồi tôi lại thấy nó có lẽ còn gần với computed COMEFROM hơn.
    • À, trình tiền xử lý C đúng là món quà cứ tiếp tục cho đi qua năm tháng :-(
  • Câu “các ngôn ngữ bậc cao được dùng rộng rãi không hỗ trợ coroutine” có thể đúng vào năm 2000, nhưng hiện nay nhiều ngôn ngữ như C++20, Lua, Python, Ruby đều hỗ trợ.

    • Python được tạo ra vào năm 1991, nên có lẽ từ khóa yield cũng ra đời khi đó hoặc không lâu sau.
      Cách cải tiến ở cuối bài là “thêm một con trỏ tới cấu trúc ngữ cảnh làm đối số hàm bổ sung, rồi khai báo mọi trạng thái cục bộ và biến trạng thái coroutine là phần tử của cấu trúc đó” trông giống như cách triển khai closure. Kiểu như biến phía được gọi thành lambda rồi dùng biến ngoài/ngữ cảnh/trạng thái để quyết định làm gì hoặc làm với giá trị nào; tôi không chắc mình hiểu như vậy có đúng không.
    • Xin lưu ý rằng Simula67 có coroutine. Dù không phải đầu tiên, nhưng nếu tôi nhớ không nhầm thì đó là ngôn ngữ lớn đầu tiên hỗ trợ coroutine.
  • Cách dùng switch không hẳn là quá hiếm, nhưng thường người ta có một con trỏ trạng thái được truyền vào hàm khởi tạo và hàm coroutine
    Tôi đã dùng cách này khá nhiều trong các dự án nhúng; một coroutine xử lý tăng/giảm tốc động cơ, còn một coroutine khác chỉ cho biết nên đi theo hướng nào. Tôi cũng từng dùng nó trong một thư viện mạng[1]. Ngay cả thư viện chuẩn cũng có các hàm coroutine như strtok()[2]
    Để nó trở nên có thể quản lý được thì không nhất thiết phải lao vào địa ngục macro, nhưng tôi chưa bao giờ thấy việc đọc luồng switch/case là thú vị cả
    [1]: https://github.com/REONTeam/libmobile/blob/master/relay.c#L3...
    [2]: https://manpages.debian.org/bookworm/manpages-dev/strtok.3.e...

  • Cũng có Simon Tatham's Portable Puzzle Collection của cùng tác giả
    https://www.chiark.greenend.org.uk/~sgtatham/puzzles/

  • Nếu điều này trông giống tà thuật C, thì bài viết của cùng tác giả về việc tạo các cấu trúc điều khiển tùy ý bằng macro cũng đáng đọc: https://www.chiark.greenend.org.uk/~sgtatham/mp/

    • Kiểu tiền tố gạch dưới vẫn thường dễ bị che khuất tên. Muốn tránh thì phải mangle tên khá xấu xí, và khác với các macro gần giống biểu thức/câu lệnh, với macro khối bên ngoài thì ngay cả các mẹo macro vệ sinh của GNU/C23 cũng không tránh được
  • Coroutine thì thú vị, nhưng trong mã thực tế cũng nên cân nhắc dùng thread thật. Bộ xử lý hiện đại có nhiều lõi, nhưng coroutine thường chỉ dùng một lõi duy nhất
    Đây đúng là một vấn đề thực tế. Cho đến gần đây, qemu vốn dùng coroutine rất nhiều vẫn đẩy phần lớn I/O thiết bị khối qua một luồng đơn, và điều này gây ra vấn đề hiệu năng. Kevin Wolf và những người khác đã sửa trong nhiều năm, nên qemu hiện đại đã dùng nhiều luồng cho I/O, và công việc này dự kiến sẽ có trong RHEL 9.4

    • Điểm liên hệ duy nhất giữa thread và coroutine là một số runtime ngôn ngữ đơn luồng chỉ cung cấp coroutine, nên đôi khi người ta dùng coroutine ở chỗ thread mới là lựa chọn tốt hơn
      Coroutine là một cách cấu trúc việc thực thi đơn luồng, và tự thân nó đã hữu ích. Mẫu producer-consumer trong bài là một ví dụ điển hình, và việc nối một stream vào parser không phải là thuật toán song song, nên thread không giúp ích gì cho việc viết nó
      Dùng mô hình đơn luồng cho công việc có thể chạy song song thì tất nhiên là kém hiệu quả, nhưng coroutine không phải là kiểu song song "nhà nghèo" mà là một cấu trúc điều khiển có ý nghĩa độc lập. Nó cũng có thể kết hợp hiệu quả với thread, giống như trong web server nơi event loop cùng dispatcher xâu chuỗi coroutine qua nhiều sự kiện blocking; và nếu runtime chạy một thread cho mỗi lõi để song song hóa thì việc điều phối giữa các thread có thể giảm xuống mức chỉ cần kiểm tra độ sâu hàng đợi công việc và chuyển request sang phía ít bận hơn
    • Việc coroutine thường chỉ dùng một lõi nhìn chung lại chính là hành vi mong muốn. Nếu đó là các tác vụ song song tách biệt, thì tự nhiên chúng sẽ xử lý các dữ liệu khác nhau
      Ý tưởng của coroutine là khi bạn có một số công việc cục bộ và dữ liệu đồng bộ, và việc biểu diễn nó dưới dạng đảo ngược — nơi hàm lặp qua một thứ gì đó rồi “đẩy” kết quả sang một consumer trừu tượng ở nơi khác — lại dễ hơn so với mô hình hàm nơi caller “kéo” vòng lặp bên trong ra
    • Có một điểm kết hợp khá hay giữa thread và coroutine. Đó là có một bộ lập lịch coroutine cho mỗi thread, và tạo một thread cho mỗi lõi
      Sau đó hầu như không di chuyển coroutine giữa các bộ lập lịch, và cũng hầu như không chia sẻ dữ liệu giữa các coroutine thuộc các bộ lập lịch khác nhau
      Coroutine cho phép một kiểu lập trình đồng thời thoải mái mà không cần khóa nào cả, nhờ cơ chế lập lịch hợp tác. Thông thường độ trễ lập lịch sẽ lớn hơn, nhưng bù lại chi phí atomic/lock biến mất, và timer cũng không liên tục ngắt thực thi để phục vụ lập lịch tiền nhiệm, nên throughput có thể khá cao
    • Lời khuyên kiểu “hãy cân nhắc thread thật” nhìn chung không tốt lắm. Ví dụ, nếu bạn chỉ muốn duyệt các nút cây trong một collection không phẳng, thì tôi không hiểu vì sao lại phải chạy thêm một thread riêng
    • Coroutine nhẹ và rất dễ đồng bộ hóa. Chúng cực kỳ phù hợp cho các phép tính tăng dần ở đơn vị nhỏ như iterator hay tokenizer. Có lẽ bạn đang nghĩ tới green thread
  • Phiên bản C++ của cách tiếp cận này: https://www.codeproject.com/Tips/29524/Generators-in-C
    Tôi cũng dùng nó trong Sciter của mình để phòng khi cần, và nó hoạt động khá tốt, khá tiện

  • Cách để đạt được điều này theo kiểu mô-đun và an toàn có lẽ là effect handler. Nó giống yield của Python, nhưng có thể trả về giá trị và không bị giới hạn trong lời gọi hàm, mà bắt scope như ngoại lệ. Nếu bạn chưa quen thì bài này là một động lực rất tốt
    Mỗi hàm được viết theo direct style có thể thực hiện một “effect” khi điều khiển cần chuyển sang nơi khác. Ở đây, c=getchar()emit(c) là những trường hợp như vậy
    Khi đó điều khiển sẽ chuyển tới effect handler, và trong trường hợp này có lẽ caller của hai hàm sẽ quyết định làm gì tiếp theo. Khi bộ giải nén xuất ra một ký tự, nó sẽ tiếp tục parser bằng cách đưa ký tự đó vào; chạy cho đến khi parser nói rằng cần thêm dữ liệu, rồi lại tiếp tục bộ giải nén
    Effect có thể được triển khai hiệu quả, đặc biệt là khi continuation bị giới hạn chỉ được gọi một lần. OCaml là trường hợp như vậy. Nó cho phép mã direct style đi cùng với an toàn kiểu/bộ nhớ, và cũng rất hữu ích trong môi trường đồng thời
    Ví dụ ở đây: https://effekt-lang.org/docs/casestudies/lexer

  • Hoàn toàn không đồng ý với đoạn kiểu như “mẹo này đương nhiên vi phạm mọi tiêu chuẩn lập trình… và tôi sẽ lập luận rằng chính các tiêu chuẩn đó mới sai”
    Không phải tiêu chuẩn lập trình sai khi từ chối đoạn mã này; đây đơn thuần chỉ là một mẹo nhỏ vui mắt. Kỹ nghệ phần mềm quy mô lớn là loại bỏ những điều bất ngờ và tạo ra mã mà cả người bị gọi dậy lúc 3 giờ sáng để debug trong tình trạng thiếu ngủ cũng có thể đọc được. Không thể kỳ vọng lập trình viên lúc nào cũng nhớ bốn quy tắc cơ bản đó
    Cũng khó mà bị thuyết phục bởi lập luận rằng việc giấu các thành phần quan trọng như switch, return, case bên trong các macro “làm rối mã” đã làm mờ cấu trúc cú pháp nhưng lại phơi bày cấu trúc thuật toán. Một chương trình tốt phải làm rõ cả cấu trúc cú pháp lẫn cấu trúc thuật toán, và cách này không đạt được điều đó. Tôi cho rằng cách Rust tạo ra máy trạng thái ngầm định trong các hàm async mới nên là hình mẫu ở đây

    • Chính thái độ cứ mãi “hạ thấp cho dễ hiểu” đến mức phải chiều theo mẫu số chung thấp nhất là nguyên nhân tạo ra phần lớn chất lượng phần mềm ngày nay — hoặc đúng hơn là sự thiếu vắng chất lượng đó. Nếu né tránh tri thức và đào tạo thì sớm muộn cũng phải trả giá
    • Sau khi chuyển từ C sang C++, tôi nhận ra có một khác biệt rất lớn giữa hai cộng đồng về việc thế nào là mã dễ đọc, tức là kỳ vọng người đọc trong tương lai cần phải hiểu những gì
      Trong thế giới C, ngay cả toán tử điều kiện ba ngôi cũng bị xem là quá kích thích, còn C99 vẫn bị coi như thứ văn minh mới. Trong thế giới C++, lý do duy nhất để ngăn người ta dùng template metaprogramming là vì với chuẩn đang dùng, có thể làm điều tương tự bằng constexpr