4 điểm bởi GN⁺ 2023-09-18 | 1 bình luận | Chia sẻ qua WhatsApp
  • Java 21, phát hành ngày 19/9/2023, đưa cách biểu đạt pattern mang tính hàm gần với Kotlin·Rust·C# vào trong Java thông qua record patternsswitch pattern matching
  • Các thay đổi tích lũy từ switch expressions của Java 14, records và instanceof pattern matching của Java 16, cùng sealed classes của Java 17 đã kết nối lại trong Java 21 để tạo nền tảng xử lý kiểu dữ liệu đại số
  • records khiến việc tách dữ liệu ổn định hơn nhờ các ràng buộc như final, tham chiếu bất biến và getter theo khuôn mẫu; record pattern cho phép lấy trực tiếp dữ liệu lồng nhau ngay trong switch
  • sealed classes/interfaces chỉ mở cho các kiểu con được cho phép, giúp tạo mô hình gần với sum type; khi dùng sealed interface cùng record, có thể giới hạn các biến thể như RGB·CMYK·YUV·HSL
  • switch của Java 21 còn hỗ trợ case null và guard when, nhưng accessor của record viết sai hoặc ngoại lệ phát sinh khi chạy guard có thể dẫn đến java.lang.MatchException

Pattern matching được ổn định trong Java 21

  • Java 21 được phát hành ngày 19/9/2023 và hỗ trợ record patterns trong switch block và switch expression
  • Cú pháp này được xem là bước ngoặt giúp Java cũng có thể biểu đạt các pattern của lập trình hàm theo cách tương tự Kotlin, Rust, C#
  • Những thay đổi cú pháp lớn ở các phiên bản Java gần đây đã dẫn tới pattern matching của Java 21
    • Java 14: ổn định hóa switch expressions
    • Java 16: ổn định hóa records, instanceof pattern matching
    • Java 17: ổn định hóa sealed classes
    • Java 21: ổn định hóa record patterns, switch pattern matching
  • Với nhóm thay đổi này, Java giờ có thể xử lý kiểu dữ liệu đại số (algebraic data types) và cách dùng mang tính thành ngữ của chúng, vốn trước đây rất khó biểu đạt

Những khái niệm tối thiểu cần biết trong lý thuyết kiểu

  • Để hiểu các tính năng của Java 21, cần nắm một vài khái niệm của lý thuyết kiểu
  • bottom/empty type biểu thị tập giá trị không thể được tính ra; trong ngôn ngữ lập trình thông thường, đây thường là tập rỗng
    • Nothing của Kotlin có constructor là private nên không thể tồn tại instance
    • Void của Java cũng có constructor private nhưng vẫn có thể chứa null, nên khó xem là bottom type đúng nghĩa
    • primitive void của Java không thể dùng làm kiểu biến, nên ở điểm này hành xử gần hơn
  • top type là tập phổ quát biểu thị mọi giá trị của mọi kiểu
    • Trong Kotlin, Any đóng vai trò này
    • Object của Java tách biệt với primitive trong mô hình đối tượng, nên khó xem là tương đương hoàn toàn với top type ở các ngôn ngữ khác
  • unit type là kiểu chỉ có đúng một giá trị
    • void của Java có thể được xem như unit type ở giá trị trả về của method, nhưng không thể truyền làm kiểu tham số
    • Unit của Kotlin được định nghĩa bằng object và cũng có thể dùng làm tham số method
  • boolean type có hai giá trị truefalse; về lý thuyết có thể biểu diễn bằng nullable unit type, nhưng không thực tế

