4 คะแนน โดย GN⁺ 2023-09-18 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • 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
  • ตัวอย่างนี้ยังแสดงฟีเจอร์พรีวิว unnamed main methods ด้วย

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 ความคิดเห็น

 
GN⁺ 2023-09-18
ความคิดเห็นจาก Hacker News
  • ฟีเจอร์ที่ใหญ่ที่สุดของ Java 21 คือการปล่อย virtual threads: https://openjdk.org/jeps/444
    ไม่รู้ทำไมถึงถูกละไว้ในบทความ ถ้ามีฟีเจอร์ไหนที่จะดึงนักพัฒนา Go เดิมให้มาหา Java ได้ ก็น่าจะเป็นอันนี้ และน่าจะโน้มน้าวคนที่ไม่ชอบแพตเทิร์น concurrency แบบ reactive style ได้ด้วย

    • ไม่คิดว่านักพัฒนา Go เดิมจะกลับไป Java ผมทำงานกับ Java มา 10 ปีแล้วเปลี่ยนไป Go และไม่มีความคิดจะกลับไปอีก
      เพราะแอปพลิเคชันและไลบรารีของ Java ยากเกินไปที่จะใช้เหตุผลตามและทำความเข้าใจเมื่อเทียบกับ Go จากเรื่อง inheritance, packaging, object-oriented, build tools ฯลฯ
      Go เรียบง่าย เข้าใจ อ่าน และดูแลรักษาง่าย packaging ก็คล้ายกับวิธีจัดไฟล์ในคอมพิวเตอร์ไว้ในโฟลเดอร์เดียว และเครื่องมือต่าง ๆ ก็มีในตัวภาษา ไม่ได้รู้สึกว่าต้องมี IDE อย่าง IntelliJ ถึงจะพอใช้งานได้
      ตอนนี้อาจเปลี่ยนไปแล้วก็ได้ แต่ไลบรารี Java ส่วนใหญ่ที่เห็นทุกวันนี้ก็ยังดูเป็นแบบนี้อยู่
    • เพราะ Java 21 ผมเลยรอ JRuby รุ่นถัดไปที่มี virtual threads อยู่ Charles Nutter โชว์เดโมผลกระทบต่อ Ruby fibers ในงานนำเสนอ JRuby เดือนสิงหาคม ซึ่งค่อนข้างใหญ่
      มีหลายอย่างที่ชอบใน JVM และระบบเครื่องมือของมัน แต่ตอนนี้ไม่ค่อยอยากเขียนโค้ด Java แล้ว JRuby ให้ข้อดีของทั้งสองฝั่งได้ในระดับหนึ่ง
      งานนำเสนออยู่ที่นี่ และเดโม virtual threads เริ่มราวนาทีที่ 45
      https://youtu.be/pzm6I4liJlg?si=GtxQ4MThEaNDfC67
    • ไม่ค่อยแน่ใจว่านักพัฒนา Golang เดิมจะย้ายมา Java เพราะฟีเจอร์นี้หรือไม่ แต่ในแง่ทิศทาง ผมสงสัยว่าทำไมชุมชน Go ถึงมี concurrency containers น้อยขนาดนี้ ในขณะที่ชุมชน Java มีมากมาย
      แม้แต่ 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
    • Java 21 ยังมี structured concurrency เป็น preview ด้วย(https://openjdk.org/jeps/453) ใช้ implementation ของ virtual threads และแค่ดูตัวอย่างก็ดูดีทีเดียว ช่วยลดความเจ็บปวดหลายอย่างเมื่อจัดการ concurrency แบบอิง thread
  • คิดว่าชื่อบทความในบล็อกเลือกได้ไม่ค่อยดี ชื่อรองที่ซ่อนอยู่คือ "Algebraic data types in Java" ซึ่งอธิบายเนื้อหาได้ตรงกว่ามาก ชื่อที่ดีกว่าน่าจะเป็น Algebraic data types in Java 21
    อาจเพราะชื่อเรื่อง ความเห็นจำนวนมากที่นี่เลยออกนอกประเด็น อยากเห็นเรื่อง algebraic data types, ข้อดีข้อเสียของ implementation ใน Java และการเปรียบเทียบเชิงเทคนิคกับภาษาอื่น ๆ มากกว่านี้

    • ผมคิดว่าคงดีถ้ามีภาษาที่ algebraic data types ได้รับความนิยมมากกว่านี้ แต่ก็ไม่แน่ใจว่าตัวเองอยากเห็น algebraic data types เข้ามาใน 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-...
    • แนวทาง sealed classes ก็ยังเปิดให้ใคร ๆ ขยายโค้ดได้เช่นกัน เพียงแต่ขยายในมิติที่ต่างจากแนวทางเมธอดในอินเทอร์เฟซ
      เมธอดในอินเทอร์เฟซและการเรียกแบบ virtual call ไม่ยืดหยุ่นมากเมื่ออยากเพิ่ม operation ใหม่ แทนที่จะเพิ่มคลาสใหม่ แค่จะเพิ่ม operation ใหม่หนึ่งตัว ก็ต้องไปเพิ่มเมธอดใหม่ในทุก implementation และอาจทำให้ implementation ที่เราไม่มีสิทธิ์เข้าถึงพังได้ เมธอดที่ไม่เกี่ยวข้องกันยังต้องถูกนิยามอยู่ในคลาสเดียวกัน ทำให้อ่านโค้ดยากลงมาก และ virtual call ก็ไม่ได้ฟรี จึงกระทบประสิทธิภาพด้วย
      ในกรณีนี้ sealed class ขยายได้ดีกว่ามาก แค่เพิ่ม switch ใหม่ไว้ที่เดียวก็จบ และไม่ทำลาย backward compatibility
      นี่คือ expression problem อันโด่งดัง
      https://pkolaczk.github.io/in-defense-of-switch/
    • ผมเข้าใจคำแนะนำให้ใช้ polymorphic dispatch แทน 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 ที่แพร่หลายยังคงอยู่ และในบทความนี้ก็โผล่ให้เห็นหลายครั้ง

    • ใน Java ความเป็นไปได้ของ null เป็นปัญหาใหญ่ แต่เฟรมเวิร์กด้าน nullability ที่อิง annotation นั้นได้ผลและแพร่กระจายไปทั่วทั้ง ecosystem โดยส่วนตัวมองว่าแทบจะจำเป็นเลย
      ผมตั้งตารอ https://jspecify.dev/ มาก ซึ่ง Google, Meta, Microsoft ฯลฯ พยายามทำให้ annotation เป็นมาตรฐาน โดยเริ่มจาก @Nullable
    • ถ้า Valhalla เข้ามา ก็จะมี nullability แบบระบุชัดเจน ด้วย ดังนั้นปัญหานั้นก็น่าจะถูกจัดการ
    • ยังไม่ใช่ภาษาแบบ expression-oriented ด้วย
  • คำตอบในบทความต่อคำถามว่า “ทำไมถึงเรียกว่า product type?” ไม่ได้ผิด แต่ถ้าพูดให้เข้าใจง่ายและกระชับกว่า จำนวนค่าที่เป็นไปได้ทั้งหมดของ product type คือผลคูณของจำนวนค่าที่เป็นไปได้ของ type ที่ประกอบกัน
    ถ้าเปลี่ยน product เป็น sum ก็เป็นจริงแบบเดียวกัน
    ที่น่าสนใจคือ จำนวนฟังก์ชันเฉพาะทั้งหมดในรูป a -> b หากดูแค่อินพุตกับเอาต์พุต จะคำนวณได้ด้วยเลขยกกำลัง กล่าวคือ (จำนวนค่าที่เป็นไปได้ของ b) ^ (จำนวนค่าที่เป็นไปได้ของ a)

    • ถ้าพูดให้เข้าใจง่ายและกระชับกว่า product type เทียบเท่ากับผลคูณคาร์ทีเซียนของเซต
    • map และ list ก็เป็นตัวอย่างอื่นของ exponential type เช่นกัน ฟังก์ชันบริสุทธิ์ในเชิงทฤษฎีสามารถถูกแทนที่ด้วยการ lookup ใน map ของค่าที่คำนวณไว้ล่วงหน้าได้ ดังนั้น intuition ที่ว่าสิ่งเหล่านี้เหมือนฟังก์ชันจึงเป็นธรรมชาติ ในบริบทนี้ list คือ map แบบพิเศษที่มี key เป็นจำนวนเต็ม
      ถ้าเขียนในเชิงคณิตศาสตร์ สำหรับ 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 ถึงไม่เดินตามเส้นทางนี้?

    • เป็นปัญหาเรื่องการทำให้เป็นมาตรฐานอีกเช่นกัน การทำมาตรฐานของ Java ช้าเหมือน C++ ถ้าดูเชิงอรรถสุดท้ายของ record pattern JEP จะเห็นว่ามีการบอกใบ้ว่าสิ่งคล้าย unapply อาจกำลังเตรียมอยู่ ดังนั้นความหวังยังไม่ได้หมดไปเสียทีเดียว
  • Java เป็นภาษาที่ยอดเยี่ยมมาโดยตลอด สิ่งที่ทำให้อยากอาเจียนคือ ecosystem แบบ enterprise เคยเห็นการ implement logic แค่บรรทัดเดียวต้องใช้ class กับ interface หลายสิบตัว