- Java 21 ซึ่งเปิดตัวเมื่อวันที่ 19 กันยายน 2023 นำรูปแบบการเขียนเชิงฟังก์ชันที่ใกล้เคียงกับ Kotlin, Rust และ C# เข้ามาใน Java ผ่าน record patterns และ switch pattern matching
- การเปลี่ยนแปลงที่สะสมมาตั้งแต่ switch expressions ใน Java 14, records และ
instanceof pattern matching ใน Java 16, sealed classes ใน Java 17 ได้มาบรรจบกันใน Java 21 เพื่อเป็นรากฐานสำหรับจัดการ ชนิดข้อมูลเชิงพีชคณิต
- records ช่วยให้แยกข้อมูลได้อย่างมั่นคงด้วยข้อจำกัดอย่าง final, การอ้างอิงแบบไม่เปลี่ยนรูป และ getter ที่เป็นแบบแผน ส่วน record pattern ทำให้ดึงข้อมูลที่ซ้อนกันออกมาได้โดยตรงใน switch
- sealed classes/interfaces เปิดให้ใช้เฉพาะ subtype ที่อนุญาตเท่านั้น เพื่อสร้างโมเดลที่ใกล้เคียงกับ sum type และเมื่อใช้ sealed interface ร่วมกับ record ก็สามารถจำกัดรูปแบบต่าง ๆ อย่าง RGB, CMYK, YUV และ HSL ได้
- switch ของ Java 21 รองรับทั้ง
null case และ when guard แต่ record accessor ที่ผิดพลาดหรือข้อยกเว้นระหว่างการรัน guard อาจนำไปสู่ java.lang.MatchException ได้
Pattern matching ที่เสถียรใน Java 21
- Java 21 เปิดตัวเมื่อวันที่ 19 กันยายน 2023 และรองรับ record patterns ใน switch block และ switch expression
- ไวยากรณ์นี้ถูกมองว่าเป็นจุดเปลี่ยนที่ทำให้ Java สามารถแสดง pattern ของ functional programming ได้ในลักษณะคล้ายกับ Kotlin, Rust และ C#
- การเปลี่ยนแปลงสำคัญด้านไวยากรณ์ใน Java รุ่นหลัง ๆ นำมาสู่ pattern matching ของ Java 21
- Java 14: ทำให้ switch expressions เสถียร
- Java 16: ทำให้ records และ
instanceof pattern matching เสถียร
- Java 17: ทำให้ sealed classes เสถียร
- Java 21: ทำให้ record patterns และ switch pattern matching เสถียร
- ชุดการเปลี่ยนแปลงนี้ทำให้ Java สามารถจัดการ ชนิดข้อมูลเชิงพีชคณิต (algebraic data types) และรูปแบบการใช้งานตามธรรมเนียมของมัน ซึ่งก่อนหน้านี้แสดงออกได้ยาก
แนวคิดขั้นต่ำจากทฤษฎีชนิดข้อมูลที่ต้องรู้
- เพื่อให้เข้าใจฟีเจอร์ของ Java 21 จำเป็นต้องรู้แนวคิดบางอย่างจากทฤษฎีชนิดข้อมูล
- bottom/empty type หมายถึงเซตของค่าที่ไม่สามารถคำนวณออกมาได้ และในภาษาโปรแกรมทั่วไปมักเป็นเซตว่าง
Nothing ของ Kotlin มี constructor เป็น private จึงไม่สามารถมีอินสแตนซ์ได้
Void ของ Java มี constructor เป็น private แต่สามารถเก็บ null ได้ จึงมองว่าเป็น bottom type ที่แท้จริงได้ยาก
- primitive
void ของ Java ไม่สามารถใช้เป็นชนิดของตัวแปรได้ จึงทำงานใกล้เคียงกว่าในแง่นี้
- top type คือเซตสากลที่แทนค่าทุกค่าของทุกชนิด
- ใน Kotlin
Any ทำหน้าที่นี้
Object ของ Java มี primitive แยกออกจาก object model จึงมองว่ามีความหมายเดียวกับ top type ของภาษาอื่นได้ยาก
- unit type คือชนิดที่มีค่าเพียงค่าเดียว
void ของ Java สามารถมองคล้าย unit type ได้ในการคืนค่าของเมธอด แต่ไม่สามารถส่งเป็นชนิดของพารามิเตอร์ได้
Unit ของ Kotlin นิยามเป็น object และใช้เป็นพารามิเตอร์ของเมธอดได้ด้วย
- boolean type มีสองค่า คือ
true และ false และแม้จะสามารถแสดงด้วย nullable unit type ได้ แต่ไม่ค่อยมีประโยชน์ในทางปฏิบัติ
Product type และ Java records
- product type คือชนิดที่รวมชนิดองค์ประกอบตั้งแต่สองชนิดขึ้นไป โดยจำนวนชนิดองค์ประกอบเรียกว่า arity หรือ degree
struct ในภาษา C เป็นตัวอย่างของ product type
- ชนิดองค์ประกอบอาจซ้ำกันได้ เช่น
int, char *, double, int
- ชนิดที่ซ้ำกันสามารถแยกแยะได้หากมองร่วมกับชื่อฟิลด์ในฐานะ ordered pair
- tuple ของ Python หรือ Rust ก็ถือเป็น product type ได้เช่นกัน โดยในกรณีนี้ดัชนีทำหน้าที่เป็นชื่อขององค์ประกอบ
- record class ที่เสถียรใน Java 16 เป็นตัวอย่างที่ดีของ product type
- ฟิลด์ของ record เป็น final และ record ไม่สามารถถูกสืบทอดได้
- สถานะของ record ถูกตั้งค่า ณ เวลาสร้าง และคงอยู่หลังจากนั้น
- อย่างไรก็ตาม หากใส่ mutable data type ไว้ภายใน record ก็ไม่ได้รับประกันความไม่เปลี่ยนรูปของเนื้อหานั้นด้วย
- class ปกติของ Java อาจผสมสถานะ public/private, สถานะแฝงที่เกิดจากการสืบทอด, mutable/static field และ getter ที่ไม่เป็นมาตรฐาน ทำให้การทำให้เป็นองค์ประกอบทั่วไปทำได้ยาก
- records รับประกันโครงสร้างที่ทำให้ฟีเจอร์ภาษาอย่าง pattern matching ทำงานได้อย่างมั่นคงด้วยข้อจำกัดต่อไปนี้
- record เป็น final class โดยนัย และไม่สามารถถูกสืบทอดได้
- ไม่สามารถ extend class อื่นนอกจาก
java.lang.Record ได้
- record component ไม่สามารถติด visibility modifier ได้
- การอ้างอิง component เป็น final เสมอ และถือว่าไม่เปลี่ยนรูป
- getter พื้นฐานใช้ชื่อฟิลด์ตรง ๆ โดย getter ของฟิลด์
a คือ a()
- backing field เป็น private โดยนัย และเข้าถึงผ่าน getter
แยกข้อมูลซ้อนกันด้วย record pattern
- switch pattern ของ Java 21 แยก ข้อมูล record ที่ซ้อนกัน โดยไม่ต้องตรวจ
instanceof และ cast อย่างชัดเจนซ้ำ ๆ
- ตัวอย่างใช้
record A(Record inner), record B(char b), record SomeOtherRecord()
- วิธีเดิมต้อง
if (r instanceof A) แล้ว cast จากนั้นทำ instanceof และ cast ซ้ำกับค่าภายใน
- switch pattern ดึงค่าที่ซ้อนกันออกมาได้โดยตรง เช่น
case A(B(char a)) -> String.valueOf(a)
- switch block มีโครงสร้างชัดเจนกว่า if-else ladder และเหมาะกับการดึงข้อมูลที่ซ้อนลึกออกมาอย่างรวดเร็ว
- หากต้องการรันโดยตรงใน Java 21 ให้วางโค้ดใน
main.java แล้วใช้คำสั่งต่อไปนี้
java --enable-preview --source 21 main.java
Sum type และ sealed classes/interfaces
- เมื่อแสดงตัวเลือกที่จำกัด สามารถใช้ Java enum ได้ แต่การแทนสีที่มีโครงสร้างข้อมูลต่างกัน เช่น RGB, HSL, YUV, CMYK นั้นจัดการด้วย enum เพียงอย่างเดียวได้ยุ่งยาก
- สามารถใช้ polymorphism แบบอิงการสืบทอดเพื่อสร้าง abstract class
Color และ class RGB, CMYK, YUV, HSL ได้ แต่ class hierarchy ปกติเป็นแบบเปิด
- ผู้ใช้ไลบรารีสามารถสร้าง class ใหม่อย่าง
RYB เพื่อสืบทอด Color ได้
- หาก API ไม่ได้ตั้งใจให้ขยายได้ variant ใหม่อาจทำให้เกิด crash หรือบั๊กละเอียดอ่อนในโค้ดที่อยู่ไกลออกไป
- sealed classes ใช้เพื่อแสดงแนวคิด sum type ใน Java
- sum type คือชนิดที่ ณ เวลาหนึ่งสามารถเป็นหนึ่งในองค์ประกอบของมันได้
- เรียกอีกอย่างว่า tagged union type
- การใช้ modifier
sealed และ clause permits ช่วยอนุญาตให้เฉพาะบาง class สืบทอดได้
public sealed class Color permits RGB, CMYK, YUV, HSL {
}
- ใน sealed class hierarchy ผู้สืบทอดโดยตรงหรือโดยอ้อมต้องมีหนึ่งใน
sealed, non-sealed, final มิฉะนั้นจะเกิด compile error
sealed: สืบทอดได้เฉพาะชนิดที่มีชื่ออยู่ใน permits
non-sealed: สืบทอดได้เหมือน class ปกติ
final: เป็น leaf ของต้นไม้การสืบทอด และขยายต่อไม่ได้
วิธีใช้ sealed interface ร่วมกับ record
- destructuring ของ switch pattern matching ทำงานกับ records แต่ records ไม่สามารถสืบทอด class อื่นนอกจาก
Record ได้
- ทางแก้คือใช้ sealed interface
- sealed interface ทำงานคล้าย sealed class
- records และ enums ก็สามารถ implement sealed interface ได้
- ในตัวอย่าง สร้าง
Color เป็น sealed interface และ implement RGB, CMYK, YUV, HSL เป็น 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 + ")";
}
}
- จากนั้นสามารถดึงค่าของแต่ละ record ได้โดยตรงใน 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 สามารถจัดการ
null case ใน switch block และ expression ได้ จึงไม่ต้องตรวจ null แยกต่างหากก่อน switch
- หาก
Color เป็น sealed type Java จะรู้ได้ว่าจัดการทุก case แล้วหรือไม่ ทำให้ทำ exhaustive switch ได้โดยไม่ต้องมี default case
Guard clause และ when
- Java 21 รองรับ guard clause ที่เพิ่มเงื่อนไขเสริมให้ switch arm
- guard clause รวมเงื่อนไขเข้ากับ case label ด้วยคีย์เวิร์ด
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...");
}
}
- เดิมทีต้องใส่
if (red > 200) อีกครั้งใน body ของ case RGB(...)
- Java match case แรกที่ประเมินได้เป็น true แบบ eager ดังนั้นควรวาง case ที่เฉพาะเจาะจงกว่าไว้ก่อน และวาง case ที่เฉพาะเจาะจงน้อยกว่าไว้ทีหลัง
- หลัง RGB case ที่มี guard ต้องมี
case RGB rgb แบบทั่วไปเพื่อรักษา exhaustive
กรณีที่เกิด MatchException
- pattern matching ของ Java 21 มี
java.lang.MatchException เข้ามาเกี่ยวข้องเพิ่มเติม
- หาก record accessor โยน exception ออกมา switch pattern อาจล้มเหลวและเกิด
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);
}
}
- ในตัวอย่างข้างต้น accessor
i() โยน ArithmeticException ทำให้ switch block โยน MatchException
- ตาม JEP 441 record accessor ที่โยน exception เสมอถือว่าผิดปกติมาก และกรณีที่ exhaustive pattern switch โยน
MatchException ก็ถือว่าพบได้ยากมากเช่นกัน
- แม้ใน exhaustive switch หากไม่มี variant ที่กำหนดไว้สำหรับ selector ถูก match เลย ก็อาจเกิด exception ได้
- JEP 441 อธิบายกรณี exhaustive switch สำหรับ enum ที่ match ล้มเหลวว่าเป็นสถานการณ์ที่ enum class ถูกเปลี่ยนหลังจาก switch ถูกคอมไพล์แล้ว
- หากเกิด exception ระหว่างการรัน guard clause ก็อาจเกิด
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;
}
}
ขอบเขตที่เหลือ
- เมื่อผสาน records, sealed types, switch pattern matching และ guard clause ของ Java 21 เข้าด้วยกัน ก็สามารถนำ building blocks ของ functional programming มาใช้กับโค้ด Java ได้
- บางหัวข้อ เช่น วิธีที่ generics โต้ตอบกับ switch patterns ไม่ได้กล่าวถึง
- บทความถัดไปจะกล่าวถึง quirks และตัวอย่างเชิงปฏิบัติที่ใช้ปรับปรุงวิธีเขียนโค้ด Java
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
ฟีเจอร์ที่ใหญ่ที่สุดของ Java 21 คือการปล่อย virtual threads: https://openjdk.org/jeps/444
ไม่รู้ทำไมถึงถูกละไว้ในบทความ ถ้ามีฟีเจอร์ไหนที่จะดึงนักพัฒนา Go เดิมให้มาหา Java ได้ ก็น่าจะเป็นอันนี้ และน่าจะโน้มน้าวคนที่ไม่ชอบแพตเทิร์น concurrency แบบ reactive style ได้ด้วย
เพราะแอปพลิเคชันและไลบรารีของ Java ยากเกินไปที่จะใช้เหตุผลตามและทำความเข้าใจเมื่อเทียบกับ Go จากเรื่อง inheritance, packaging, object-oriented, build tools ฯลฯ
Go เรียบง่าย เข้าใจ อ่าน และดูแลรักษาง่าย packaging ก็คล้ายกับวิธีจัดไฟล์ในคอมพิวเตอร์ไว้ในโฟลเดอร์เดียว และเครื่องมือต่าง ๆ ก็มีในตัวภาษา ไม่ได้รู้สึกว่าต้องมี IDE อย่าง IntelliJ ถึงจะพอใช้งานได้
ตอนนี้อาจเปลี่ยนไปแล้วก็ได้ แต่ไลบรารี Java ส่วนใหญ่ที่เห็นทุกวันนี้ก็ยังดูเป็นแบบนี้อยู่
มีหลายอย่างที่ชอบใน JVM และระบบเครื่องมือของมัน แต่ตอนนี้ไม่ค่อยอยากเขียนโค้ด Java แล้ว JRuby ให้ข้อดีของทั้งสองฝั่งได้ในระดับหนึ่ง
งานนำเสนออยู่ที่นี่ และเดโม virtual threads เริ่มราวนาทีที่ 45
https://youtu.be/pzm6I4liJlg?si=GtxQ4MThEaNDfC67
แม้แต่
sync.Mapก็ไม่ใช่ของทั่วไปแบบConcurrentMapของ Java แต่ถูกปรับให้เหมาะกับ use case เฉพาะสองแบบ Java มี concurrent set, queue, barrier, phaser, fork-join pool และอื่น ๆ แม้จะมี goroutine แล้ว container เหล่านี้ก็น่าจะมีประโยชน์พอสมควร อย่างน้อย fork-join ก็ไม่ใช่สิ่งที่ implement ได้แบบเล็กน้อยนัก การใช้ mutex ไปทุกที่รู้สึกเป็นระดับต่ำเกินไปผมรู้ว่ามี implementation จาก third-party แต่ concurrency นั้นทำให้ถูกต้องได้ยากมาก จนถ้าไม่ใช่ระดับที่มีความเป็นผู้ใหญ่และมีผู้ใช้กับนักพัฒนาจำนวนมากหนุนหลังอย่าง JCTools ของ Java หรือ Google Guava ก็ลังเลที่จะนำแพ็กเกจ third-party มาใช้
Executor.newVirtualThreadPerTaskExecutorกับgoแสดงให้เห็นแก่นสำคัญว่าทำไมนักพัฒนา Go จึงไม่น่าจะย้ายไป Javaถ้าจะแก้ให้ถูก จริง ๆ แล้วมันใกล้กับ
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { executor.submit(...) }มากกว่าจะเป็นgoคิดว่าชื่อบทความในบล็อกเลือกได้ไม่ค่อยดี ชื่อรองที่ซ่อนอยู่คือ "Algebraic data types in Java" ซึ่งอธิบายเนื้อหาได้ตรงกว่ามาก ชื่อที่ดีกว่าน่าจะเป็น Algebraic data types in Java 21
อาจเพราะชื่อเรื่อง ความเห็นจำนวนมากที่นี่เลยออกนอกประเด็น อยากเห็นเรื่อง algebraic data types, ข้อดีข้อเสียของ implementation ใน Java และการเปรียบเทียบเชิงเทคนิคกับภาษาอื่น ๆ มากกว่านี้
โค้ด Java เดิมไม่ได้หายไปไหน ดังนั้นถ้าโค้ดแบบนี้ถูกปนเข้ามาแบบสุ่ม ๆ มันจะดีขึ้นจริงหรือ
ฟีเจอร์ sealed classes ที่อธิบายตรงนี้ให้ความรู้สึกว่าเข้าใจผิดไปหมด
ตรรกะคือ ถ้ามีอินเทอร์เฟซทั่วไป ใคร ๆ ก็สามารถสร้างคลาสใหม่ที่ implement มันได้ และโค้ดอย่าง
if (x instanceof Foo) { ... } else if (x instanceof Bar) { ... }จะพังตอนรันไทม์ถ้ามีคนเพิ่มคลาสใหม่เข้ามา ดังนั้นถ้าใช้ฟีเจอร์ sealed interface ใหม่เพื่อไม่ให้ใครสร้างคลาสใหม่ที่ implement อินเทอร์เฟซนั้นได้ คำสั่งifก็จะไม่พัง ประมาณนั้นแต่การเขียนโปรแกรมเชิงวัตถุไม่ได้คิดและแก้ปัญหานี้ไว้แล้วหรือ? ผมรู้ว่าตอนนี้ OOP ไม่ได้เป็นกระแสเท่าเดิมแล้ว แต่ Java ก็เป็นภาษาเชิงวัตถุ
วิธีแก้คือเพิ่มเมธอดเข้าไปในอินเทอร์เฟซ แล้วให้ทุกคลาส implement เมธอดนั้น จากนั้นแทนที่จะไล่ระบุทุกตัวเลือกใน if/switch ขนาดยักษ์ ก็แค่เรียกเมธอดนั้น
วิธีนี้ดีกว่าการปิดกั้นไม่ให้ขยายโค้ด และกลับทำให้ขยายได้ด้วยซ้ำ implementer ใหม่แค่ implement เมธอดนั้นก็พอ และคอมไพเลอร์จะบังคับให้ จึงไม่มีทางเผลอลืมได้
ตัวอย่างเรื่องปริภูมิสีในบทความ (RGB, CMYK ฯลฯ) เป็นตัวอย่างโต้แย้งที่ดีมาก ถ้าเขียนโค้ดที่ใช้ปริภูมิสี ผู้ใช้หรือลูกค้าอาจต้องใช้ปริภูมิสีแปลก ๆ และหายากที่เราไม่เคยนึกถึงก็ได้ ผมไม่อยากสร้างโค้ดที่รองรับเฉพาะปริภูมิสีที่ไล่ใส่ไว้ใน if/switch ขนาดยักษ์ และกลายเป็นโค้ดที่ขยายไม่ได้เพราะโครงสร้างแบบนั้น
sealed classes แก้ปัญหานี้ได้ แต่ก็แลกมาด้วยปัญหาใหม่ว่า “ถ้าต้องมีคลาสที่ขยายเพิ่มอีก และไม่สามารถรู้ทั้งหมดล่วงหน้าได้ล่ะ?” สุดท้ายคำถามก็คือมีวิธีทำให้ได้ทั้งสองอย่างหรือไม่
ปัญหานี้เรียกว่า expression problem [1]
มีภาษาที่มี static type บางภาษาที่แก้ expression problem ได้ และ Java ก็เป็นหนึ่งในนั้น [2] เพียงแต่แนวทางใน Java ยังซับซ้อนและไม่สะดวกมาก จึงแทบไม่ถูกใช้ ถ้าอยากอยู่ในโลก Haskell หรือ JVM ต่อ Scala ทำเรื่องนี้ได้ดีกว่ามาก
[1] https://en.wikipedia.org/wiki/Expression_problem
[2] https://koerbitz.me/posts/Solving-the-Expression-Problem-in-...
เมธอดในอินเทอร์เฟซและการเรียกแบบ virtual call ไม่ยืดหยุ่นมากเมื่ออยากเพิ่ม operation ใหม่ แทนที่จะเพิ่มคลาสใหม่ แค่จะเพิ่ม operation ใหม่หนึ่งตัว ก็ต้องไปเพิ่มเมธอดใหม่ในทุก implementation และอาจทำให้ implementation ที่เราไม่มีสิทธิ์เข้าถึงพังได้ เมธอดที่ไม่เกี่ยวข้องกันยังต้องถูกนิยามอยู่ในคลาสเดียวกัน ทำให้อ่านโค้ดยากลงมาก และ virtual call ก็ไม่ได้ฟรี จึงกระทบประสิทธิภาพด้วย
ในกรณีนี้ sealed class ขยายได้ดีกว่ามาก แค่เพิ่ม switch ใหม่ไว้ที่เดียวก็จบ และไม่ทำลาย backward compatibility
นี่คือ expression problem อันโด่งดัง
https://pkolaczk.github.io/in-defense-of-switch/
instanceofและเคยเห็น Bob Martin พูดเรื่องนี้ยาว ๆ แล้ว แต่ไม่เห็นด้วยการทำ polymorphic dispatch แบบนี้ทำให้วัตถุต้องจัดการหลาย concern ด้วยตัวเอง
ในวิดีโอเกม
Carอาจมี.render(),.collide(),.playSound()ภายหลังถ้าเพิ่มDogก็แค่ implement สามเมธอดนี้ และไม่จำเป็นต้องแก้หรือคอมไพล์Renderer,PhysicsEngine,SoundEngineใหม่ โปรแกรมเมอร์คนอื่นก็เพิ่ม entity ได้โดยไม่เอาบั๊กมาใส่ในโค้ดล้ำค่าของเรา ดูดีทีเดียวแต่ตอนนี้
CarและDogต้องรู้ทั้งกราฟิก ฟิสิกส์ และเสียงทั้งหมด และ entity ก็ไม่ได้ดำรงอยู่อย่างโดดเดี่ยว รถกับสุนัขต้องถูกเรนเดอร์ตามลำดับที่ถูกต้อง และอาจบังกันได้ การชนก็ต้องตรวจสอบกันเองด้วย อย่างที่เคยเจอใน game jam จริง ๆ คนรับผิดชอบเสียงอาจต้องเข้าไปในทุก object เพื่อเพิ่มพฤติกรรมด้านเสียงสู้ตอนคิดเรื่องฟิสิกส์ทำงานใน
Physics.collideAll()และใช้instanceofจัดการกรณีพิเศษเมื่อจำเป็น ส่วนตอนคิดเรื่องกราฟิกก็ทำงานในGraphics.renderAll()จะดีกว่ามากในงานพัฒนาเว็บ Java ฝั่งแบ็กเอนด์ทั่วไปก็คล้ายกัน เมื่อจะตัดสินใจแปลง Java object เป็น HTTP response ใน REST controller การดูทั้งหมดในเมธอดเดียวแล้ว map
{instanceof Forbidden}เป็น 403 และ{instanceof NotFound}เป็น 404 จะดีกว่า ผมไม่อยากใส่getCode()หรือเนื้อหาเฉพาะ REST ลงไปในคลาส Java เองStringเป็นfinalก็มีเหตุผลของมัน และอาจถึงขั้นมองได้ว่าควรให้finalเป็นค่าเริ่มต้น แล้วระบุเฉพาะคลาสที่จะอนุญาตให้ทำ subclass ว่าเป็นopenใน functional programming ตัวอย่างคลาสสิกของ sum type คือ list ซึ่งมีแค่
Element(T head, List tail)กับNil()เท่านั้น ไม่มีเหตุผลให้ขยาย และจริง ๆ ถ้าขยายก็อาจกลายเป็นโค้ดที่ผิดเมื่อใช้ร่วมกับทุกฟังก์ชันที่จัดการ listอีกอย่าง visitor pattern ที่คล้ายกับ pattern matching นั้นยืดยาวมาก และพึ่งพาแฮ็กที่อาศัย semantics ของ method dispatch ปกติใน Java ในกรณีนี้ผมเห็นว่า pattern matching อ่านง่ายกว่าหลายเท่า
ตัวอย่างเช่น ลองนึกถึงอินเทอร์เฟซด้านความปลอดภัยที่ตรวจสอบ security token
ถ้าเป็นอินเทอร์เฟซทั่วไป ก็ง่ายที่จะ implement แล้วเพิกเฉยต่อ token (อนุญาตทั้งหมด), ขโมย token หรือใส่ backdoor หากคลาสแบบนั้นถูก inject เข้าไปในจุดที่มีการตรวจสอบความปลอดภัย ระบบก็อาจถูกเจาะได้
ถ้าเป็น sealed interface ก็จะไม่มี implementation ใหม่ที่ไม่ได้รับอนุมัติอยู่ได้ หากได้รับ object ที่อ้างว่า implement อินเทอร์เฟซนั้น ก็รับประกันได้ว่าเป็นหนึ่งใน implementation ที่ผ่านการตรวจสอบและทำการตรวจสอบความปลอดภัยจริง เท่ากับกำจัดบั๊กความปลอดภัยและ exploit ได้ทั้งหมวดหนึ่งเลย
เป็นบทความที่ดีสำหรับคนที่พอรู้จัก sum type แต่ยังไม่ค่อยรู้จัก sum type ของ Java
แต่ก็ไม่แน่ใจว่าแค่ sum type อย่างเดียวจะทำให้กลับมาชอบ Java ได้ไหม ความเป็นไปได้ของ
nullที่แพร่หลายยังคงอยู่ และในบทความนี้ก็โผล่ให้เห็นหลายครั้งnullเป็นปัญหาใหญ่ แต่เฟรมเวิร์กด้าน nullability ที่อิง annotation นั้นได้ผลและแพร่กระจายไปทั่วทั้ง ecosystem โดยส่วนตัวมองว่าแทบจะจำเป็นเลยผมตั้งตารอ https://jspecify.dev/ มาก ซึ่ง Google, Meta, Microsoft ฯลฯ พยายามทำให้ annotation เป็นมาตรฐาน โดยเริ่มจาก
@Nullableคำตอบในบทความต่อคำถามว่า “ทำไมถึงเรียกว่า product type?” ไม่ได้ผิด แต่ถ้าพูดให้เข้าใจง่ายและกระชับกว่า จำนวนค่าที่เป็นไปได้ทั้งหมดของ product type คือผลคูณของจำนวนค่าที่เป็นไปได้ของ type ที่ประกอบกัน
ถ้าเปลี่ยน product เป็น sum ก็เป็นจริงแบบเดียวกัน
ที่น่าสนใจคือ จำนวนฟังก์ชันเฉพาะทั้งหมดในรูป
a -> bหากดูแค่อินพุตกับเอาต์พุต จะคำนวณได้ด้วยเลขยกกำลัง กล่าวคือ(จำนวนค่าที่เป็นไปได้ของ b) ^ (จำนวนค่าที่เป็นไปได้ของ a)ถ้าเขียนในเชิงคณิตศาสตร์ สำหรับ list ของ Bool ด้านซ้ายคือจำนวนสมาชิก และด้านขวาคือจำนวนความเป็นไปได้ทั้งหมด
0 : 1
1 : 2
2 : 4
3 : 8
4 : 16
5 : 32
ต่อไปเรื่อย ๆ แบบนี้
กำลังรอให้ Project Valhalla เสร็จสมบูรณ์ เพื่อให้ในที่สุด Java มี value type เข้ามา พอถึงตอนนั้นก็น่าจะกลายเป็นหนึ่งในภาษาที่ค่อนข้างดี เพราะมีทั้ง sum type, value type และ coroutine
เดิมที Java ไม่ใช่ภาษาที่แย่จริง ๆ
ปัญหาอยู่ที่ผู้คนต่างหาก ทั้งการ over-engineering ขนาดมหึมา แนวคิดเชิง abstraction ที่มากเกินไปจนทำให้เข้าใจ codebase ได้ยาก เวทมนตร์โค้ดในรูป annotation ที่เหมือนคำสั่ง GOTO ย้อนกลับ และ DI framework ล้วนเป็นปัญหา
สิ่งที่ต้องแก้ไม่ใช่ภาษา แต่คือ ecosystem ภายใน ecosystem ของ Java จำเป็นต้องมีขบวนการ “ปฏิรูปศาสนา” บางอย่าง
การย้ายไป Kotlin, Clojure, Scala อย่างเดียวไม่พอ
HammerFactoryFactoryที่ผลิตHammerFactoryออกมาได้ แต่ ecosystem ของ Java ส่งเสริมและสนับสนุนวิธีแก้ปัญหาแบบนี้ ผมว่า C# ก็คล้ายกันสิ่งหนึ่งที่ Java ต้องการจริง ๆ คือฟังก์ชันอิสระ หรือฟังก์ชันที่มี namespace กำกับ บางครั้งไม่จำเป็นต้องมี class แค่ฟังก์ชันใน module หรือ namespace ก็พอแล้ว แต่ไม่เข้าใจว่าทำไมถึงทำไม่ได้
ผู้เขียนอธิบายเหตุผลที่ต้องมี Records โดยยกประเด็นว่า object ส่วนใหญ่ใน Java เก็บทุก field เป็น private และให้เข้าถึงได้ผ่านเมธอด accessor สำหรับอ่าน/เขียนเท่านั้น
แต่ไม่มีข้อบังคับในระดับภาษาที่ทำให้การนิยาม accessor เป็นมาตรฐาน ดังนั้นถ้าตั้งชื่อ getter ของ
fooเป็นgetBarก็ยังทำงานได้ แต่จะทำให้คนที่พยายามเข้าถึงbarสับสนScala รองรับ pattern matching กับ object ที่ implement เมธอด
unapplyวิธีนี้ถูกมองว่าเป็นสิ่งอันตรายหรือเปล่า? ทำไม Java ถึงไม่เดินตามเส้นทางนี้?unapplyอาจกำลังเตรียมอยู่ ดังนั้นความหวังยังไม่ได้หมดไปเสียทีเดียวJava เป็นภาษาที่ยอดเยี่ยมมาโดยตลอด สิ่งที่ทำให้อยากอาเจียนคือ ecosystem แบบ enterprise เคยเห็นการ implement logic แค่บรรทัดเดียวต้องใช้ class กับ interface หลายสิบตัว