Product type và Java records

  • product type là kiểu kết hợp từ hai hay nhiều kiểu thành phần, và số lượng kiểu thành phần được gọi là arity hoặc degree
  • struct của C là một ví dụ về product type
    • Các kiểu thành phần như int, char *, double, int có thể lặp lại
    • Với kiểu lặp lại, có thể phân biệt chúng như ordered pair đi kèm tên field
  • tuple trong Python hay Rust cũng có thể xem là product type; khi đó, chỉ số đóng vai trò tên của thành phần
  • record class được ổn định trong Java 16 là một ví dụ tốt của product type
    • field trong record là final, và record không thể bị kế thừa
    • trạng thái của record được thiết lập tại thời điểm tạo và được giữ nguyên sau đó
    • tuy nhiên, nếu đặt mutable data type bên trong record thì không thể đảm bảo tính bất biến của chính nội dung đó
  • Java class thông thường có thể trộn lẫn trạng thái public/private, trạng thái ẩn phát sinh từ kế thừa, mutable/static field, và getter phi chuẩn, nên khó khái quát các thành phần của nó
  • records đảm bảo một cấu trúc đủ ổn định để các tính năng ngôn ngữ như pattern matching hoạt động tin cậy nhờ các ràng buộc sau
    • record ngầm định là final class và không thể bị kế thừa
    • không thể extend lớp nào ngoài java.lang.Record
    • record component không thể gắn visibility modifier
    • tham chiếu tới component luôn là final và được xem là bất biến
    • getter mặc định dùng nguyên tên field, nên getter của field a sẽ là a()
    • backing field ngầm định là private và được truy cập qua getter

Phân rã dữ liệu lồng nhau bằng record pattern

  • switch pattern của Java 21 cho phép phân rã dữ liệu record lồng nhau mà không cần lặp đi lặp lại việc kiểm tra instanceof và ép kiểu tường minh
  • Trong ví dụ, dùng record A(Record inner), record B(char b), record SomeOtherRecord()
    • Cách cũ là sau if (r instanceof A) lại ép kiểu, rồi tiếp tục lặp instanceof và ép kiểu với giá trị bên trong
    • switch pattern cho phép trích xuất trực tiếp giá trị lồng nhau như case A(B(char a)) -> String.valueOf(a)
  • switch block có cấu trúc rõ ràng hơn if-else ladder, và phù hợp để lấy nhanh dữ liệu lồng sâu
  • Để chạy trực tiếp trên Java 21, có thể đặt code vào main.java và dùng lệnh sau
java --enable-preview --source 21 main.java

Sum type và sealed classes/interfaces

  • Khi cần biểu diễn số lựa chọn hữu hạn, có thể dùng enum của Java, nhưng các biểu diễn màu có cấu trúc dữ liệu khác nhau như RGB·HSL·YUV·CMYK sẽ khá bất tiện nếu chỉ dùng enum
  • Có thể tạo đa hình dựa trên kế thừa bằng abstract class Color cùng các class RGB, CMYK, YUV, HSL, nhưng class hierarchy thông thường là mở
    • Người dùng thư viện có thể tạo class mới như RYB để kế thừa Color
    • Nếu API không chủ đích cho phép mở rộng, biến thể mới có thể gây crash hoặc bug tinh vi ở đoạn code rất xa
  • sealed classes được dùng để biểu đạt khái niệm sum type trong Java
    • sum type là kiểu mà tại một thời điểm chỉ có thể là một trong các thành phần của nó
    • còn được gọi là tagged union type
  • Khi dùng modifier sealed và mệnh đề permits, chỉ các class cụ thể mới được phép kế thừa
public sealed class Color permits RGB, CMYK, YUV, HSL {
}
  • Trong sealed class hierarchy, các lớp kế thừa trực tiếp hoặc gián tiếp phải có một trong ba modifier sealed, non-sealed, final; nếu không sẽ phát sinh compile error
    • sealed: chỉ các kiểu được nêu trong permits mới có thể kế thừa
    • non-sealed: có thể kế thừa như class thông thường
    • final: là leaf của cây kế thừa và không thể mở rộng thêm

Cách kết hợp sealed interface và record

  • destructuring trong switch pattern matching hoạt động với record, nhưng record không thể kế thừa lớp nào ngoài Record
  • Cách giải quyết là dùng sealed interface
    • sealed interface hoạt động tương tự sealed class
    • record và enum cũng có thể implement sealed interface
  • Trong ví dụ, Color được tạo thành sealed interface, còn RGB, CMYK, YUV, HSL được triển khai bằng record
public sealed interface Color permits RGB, CMYK, YUV, HSL {
    String getDescription();
}

