4 điểm bởi GN⁺ 2025-08-20 | 1 bình luận | Chia sẻ qua WhatsApp
  • Cách lập trình từ trái sang phải là cách mà chương trình luôn giữ trạng thái hợp lệ ngay khi nhập mã, nhờ đó sự hỗ trợ của công cụ như tự động hoàn thành trong trình soạn thảo được tối đa hóa
  • List comprehension của Python cản trở tính năng tự động hoàn thành do dùng biến chưa được khai báo và thiếu suy luận kiểu
  • Rust và JavaScript cho phép cấu thành chương trình một cách tự nhiên từ trái sang phải, nên việc dùng biến và khám phá phương thức trực quan hơn
  • Phong cách hàm của C và Python làm giảm hiệu quả trải nghiệm lập trình do tên hàm hoặc cấu trúc khó được khám phá
  • Với logic có độ phức tạp cao, mã được triển khai từ trái sang phải dễ đọc hơn, đồng thời tốt hơn về bảo trì và khả năng mở rộng

Lập trình từ trái sang phải

Mã phải hợp lệ ngay tại thời điểm được nhập


Giới hạn của list comprehension trong Python

  • Cú pháp list comprehension của Python words_on_lines = [line.split() for line in text.splitlines()] gây ra vấn đề vì phải truy cập một biến chưa được khai báo (line), khiến trình soạn thảo không thể cung cấp tự động hoàn thành hay suy luận kiểu một cách đúng đắn
  • Trong quá trình nhập mã từng phần
    • nếu nhập words_on_lines = [line.sp thì trình soạn thảo không biết kiểu của line, nên không thể gợi ý phương thức
    • cũng khó phát hiện các lỗi tiềm ẩn như gõ sai tên biến (lime chẳng hạn)
  • Muốn nhận được gợi ý đúng thì phải viết mã chưa hoàn chỉnh, và quá trình đó gây cảm giác không trực quan và bất tiện

Cấu thành từ trái sang phải trong Rust

  • Ví dụ Rust (let words_on_lines = text.lines().map(|line| line.split_whitespace());) cho thấy
    • cùng với khai báo hàm ẩn danh, biến (line) được xem là đã khai báo ngay tại thời điểm xuất hiện đầu tiên, nên có thể tự động hoàn thành và gợi ý phương thức ngay lập tức
    • thực tế, phương thức split_whitespace cũng được tìm thấy dễ dàng nhờ gợi ý tự động
  • Cách này giúp chương trình luôn giữ trạng thái hợp lệ dù chỉ là từng phần, nên IDE hoặc trình soạn thảo có thể hỗ trợ việc viết mã theo thời gian thực

Progressive Disclosure và tính khả dụng của API

  • Progressive Disclosure là nguyên lý thiết kế cho phép người dùng chỉ phải đối mặt với mức độ phức tạp đúng theo nhu cầu, và điều này cũng có thể áp dụng vào lập trình
    • Ví dụ: UX của trình xử lý văn bản chỉ hiện các tùy chọn liên quan khi thêm ảnh
  • Ngôn ngữ C thiếu kiểu hỗ trợ này
    • mọi hàm liên quan tới FILE *file không thể được khám phá qua file., nên phải ghi nhớ các mẫu tên hàm như fread, fclose, v.v., và rất khó phát hiện chức năng
    • ngược lại, trong một ngôn ngữ lý tưởng, các gợi ý phương thức thông qua file. sẽ giúp khám phá dần dần các chức năng liên quan một cách dễ dàng

Khác biệt trong khả năng khám phá hàm và phương thức

  • So sánh ví dụ map(len, text.split()) của Python với text.split(" ").map(word => word.length) của JavaScript
    • trong Python, khó đoán chính xác tên hàm như len, length, size, nên phải thử nhiều lần mới biết cái nào thực sự hoạt động
    • trong JavaScript, chỉ cần nhập .l sau word. là trình soạn thảo đã có thể gợi ý length và các phương thức khác, nên khả năng khám phá cao hơn
    • ngay cả các hàm bậc cao như map cũng thể hiện rõ tức thì giá trị trả về và kiểu dữ liệu

Logic càng phức tạp càng thấy rõ lợi thế của cách viết có cấu trúc

  • Với logic có độ phức tạp cao (đoạn mã Python dài lồng nhiều filter, lambda)
    • người đọc phải liên tục kiểm tra lại điểm bắt đầu và kết thúc của mã, và gặp suy giảm tính dễ đọc cùng khó khăn trong việc hiểu điều kiện hay ghép cặp dấu ngoặc
  • Ở phiên bản JavaScript của cùng logic đó, có thể đọc và hiểu tuần tự mã từ trên xuống dưới, từ trái sang phải

Nguyên tắc cốt lõi

Mã phải hợp lệ ở mọi thời điểm được nhập

  • Chỉ riêng text cũng vẫn là trạng thái hợp lệ của chương trình
  • Viết đến text.split(" ") vẫn hợp lệ, và khi tiếp tục nhập .map(word => word.length) thì toàn bộ vẫn luôn là trạng thái trung gian hợp lệ
  • Mẫu viết mã như vậy làm tăng khả năng hỗ trợ theo thời gian thực của trình soạn thảo, và trong môi trường REPL còn có thể kiểm tra kết quả ngay lập tức

Kết luận

  • Thiết kế ngôn ngữ và API nên hỗ trợ việc nhập mã một cách tự nhiên từ trái sang phải, đồng thời tạo ra một chương trình hợp lệ ở mỗi bước trung gian
  • Thiết kế API tốt là yếu tố cốt lõi để cải thiện trải nghiệm viết mã này

1 bình luận

 
GN⁺ 2025-08-20
Ý kiến trên Hacker News
  • Một trong những nhược điểm của SQL là câu truy vấn bắt đầu bằng SELECT thay vì FROM, nên khó nắm ngay được đang xử lý thực thể nào (bảng nào), đồng thời cũng cản trở việc để smart editor hỗ trợ viết truy vấn hiệu quả hơn, đi theo thứ tự FROM -> SELECT -> WHERE sẽ tự nhiên hơn, đặc biệt vì tên cột được định ở mệnh đề SELECT rồi mới được tham chiếu ở WHERE nên tôi càng nghĩ như vậy, thật ra thay vì SELECT * FROM table thì chỉ cần viết FROM table thôi cũng có thể xem là mệnh đề SELECT có thể được lược bỏ, nghe có thể như một ông già hay càm ràm vì than phiền chuyện này, nhưng đơn giản đây chỉ là điều tôi thấy tiếc
    • PSQL và PRQL thực sự dùng thứ tự truy vấn với FROM đứng trước, BigQuery cũng недавно được bổ sung cú pháp pipe/mũi tên, và cũng có extension của cộng đồng DuckDB nên rất đáng thử DuckDB - PSQL, DuckDB - PRQL
    • Lý do SQL viết như vậy là vì trong nền tảng đại số quan hệ, phép chiếu (Projection) luôn được viết trước, nên theo tiêu chuẩn thì không thể dùng bí danh cột trong WHERE, vì selection (WHERE) diễn ra trước projection (SELECT), nhân tiện MySQL 8 cũng có cú pháp TABLE <table> nên cũng đáng tham khảo
    • Thực tế, thứ tự xử lý nội bộ của phần lớn SQL engine là FROM -> WHERE -> SELECT, nên bí danh cột được định nghĩa trong SELECT có thể dùng ở GROUP BY, HAVING, ORDER BY nhưng không dùng được ở WHERE
    • Trong C#, DSL biên dịch xuống SQL (LINQ-to-SQL) cũng có cấu trúc FROM đứng trước, và khi viết các mệnh đề khác trong IDE, nhờ autocomplete nên có thể nhận gợi ý field ngay, vì vậy tôi thấy cấu trúc này rất tốt
    • Kusto, ngôn ngữ truy vấn phân tích dữ liệu của Azure, cũng có dạng tương tự dùng pipe Giới thiệu truy vấn Kusto, style LINQ của .NET cũng vậy, thành thật mà nói SQL cũng nên tích cực đưa vào các biến thể bắt đầu từ FROM hơn, và tôi không nghĩ đó là chuyện khó, chỉ là còn thiếu những nỗ lực để cải thiện usability
  • Tôi thấy khó hiểu vì sao Python lại được yêu thích đến vậy, chỉ cần có từ hai người cùng làm là ngôn ngữ này trở nên đau khổ vô hạn, những gì tác giả chỉ ra chỉ là phần nổi của tảng băng mà thôi
    • Tôi nghĩ cũng giống lý do mọi người không đổ xô sang các ngôn ngữ họ Lisp, tính chặt chẽ về mặt toán học không đồng nghĩa với tính dễ đọc, list/dict/set comprehensions của Python cũng giống như vòng lặp for có kiểu được xác định, ai cũng lo về chuyện type của Python quá lỏng, nhưng cú pháp duy nhất xác định rõ kiểu trả về (list comprehension) lại bị nhắm tới thì khá lạ, ở hầu hết ngôn ngữ khác bao gồm cả Rust cũng không theo thứ tự "from iter as var", ngoài ra so sánh cú pháp gọi hàm giữa các ngôn ngữ cũng khá thú vị (giống như Python cũng có functools.map)
    • Chỉ vì bạn không hiểu điều gì đó không có nghĩa điều đó trở thành một đức tính, chắc chắn có lý do nào đó khiến Python được yêu thích, dĩ nhiên nhược điểm là rất rõ, nhưng chỉ riêng điều đó thì không có ý nghĩa, phải so sánh tổng thể ưu và nhược điểm, và cũng phải so với các ngôn ngữ khác theo cách đó
    • Tôi cũng thích Python (miễn là đội nhỏ, chương trình ngắn và vòng đời ngắn), không có static type nên triển khai nhanh, nhưng nhờ hệ thống type đủ mạnh nên cũng không bị hỏng hoàn toàn, tôi nghĩ đó là lý do nó phổ biến trong data science, rất thuận tiện khi thăm dò khám phá, ngược lại với chương trình lớn hoặc được nhiều team bảo trì lâu dài thì rõ ràng có nhược điểm, cuối cùng không có ngôn ngữ vạn năng, ít nhất cần hai loại là "ngôn ngữ để thử nghiệm và chạy nhanh (Soft)" và "ngôn ngữ phù hợp để quản lý lâu dài (Hard)"
    • Trước đây tôi cũng hoàn toàn đồng ý với ý kiến trên, nhưng nhờ type annotation và type checking mà việc cộng tác với mã Python do người khác viết đã dễ hơn rất nhiều, tôi vẫn chưa nghĩ nó phù hợp tới mức cho dự án quy mô lớn, nhưng khi có type thì Python đã trở thành scripting language tôi thích nhất
    • Tôi cũng tránh dùng những thứ như list comprehension trong shared codebase và hướng tới style Python thật đơn giản, đây là ngôn ngữ nổi tiếng với khẩu hiệu "chỉ nên có một cách", nhưng trên thực tế lại có quá nhiều cách cùng tồn tại, list comprehension cá nhân tôi thấy vui và thỏa mãn, nhưng nếu mọi người phải cùng đi một con đường thì tôi nghĩ cú pháp này không nên tồn tại
  • Tôi đồng cảm với quan điểm rằng "chương trình phải hợp lệ ngay khi đang gõ", nhưng ngoài đời chúng ta không phải lúc nào cũng viết code theo thứ tự từ trái sang phải, từng dòng một, cũng thường có lúc viết phần khác trước hoặc khai báo biến sau, ví dụ có khi dùng biến trước rồi rất lâu sau mới khai báo
    • Xét việc code được viết một lần nhưng được đọc hàng chục, hàng trăm lần, tôi nghĩ code có thể đọc tuần tự sẽ dễ đọc hơn nhiều so với code đòi hỏi phải nhảy qua lại
    • Thật ra cuộc thảo luận này hơi lệch nhẹ khỏi trọng tâm của bài viết, nhưng đó là một góc nhìn thú vị
    • Hoàn toàn đồng ý, chỉ khi tạo file mới thì tôi mới viết code tuần tự từ đầu, khi thêm field thì không cần phải quay về từ định nghĩa class mà thường tạo ngay code sử dụng field đó trước, lúc cải thiện câu điều kiện cũng thường có những lúc tạm thời rơi vào trạng thái không hợp lệ (báo lỗi)
    • Tôi cũng đồng ý với ý này, nhưng có một nguyên tắc quan trọng liên quan là việc "chặn luôn cả compile chỉ vì bạn còn chưa viết xong" là quá tay, lỗi nên là non-blocking, nhưng một số ngôn ngữ lại chặn hẳn code chưa hoàn chỉnh (ví dụ: biến không dùng, thiếu return, v.v.)
    • Đôi khi tôi thấy hơi khó chịu vì IDE dường như không hiểu đúng thứ tự mà tôi thực sự đang viết code
  • Trong một số IDE có tính năng template code, nhập viết tắt là nó mở rộng thành cấu trúc code rồi dùng tab để lần lượt điền các placeholder, lúc này thứ tự di chuyển bằng tab không nhất thiết phải từ trái sang phải, ví dụ có thể là {3} for {2} in {1}, những công cụ như vậy tạo ra điểm thỏa hiệp giữa "cú pháp dễ đọc" và "cú pháp dễ gõ", tôi nghiêng về phía ưu tiên cú pháp dễ đọc kể cả khi phải tận dụng tooling, và tôi không nghĩ nhất thiết phải cố chấp với cấu trúc "for-in"
  • Dạo này trên Hacker News dường như có sự đồng thuận rằng Python đã bỏ lỡ toán tử pipe, khi chuyển từ Mathematica sang R tôi đã sớm nhận ra giá trị của pipe, khi viết code biến đổi dữ liệu theo từng bước trong data science nó thực sự trực quan và dễ đọc, Python được dùng trong nhiều lĩnh vực nhưng tôi tò mò không biết ngoài phân tích dữ liệu thì pipe có còn lợi thế trong bối cảnh khác không, tôi đang cố hiểu vì sao Python không đưa pipe vào
    • Nếu đi thêm một bước từ toán tử pipe, tôi nghĩ reverse assignment cũng đáng thử, thay vì gán kết quả cho biến kiểu 'let foo = ...' thì tôi cũng muốn thử dạng như '... =: foo'
    • Toán tử pipe của R (đặc biệt là tidyverse R) là "killer app" quan trọng nhất đối với tôi, tôi chưa thấy ngôn ngữ nào khiến công việc dữ liệu dễ và vui đến vậy, ví dụ thay vì lồng công thức kiểu bake(divide(add(knead(mix(flour, water, sugar, butter)),eggs),12),450,12) cho công thức bánh quy, dùng pipe như mix(flour, water, sugar, butter) %>% knead() %>% add(eggs) %>% divide(12) %>% bake(temp=450, minutes=12) thì dễ hơn và nhìn đẹp hơn rất nhiều
    • Trong pandas của Python, nếu dùng cú pháp pipe thì sẽ thành như dưới đây
      result = (df
       .pipe(fun1, arg1=1)
       .pipe(fun2, arg2=2)
      )
      
      Còn trong R thì
      result <- df |>
       fun1(., arg1=1) |>
       fun2(., arg2=2)
      
      Cả hai đều khá dễ đọc, nhưng điểm mạnh của R là pipe hoạt động tốt hơn cả bên ngoài dataframe
  • Cuộc tranh luận này gần như là một cuộc chiến tôn giáo, giống như tranh luận ngôn ngữ FP (hàm) vs OOP (hướng đối tượng), hay vim và emacs, trong vim thì toán tử đứng trước, còn emacs thì thứ tự chọn trước, những ngôn ngữ "đọc như tiếng Anh" thường có cấu trúc động từ đứng trước (Lisp/Scheme là vậy), ngược lại những ngôn ngữ như tiếng Đức hay tiếng Tamil nơi động từ đứng cuối thì hợp với style OOP hơn (danh từ trước), ví dụ trong tiếng Tamil là "water drink" còn tiếng Anh là "drink water", nên có thể có người thấy vim tự nhiên hơn, thay vì nói style nào tốt hơn thì có lẽ chúng được tạo ra để hợp với công cụ/tính cách của từng người, và ngày nay với language model thì hầu như chuyện gì cũng làm được
    • Về câu "nếu thiết kế để đọc như tiếng Anh thì động từ phải đứng trước", điều đó đúng với imperative language, nhưng nếu là declarative language mà muốn đọc như tiếng Anh thì chủ ngữ phải đứng trước
    • Về câu "tiếng Đức lúc nào cũng để động từ ở cuối à", thực ra trong câu đơn giản thì động từ đứng thứ hai ("I drink water" → "Ich trinke Wasser"), chứ không phải luôn nằm hẳn ở cuối câu
    • Về chuyện vim có toán tử đứng trước, thực ra Kakoune lại làm ngược lại, và tôi thấy cách này hợp lý hơn nhiều Giải thích về Kakoune
  • Mặt khác, cú pháp "from some_library import child_module" của Python lại rất trực quan, còn trong JS thì cấu trúc như "import { asYetUnknownModule } from SomeLibrary" khiến tôi thấy kém trực quan hơn nhiều
    • Trong JS, nếu dùng namespace import như dưới đây
      import * as someLibrary from "some-library"
      someLibrary.someFunction()
      
      thì thực tế IDE autocomplete hoạt động rất tốt nên tôi thấy khá ổn Giải thích namespace import trên MDN
    • Tôi thấy hơi khó hiểu vì sao mọi người lại ám ảnh với từ khóa "from", đơn giản là
      import SomeLibrary {
        asYetUnknownModule
      }
      
      viết như vậy là được mà
  • ReScript chính vì lý do này đã đổi API từ data-last sang data-first, nhờ khả năng type inference tuyệt vời nên gần như lúc nào cũng có autocomplete chính xác và đúng kiểu, trải nghiệm phát triển rất tốt, dĩ nhiên nếu khai báo hàm mà không có tham chiếu nào (nên không thể biết type) thì vẫn có vấn đề, nhưng chỉ cần thêm type hoặc gọi trước là giải quyết được, cũng xin giới thiệu bài blog liên quan So sánh data-first và data-last
  • Tôi đã liên tục nhấn mạnh góc nhìn này, và nó cũng liên hệ với lý do Ruby luôn cho tôi cảm giác dễ hơn nhiều, đặc biệt là tôi chưa từng dùng sâu cả Python lẫn Ruby ở mức production, nhưng tôi thật sự không hiểu vì sao Python lại được cài đặt rộng rãi và bắt đầu được dùng nhiều đến vậy, Ruby không phải không có nhược điểm, nhưng có vẻ không nhiều người viết script phải đối mặt với những thay đổi phức tạp mà Python đã trải qua, ít nhất Ruby cũng không có những vụ xung đột version lớn trong suốt 10 năm qua
  • Nhìn chung tôi hoàn toàn đồng ý với những điểm bài viết chỉ ra, tôi nghĩ cấu trúc có ngữ cảnh ở phía trước và được đọc từ trái sang phải sẽ hợp với LLM hoặc autocomplete hơn, tuy vậy thay vì viết code ví dụ kiểu len(list(filter(lambda line: all([abs(x) >= 1 and abs(x) <= 3 for x in line]) and (all([x > 0 for x in line]) or all([x < 0 for x in line])), diffs))), thì dùng NumPy array sẽ tốt hơn vì không cần tạo thêm list trong bộ nhớ và cũng phù hợp hơn để xử lý cả dòng cùng lúc, ví dụ
    sum(1 for line in diffs
      if ((np.abs(line) >= 1) & (np.abs(line) <= 3)).all()
        and ((line > 0).all() or (line < 0).all()))
    
    cách này phản ánh "từ trái sang phải" tốt hơn nhiều
    • Phiên bản numpy vẫn hơi có vẻ bí hiểm ("line > 0" thì ổn nhưng quy tắc broadcasting có thể trở nên phức tạp), các collection API trong những ngôn ngữ chặt về type như ví dụ JavaScript, C#, Java, Scala mà tác giả đưa ra trông gọn gàng hơn, gu của tôi là Kotlin vì có thể viết như dưới đây
      diffs.countIf { line -> 
        line.all { abs(it) in 1..3 } and ( 
          line.all { it > 0} or
          line.all { it < 0}
        )
      }