record RGB(int red, int green, int blue) implements Color {
    public String getDescription() {
        return "RGB Color: (" + red + ", " + green + ", " + blue + ")";
    }
}
  • Sau đó có thể trích xuất trực tiếp giá trị của từng record trong switch
switch (color) {
  case RGB(int red, int green, int blue) -> {
  }
  case CMYK(double cyan, double magenta, double yellow, double black) -> {
  }
  case YUV(int y, int u, int v) -> {
  }
  case HSL hsl -> {
    System.out.println(hsl.getDescription());
  }
  case null -> {
    System.out.println("How did color become null?!");
  }
}
  • Java 21 cho phép xử lý case null trong switch block và expression, nên không cần kiểm tra null riêng trước switch
  • Nếu Color là một sealed type, Java có thể biết liệu mọi case đã được xử lý hay chưa, nhờ đó có thể viết exhaustive switch mà không cần default case

Guard clause và when

  • Java 21 hỗ trợ guard clause để gắn điều kiện bổ sung vào từng switch arm
  • guard clause tích hợp điều kiện ngay trong case label bằng từ khóa when
switch (color) {
  case RGB(int red, int green, int blue) when red > 200 -> {
    System.out.println("Very red.");
  }
  case RGB rgb when rgb.green > 100 -> {
    System.out.println("Sort of green...");
  }
  case RGB rgb -> {
    System.out.println("Not that red...");
  }
}
  • Trước đây, phải đặt lại if (red > 200) bên trong phần thân của case RGB(...)
  • Java sẽ khớp một cách eager với case đầu tiên đánh giá là true, nên cần đặt case cụ thể hơn ở trước và case ít cụ thể hơn ở sau
  • Sau một RGB case có guard, vẫn cần case RGB rgb thông thường để giữ tính exhaustive

Khi nào xảy ra MatchException

  • Với pattern matching của Java 21, java.lang.MatchException cũng là một ngoại lệ cần lưu ý
  • Nếu record accessor ném ngoại lệ, switch pattern sẽ thất bại và có thể phát sinh MatchException
record R(int i) {
    public int i() {
        return i / 0;
    }
}

static void exampleAnR(R r) {
    switch(r) {
        case R(var i): System.out.println(i);
    }
}
  • Trong ví dụ trên, accessor i() ném ArithmeticException, nên switch block sẽ ném MatchException
  • Theo JEP 441, record accessor luôn ném ngoại lệ là trường hợp rất bất thường, và việc exhaustive pattern switch ném MatchException cũng rất hiếm gặp
  • Ngay cả với exhaustive switch, ngoại lệ vẫn có thể xảy ra nếu selector không khớp với bất kỳ variant nào được chỉ định
    • JEP 441 giải thích trường hợp exhaustive switch với enum thất bại khi khớp là do enum class đã bị thay đổi sau khi switch được biên dịch
  • Ngoại lệ phát sinh khi chạy guard clause cũng có thể dẫn đến MatchException
static void example(Object obj) {
    switch (obj) {
        case R r when (r.i / 0 == 1): System.out.println("It's an R!");
        default: break;
    }
}

Phần còn lại

  • Khi kết hợp records, sealed types, switch pattern matching và guard clause trong Java 21, có thể đưa các khối xây dựng của lập trình hàm vào code Java
  • Một số chủ đề như cách generics tương tác với switch patterns không được đề cập ở đây
  • Bài viết tiếp theo sẽ nói về các quirks và ví dụ thực tế có thể dùng để cải thiện cách viết code Java

1 bình luận

 
GN⁺ 2023-09-18
Ý kiến trên Hacker News
  • Tính năng lớn nhất của Java 21 là việc phát hành virtual threads: https://openjdk.org/jeps/444
    Không hiểu sao bài viết lại bỏ sót. Nếu có tính năng nào có thể kéo các lập trình viên Go hiện tại sang Java thì có thể chính là nó, và có lẽ nó cũng sẽ thuyết phục được những người vốn không thích các mẫu đồng thời kiểu reactive.

    • Tôi không nghĩ các lập trình viên Go hiện tại sẽ quay lại Java. Tôi đã làm Java 10 năm rồi chuyển sang Go, và không có ý định quay lại.
      Ứng dụng và thư viện Java quá khó suy luận và hiểu so với Go vì kế thừa, đóng gói, hướng đối tượng, công cụ build, v.v.
      Go đơn giản, dễ hiểu, dễ đọc và dễ bảo trì. Cách đóng gói giống với việc sắp xếp file trên máy tính vào một thư mục duy nhất, và công cụ cũng được tích hợp sẵn trong ngôn ngữ. Nó cũng không tạo cảm giác phải có IDE như IntelliJ thì mới tạm dùng được.
      Có thể bây giờ đã khác, nhưng hầu hết các thư viện Java tôi thấy gần đây vẫn trông như vậy.
    • Vì Java 21 đã có virtual threads, tôi đang mong chờ bản phát hành JRuby tiếp theo có chúng. Charles Nutter đã demo tác động của chúng lên fiber của Ruby trong một bài trình bày JRuby hồi tháng 8, và tác động khá lớn.
      Có nhiều điều tôi thích ở JVM và hệ công cụ của nó, nhưng giờ viết mã Java thì tôi không thích lắm. JRuby phần nào đem lại ưu điểm của cả hai phía.
      Bài trình bày ở đây, và phần demo virtual threads bắt đầu khoảng phút 45:
      https://youtu.be/pzm6I4liJlg?si=GtxQ4MThEaNDfC67
    • Tôi không chắc các lập trình viên Golang hiện tại sẽ chuyển sang Java vì tính năng này. Nhưng xét về định hướng, tôi thắc mắc vì sao cộng đồng Go lại có ít container đồng thời đến vậy, trong khi cộng đồng Java có rất nhiều.
      Ngay cả sync.Map cũng không phải là thứ tổng quát như ConcurrentMap của Java, mà được chuyên biệt cho hai trường hợp sử dụng cụ thể. Java có tập hợp đồng thời, hàng đợi, barrier, phaser, fork-join pool, v.v. Dù có goroutine, những container như vậy vẫn đủ hữu ích; ít nhất thì fork-join không phải thứ quá tầm thường để triển khai. Dùng mutex ở khắp nơi tạo cảm giác quá cấp thấp.
      Tôi biết có các triển khai bên thứ ba, nhưng đồng thời quá khó để làm cho đúng, nên nếu không đạt mức trưởng thành và được nhiều người dùng lẫn nhà phát triển hậu thuẫn như JCTools của Java hay Google Guava, tôi sẽ ngần ngại khi dùng một gói bên thứ ba.
    • Sự đối lập giữa Executor.newVirtualThreadPerTaskExecutorgo cho thấy rất rõ lý do cốt lõi khiến các lập trình viên Go sẽ không chuyển sang Java.
      Nói chính xác hơn, thực ra nó gần với try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { executor.submit(...) } thay vì go.
    • Java 21 cũng cung cấp structured concurrency dưới dạng preview (https://openjdk.org/jeps/453). Nó dùng triển khai virtual threads, và chỉ nhìn ví dụ thôi cũng đã thấy khá ổn, đồng thời giảm bớt nhiều nỗi đau khi xử lý đồng thời dựa trên thread.
  • Tôi nghĩ tiêu đề bài blog được chọn chưa hay. Phụ đề ẩn là "Algebraic data types in Java", và câu này mô tả nội dung phù hợp hơn nhiều. Tiêu đề tốt hơn lẽ ra là Algebraic data types in Java 21.
    Có lẽ vì tiêu đề mà khá nhiều bình luận ở đây đi chệch chủ đề. Tôi muốn thấy nhiều hơn về kiểu dữ liệu đại số, ưu nhược điểm của triển khai trong Java, và so sánh kỹ thuật với các ngôn ngữ khác.

    • Tôi nghĩ sẽ tốt nếu có một ngôn ngữ phổ biến hơn dùng nhiều kiểu dữ liệu đại số, nhưng tôi không chắc mình thật sự muốn thấy kiểu dữ liệu đại số xuất hiện trong Java.
      Mã Java hiện có sẽ không biến mất, nên nếu loại mã này bị trộn lẫn ngẫu nhiên vào đó thì liệu có thật sự tốt hơn không?
    • Ban đầu tôi đã đặt tiêu đề như vậy, nhưng đổi vào phút chót, và kết quả là có vẻ đã làm lệch hướng bài viết.
  • Tính năng sealed classes được giải thích ở đây có cảm giác như sai hoàn toàn
    Lập luận là nếu có một interface thông thường thì bất kỳ ai cũng có thể tạo lớp mới triển khai nó, và đoạn mã như if (x instanceof Foo) { ... } else if (x instanceof Bar) { ... } sẽ hỏng lúc runtime nếu ai đó thêm một lớp mới. Vì vậy, với tính năng sealed interface mới, nếu không ai có thể tạo lớp mới triển khai interface đó thì câu lệnh if sẽ không bị hỏng
    Nhưng chẳng phải lập trình hướng đối tượng đã nghĩ đến và giải quyết vấn đề này rồi sao? Tôi biết hiện nay hướng đối tượng không còn thời thượng như trước, nhưng Java là ngôn ngữ hướng đối tượng
    Cách giải quyết là thêm một method vào interface và bắt tất cả các lớp triển khai method đó. Khi đó thay vì liệt kê mọi lựa chọn trong một câu lệnh if/switch khổng lồ, ta chỉ cần gọi method
    Cách này tốt hơn việc chặn không cho mở rộng code; ngược lại, nó cho phép mở rộng. Người viết implementation mới chỉ cần triển khai method đó, và vì compiler bắt buộc nên cũng không thể vô tình bỏ sót
    Ví dụ về không gian màu trong bài (RGB, CMYK, v.v.) là một phản ví dụ rất hay. Bạn viết code dùng không gian màu, nhưng người dùng hoặc khách hàng có thể cần dùng một không gian màu lạ và hiếm mà bạn chưa nghĩ tới. Tôi không muốn tạo ra code không thể mở rộng vì cấu trúc đó chỉ hỗ trợ những không gian màu được liệt kê trong một câu lệnh if/switch khổng lồ

    • Cách giải quyết là thêm method vào interface sẽ trở nên rắc rối nếu bạn không thể biết trước mọi phép toán sẽ cần trong tương lai
      Sealed classes giải quyết vấn đề này. Nhưng đổi lại nó tạo ra vấn đề mới: “nếu cần thêm các lớp mở rộng và không thể biết trước tất cả chúng thì sao?” Cuối cùng câu hỏi là có cách nào đạt được cả hai hay không
      Vấn đề này được gọi là expression problem [1]
      Có những ngôn ngữ kiểu tĩnh giải được expression problem, và Java cũng là một trong số đó [2]. Tuy nhiên cách làm trong Java vẫn rất phức tạp và bất tiện nên hầu như không được dùng. Nếu muốn ở lại với Haskell hoặc thế giới JVM, Scala làm phần này tốt hơn nhiều
      [1] https://en.wikipedia.org/wiki/Expression_problem
      [2] https://koerbitz.me/posts/Solving-the-Expression-Problem-in-...
    • Cách dùng sealed classes cũng cho phép bất kỳ ai mở rộng code, nhưng mở rộng theo một chiều khác so với cách dùng method trong interface
      Cách dùng method trong interface và virtual call rất kém linh hoạt khi bạn muốn thêm một phép toán mới thay vì thêm lớp mới. Chỉ cần thêm một phép toán mới thôi cũng phải đi đến mọi implementation để thêm method mới, và có thể làm hỏng các implementation mà bạn không có quyền truy cập. Các method không liên quan với nhau cũng phải được định nghĩa trong cùng một lớp, khiến khả năng đọc code giảm mạnh; virtual call cũng không miễn phí nên còn ảnh hưởng đến hiệu năng
      Trong trường hợp này sealed class mở rộng tốt hơn nhiều. Chỉ cần thêm một switch mới ở một nơi là xong, và cũng không phá vỡ tương thích ngược
      Đây chính là expression problem nổi tiếng
      https://pkolaczk.github.io/in-defense-of-switch/
    • Tôi hiểu khuyến nghị dùng polymorphic dispatch thay cho instanceof, và cũng đã thấy Bob Martin nói rất dài về chuyện đó, nhưng tôi không đồng ý
      Để làm polymorphic dispatch kiểu này, đối tượng phải tự xử lý nhiều mối quan tâm khác nhau
      Trong một trò chơi điện tử, Car có thể có .render(), .collide(), .playSound(). Sau này thêm Dog thì chỉ cần triển khai ba method này, không cần sửa hay biên dịch lại Renderer, PhysicsEngine, SoundEngine. Lập trình viên khác cũng có thể thêm entity mà không đưa bug vào đoạn code quý giá của tôi. Nghe có vẻ tốt
      Nhưng giờ CarDog đều phải biết về đồ họa, vật lý và âm thanh. Và entity không tồn tại cô lập. Xe và chó phải được render theo đúng thứ tự và có thể che khuất nhau. Va chạm cũng phải kiểm tra lẫn nhau. Như tôi từng gặp trong một game jam thực tế, cũng có tình huống người phụ trách âm thanh phải chui vào mọi đối tượng để thêm hành vi âm thanh
      Thay vào đó, khi nghĩ về vật lý thì làm việc trong Physics.collideAll() và xử lý đặc biệt bằng instanceof nếu cần; khi nghĩ về đồ họa thì làm việc trong Graphics.renderAll() sẽ tốt hơn nhiều
      Trong phát triển web backend Java hằng ngày cũng tương tự. Khi quyết định cách chuyển Java object thành HTTP response trong REST controller, tốt hơn là nhìn toàn bộ trong một method và map {instanceof Forbidden} thành 403, {instanceof NotFound} thành 404. Tôi không muốn nhét getCode() hay nội dung riêng cho REST vào chính các class Java
    • Cho phép mở rộng không phải lúc nào cũng hợp lý. Stringfinal có lý do của nó, và thậm chí có thể cho rằng nên đặt final làm mặc định, chỉ những lớp cho phép subclass mới cần được ghi rõ là open
      Trong lập trình hàm, ví dụ điển hình của sum type là list. Nó chỉ có Element(T head, List tail)Nil(). Không có lý do gì để mở rộng nó, và trên thực tế nếu mở rộng thì có thể trở thành code sai khi kết hợp với mọi hàm xử lý list
      Ngoài ra, visitor pattern tương tự pattern matching rất dài dòng và dựa vào một thủ thuật tận dụng ngữ nghĩa dispatch method thông thường của Java. Ở đây tôi cho rằng pattern matching dễ đọc hơn nhiều lần
    • Những tính năng như thế này có các use case hợp lệ
      Ví dụ có thể nghĩ đến một interface bảo mật để xác thực security token
      Nếu là interface thông thường, rất dễ triển khai nó để bỏ qua token (cho phép tất cả), đánh cắp token, hoặc cài backdoor. Nếu một lớp như vậy được inject vào nơi thực hiện kiểm tra bảo mật, bảo mật có thể bị phá vỡ
      Với sealed interface, implementation mới chưa được phê duyệt không thể tồn tại. Nếu nhận được một object tuyên bố triển khai interface đó, ta được đảm bảo rằng nó là một trong những implementation đã được xác minh và thực sự thực hiện kiểm tra bảo mật. Như vậy đã loại bỏ hẳn một nhóm bug bảo mật và exploit
  • Đây là một bài viết hay đối với người biết về sum type nhưng chưa rõ sum type trong Java
    Tuy nhiên, tôi không chắc chỉ riêng sum type có khiến tôi thích Java hay không. Khả năng null trên diện rộng vẫn còn đó, và trong bài này nó cũng nhiều lần lộ diện

    • Trong Java, khả năng null là một vấn đề lớn, nhưng các framework về khả năng null dựa trên annotation khá hiệu quả và đã lan rộng trong toàn bộ hệ sinh thái. Cá nhân tôi xem chúng gần như là bắt buộc
      Tôi rất kỳ vọng vào https://jspecify.dev/, nơi Google, Meta, Microsoft, v.v. đang cố gắng chuẩn hóa annotation, bắt đầu từ @Nullable
    • Khi Valhalla được đưa vào, khả năng null tường minh cũng sẽ xuất hiện, nên vấn đề đó cũng sẽ được xử lý
    • Nó cũng chưa phải là một ngôn ngữ thiên về biểu thức
  • Câu trả lời trong bài cho câu hỏi "tại sao gọi là product type?" không sai, nhưng nói trực quan và ngắn gọn hơn thì tổng số giá trị khả dĩ của một product type là tích số lượng giá trị khả dĩ của các kiểu thành phần
    Nếu thay product bằng sum thì điều tương tự cũng đúng
    Thú vị là tổng số hàm riêng biệt có dạng a -> b, nếu chỉ xét đầu vào và đầu ra, có thể tính bằng lũy thừa. Tức là (số giá trị khả dĩ của b) ^ (số giá trị khả dĩ của a)

    • Nói trực quan và ngắn gọn hơn, product type tương đương với tích Descartes của các tập hợp
    • Map và list cũng là những ví dụ khác của kiểu lũy thừa. Về mặt lý thuyết, một hàm thuần có thể được thay thế bằng thao tác tra cứu map trên các giá trị đã tính trước, nên trực giác rằng chúng giống với hàm là tự nhiên. Trong ngữ cảnh này, list là một dạng map đặc biệt có khóa là số nguyên
      Viết dưới dạng toán học, với list của Bool, bên trái là số phần tử còn bên phải là tổng số khả năng
      0 : 1
      1 : 2
      2 : 4
      3 : 8
      4 : 16
      5 : 32
      Và cứ tiếp tục như vậy
  • Tôi đang chờ Project Valhalla hoàn tất để cuối cùng Java có value type. Khi đó, với sum type, value type và coroutine, có lẽ nó sẽ trở thành một trong những ngôn ngữ khá ổn

  • Java vốn không thật sự là một ngôn ngữ tệ
    Vấn đề là con người. Vấn đề là sự over-engineering khổng lồ, quá nhiều khái niệm trừu tượng khiến khó nắm bắt codebase, những thứ bùa chú mã dưới dạng annotation giống như câu lệnh GOTO ngược, và các framework DI
    Thứ cần sửa không phải ngôn ngữ mà là hệ sinh thái. Trong hệ sinh thái Java cần một phong trào kiểu "cải cách tôn giáo"
    Chỉ chuyển sang Kotlin, Clojure, Scala là chưa đủ

    • Trong bất kỳ ngôn ngữ nào, bạn cũng có thể tạo một HammerFactoryFactory để sinh ra HammerFactory. Nhưng hệ sinh thái Java khuyến khích và cổ vũ cách giải quyết vấn đề kiểu này. Tôi nghĩ C# cũng tương tự
      Một thứ Java thật sự cần là hàm độc lập, hoặc hàm có namespace. Đôi khi không cần class, chỉ cần một hàm trong module hoặc namespace là đủ, nhưng tôi không hiểu vì sao lại không được
  • Tác giả giải thích vì sao cần Records, và nêu rằng hầu hết object Java đều để mọi field là private và chỉ truy cập thông qua các phương thức accessor đọc/ghi
    Nhưng không có quy ước bắt buộc ở cấp độ ngôn ngữ để định nghĩa accessor, nên nếu đặt tên getter của foogetBar thì vẫn chạy, nhưng có thể gây nhầm lẫn cho người muốn truy cập bar
    Scala hỗ trợ pattern matching cho object triển khai phương thức unapply. Cách này có bị xem là có hại không? Vì sao Java không đi theo hướng này?

    • Lại là vấn đề chuẩn hóa. Việc chuẩn hóa của Java chậm như C++. Nếu nhìn vào chú thích cuối cùng của JEP về record pattern, nó ám chỉ rằng thứ gì đó như unapply có thể đang được chuẩn bị, nên hy vọng vẫn chưa hoàn toàn tắt
  • Java luôn là một ngôn ngữ tuyệt vời. Thứ khiến người ta muốn nôn là hệ sinh thái kiểu enterprise. Tôi từng thấy người ta dùng hàng chục class và interface chỉ để triển khai một dòng logic