2 คะแนน โดย GN⁺ 2024-09-06 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • Clojure 1.12.0 ยังคงใช้ Java 8 bytecode และถูกประกาศว่าเป็นรีลีสสุดท้ายที่อิง Java 8 ก่อนที่รีลีสถัดไปจะย้ายเกณฑ์ความเข้ากันได้ขั้นต่ำของ Java และมาตรฐาน bytecode ไปยัง Java LTS รุ่นใหม่กว่า
  • ในสภาพแวดล้อม virtual threads ของ JDK 21, lazy-seq และ delay เปลี่ยนจากการใช้ synchronized มาใช้ lock เพื่อลดสถานการณ์ที่ blocking I/O ทำให้ thread จริงถูก pin ไว้
  • ใน REPL สามารถเพิ่มไลบรารีด้วย add-lib, add-libs, sync-deps ได้โดยไม่ต้องรีสตาร์ท JVM แต่ฟีเจอร์นี้จำกัดไว้สำหรับ การใช้งานแบบโต้ตอบระหว่างพัฒนา
  • การทำงานร่วมกับ Java กว้างขึ้น โดยเพิ่ม method values, :param-tags, ไวยากรณ์ array class, การแปลง functional interface, Supplier และฟังก์ชันจัดการ Java Stream
  • ด้านประสิทธิภาพและความเข้ากันได้ มีการเพิ่ม PersistentVector spliterator, การจัดการ drop/partition ที่มีประสิทธิภาพขึ้น, การเข้มงวดนโยบาย Var interning, แก้ไข CVE-2024-22871 และจัดระเบียบตัวระบุของ Java serialization

ความเข้ากันได้กับ Java 8, ความปลอดภัย และการจัดระเบียบ serialization

  • ข้อมูลการดาวน์โหลดและการใช้งาน Clojure 1.12.0 ดูได้ที่หน้า Downloads
  • รีลีสนี้ยังคงยึด Java 8 เป็นเกณฑ์
    • Clojure 1.12 สร้าง Java 8 bytecode เช่นเดียวกับ Clojure 1.10 และ 1.11
    • รีลีสถัดไปมีแผนจะย้าย bytecode และความเข้ากันได้ขั้นต่ำของ Java ไปยัง Java LTS รุ่นใหม่กว่า
  • ปัญหา การ pin virtual thread ใน JDK 21 ได้รับการบรรเทา
    • ก่อน 1.12, lazy-seq และ delay รันโค้ดของผู้ใช้ภายในบล็อก synchronized เพื่อรับประกันพฤติกรรมที่รันเพียงครั้งเดียว
    • ณ JDK 21, synchronized ยังไม่เข้าร่วมกับ cooperative blocking ดังนั้นหากโค้ดดังกล่าวทำ blocking I/O ก็อาจ pin thread จริงไว้ได้
    • เมื่อใช้ -Djdk.tracePinnedThreads=full JDK 21 อาจแสดงคำเตือนเกี่ยวกับสถานการณ์นี้
    • ใน 1.12, lazy-seq และ delay ใช้ lock แทนบล็อก synchronized
  • มีการรวมการแก้ไขด้านความปลอดภัย CVE-2024-22871 และคำแนะนำที่เกี่ยวข้องอยู่ที่ GHSA-vr64-r9qj-h27f
  • มีการกำหนด serialVersionUID ของคลาสที่เกี่ยวข้องกับ Java serialization อย่างชัดเจน
    • ชนิดข้อมูลของ Clojure ได้ implement อินเทอร์เฟซ Java serialization มาตั้งแต่ Clojure 1.0
    • Java serialization จะทำงานเมื่อ identifier ที่สร้างจากชื่อคลาส ลำดับชั้นของชนิด และฟิลด์ serialization ตรงกันในระหว่าง deserialization
    • Clojure ไม่รับประกันความสอดคล้องของ serialization ระหว่างเวอร์ชัน แต่มีการเปลี่ยนแปลงเพื่อเพิ่มการควบคุมในอนาคตไม่ให้ทำลายความเข้ากันได้เกินจำเป็น
  • dependency ต่าง ๆ ก็ได้รับการอัปเดตด้วย
    • spec.alpha อัปเดตเป็น 0.5.238
    • core.specs.alpha อัปเดตเป็น 0.4.74

ฟีเจอร์สำหรับจัดการไลบรารีและเครื่องมือใน REPL

  • ระหว่างการพัฒนา อาจมีสถานการณ์ที่ต้องเพิ่มไลบรารีโดยไม่รีสตาร์ท JVM
    • การประเมินเชิงทดลอง
    • การเพิ่ม dependency ที่รู้จักเข้าไปในโปรเจกต์
    • การเพิ่มไลบรารีสำหรับงานเฉพาะ
  • Clojure 1.12 มีฟังก์ชันใหม่สำหรับเพิ่มไลบรารีโดยไม่สูญเสียสถานะ REPL
    • add-lib: ดาวน์โหลด lib ที่ไม่มีอยู่ใน classpath และเพิ่มเข้า classloader
      • lib ที่มีอยู่แล้วใน classpath จะไม่ถูกอัปเดต
      • หากไม่มีพิกัด จะใช้ Maven เวอร์ชันล่าสุด หรือหากสามารถอนุมานชื่อ git repository ได้ จะใช้ git เวอร์ชันล่าสุดหรือ tag ล่าสุด
    • add-libs: resolve ไลบรารีและเวอร์ชันใหม่หลายรายการพร้อมกัน
    • sync-deps: เรียก add-libs สำหรับ lib ที่อยู่ใน deps.edn แต่ยังไม่อยู่ใน classpath
  • ฟังก์ชันเหล่านี้ตั้งใจให้ใช้เฉพาะ REPL ระหว่างพัฒนา เท่านั้น
    • วิธีที่ถูกต้องสำหรับ build และดูแลโค้ด production ยังคงเป็นการใช้ deps.edn
    • ทั้งสามฟังก์ชันตรวจสอบว่า *repl* ถูก bind เป็น true หรือไม่
    • clojure.main/repl จะ bind flag นี้ให้อัตโนมัติ
    • ใน REPL ของ clojure.main ฟังก์ชันใหม่จะถูก refer เข้า namespace user โดยอัตโนมัติ
    • ใน REPL อื่น ๆ อาจต้องใช้ (require '[clojure.repl.deps :refer :all])
  • การ resolve และดาวน์โหลดไลบรารีดำเนินการโดย tools.deps
    • เพื่อไม่ต้องใส่ tools.deps และ dependency ของมันลงใน classpath ของโปรเจกต์ระหว่างพัฒนา จึงมีการเพิ่ม API ใหม่ที่เรียกฟังก์ชันผ่าน Clojure CLI ในโปรเซสแยกต่างหาก
  • clojure.tools.deps.interop/invoke-tool เรียกฟังก์ชันเครื่องมือใน โปรเซสแยกต่างหาก
    • classpath ของเครื่องมือถูกกำหนดใน deps.edn
    • ไม่จำเป็นต้องเพิ่ม dependency ของเครื่องมือเข้า classpath ของโปรเจกต์
    • ฟีเจอร์ add-lib ถูกสร้างโดยใช้ invoke-tool และยังสามารถใช้ build หรือเรียกเครื่องมือของผู้ใช้แบบโต้ตอบได้ด้วย
    • ดูโปรโตคอลการรันฟังก์ชันได้ใน CLI reference

API สำหรับรันโปรเซสภายนอก

  • นอกจาก namespace clojure.java.shell เดิมแล้ว ยังมีเส้นทางที่ใช้ API ใหม่ของ Java สำหรับข้อมูลโปรเซส การควบคุม และการ redirect I/O
  • Clojure 1.12 เพิ่ม namespace ใหม่ clojure.java.process
    • ใช้ API ใหม่ที่เกี่ยวข้องกับโปรเซสของ Java
    • ออกแบบให้ใช้งานง่ายกว่าวิธีเดิม
  • ฟังก์ชันหลักมีดังนี้
    • start: ควบคุม stream ได้เต็มรูปแบบ และเข้าถึง Java object พื้นฐานได้สำหรับการใช้งานขั้นสูง
    • exec: รองรับกรณีทั่วไปที่รันโปรเซสภายนอกและคืนค่า stdout เมื่อทำงานเสร็จ

การขยายการทำงานร่วมกับ Java

  • เพิ่ม method values ทำให้ใช้ Java method ใน higher-order function ได้โดยตรงมากขึ้น
    • ก่อนหน้านี้ หากต้องการส่ง Java method ให้ map เป็นต้น ต้องห่อเป็นฟังก์ชันเอง
    • การ wrap ด้วยมือยืดยาว และอาจต้องใช้ hint เพื่อแยก overload หรือเกิด reflection/boxing เพิ่มเติม
    • ตอนนี้สามารถใช้ qualified methods ในตำแหน่ง value ได้เหมือนฟังก์ชันทั่วไป และ compiler จะสร้างฟังก์ชัน wrapperให้อัตโนมัติ
    • หาก qualified method ไม่สามารถ resolve ได้เพราะ overload compiler จะสร้างการเรียกแบบ reflection
    • นักพัฒนาสามารถระบุ method signature เดี่ยวที่ต้องการด้วย metadata :param-tags
  • ไวยากรณ์ qualified method ระบุคลาสและเมธอด
    • Classname/method: ค่า Clojure function ที่เรียก static method
    • Classname/.method: ค่า Clojure function ที่เรียก instance method
    • Classname/new: ค่า Clojure function ที่เรียก constructor
    • หากต้องการแยก static method กับ instance method ต้องใช้ไวยากรณ์ Classname/method และ Classname/.method
  • metadata :param-tags ใช้สำหรับ resolve method ที่ overload
    • qualified method ที่ใช้เป็น value ให้เพียงชื่อคลาสและชื่อเมธอด จึงไม่สามารถ resolve overloaded method ได้
    • :param-tags เป็นเวกเตอร์รูปแบบ [tag …] โดยแต่ละ tag สอดคล้องกับพารามิเตอร์ของ signature ที่ต้องการ
    • สำหรับพารามิเตอร์ของชนิดที่ไม่ได้ overload สามารถใช้ placeholder _ ได้
    • เมื่อให้ :param-tags compiler ต้องสามารถ resolve เป็น method เดี่ยวได้ในเวลา compile
    • ไวยากรณ์ metadata reader ใหม่ ^[tag …] จะติด metadata :param-tags ให้กับ member symbol
  • เพิ่ม ไวยากรณ์ array class
    • Clojure รองรับ symbol ชื่อคลาสเป็นค่า class object และ type hint แต่ไม่มีไวยากรณ์ array class นอกจากแบบสตริง
    • ตอนนี้สามารถอ้างอิง array class ด้วย symbol รูปแบบ ComponentClass/#dimensions
    • ตัวอย่าง: String/1, java.lang.String/1, long/2
    • component class อาจเป็นชื่อคลาสเต็ม คลาสที่ import แล้ว หรือ primitive ก็ได้
    • ไวยากรณ์ array class ใช้ได้ทั้งเป็น type hint และเป็น value
  • ปรับปรุงการทำงานร่วมกับ functional interface ของ Java
    • functional interface ของ Java มี @FunctionalInterface และมีเมธอดเดียว
    • ฟังก์ชัน Clojure สามารถส่งให้การเรียกเมธอด Java ที่รับ functional interface ได้ หาก arity ตรงกัน
    • Clojure compiler สร้าง lambda adapter เพื่อแปลง Clojure function เป็น functional interface ที่ต้องการโดยนัย
    • หากต้องการหลีกเลี่ยงการสร้าง adapter ซ้ำใน loop สามารถบังคับอย่างชัดเจนได้โดยใส่ hint ให้ชื่อใน let binding
  • การทำงานร่วมกับ Supplier ก็ดีขึ้นด้วย
    • ก่อนหน้านี้ หากต้องเรียกเมธอดที่รับ Supplier ซึ่งจัดหาค่า ต้องเขียน adapter ด้วย reify
    • ตอนนี้ implementation ของ IDeref ใน Clojure เช่น delay, future, atom implement อินเทอร์เฟซ Supplier โดยตรง

การจัดการ Stream และการปรับปรุงประสิทธิภาพของคอลเลกชัน

  • เพิ่มฟังก์ชันสำหรับบริโภค Stream ที่ Java API ส่งคืนมากขึ้นเรื่อย ๆ ในสไตล์ Clojure
    • มีฟังก์ชันทำงานร่วมกับ Stream พร้อมกับการรองรับ functional interface ใน Clojure 1.12
    • (stream-seq! stream) ⇒ seq
    • (stream-reduce! f [init-val] stream) ⇒ val
    • (stream-transduce! xf f [init-val] stream) ⇒ val
    • (stream-into! to-coll [xf] stream) ⇒ to-coll
    • ทุกฟังก์ชันเป็น terminal stream operation และ consume stream
  • PersistentVector ให้ spliterator โดยตรง ซึ่งใช้ใน implementation ของ stream ใน Java collections
    • spliterator คือ iterator ที่สามารถแบ่งได้เพื่อการ traverse แบบ parallel ที่เร็วขึ้น
    • custom spliterator ใหม่ของ PersistentVector รองรับ parallelism และปรับปรุงประสิทธิภาพอย่างมาก
  • ปรับปรุงประสิทธิภาพของ drop, nthrest, nthnext และการจัดการ partition
    • CLJ-2713 เพิ่มอินเทอร์เฟซภายใน IDrop เพื่อบ่งชี้ว่าคอลเลกชันสามารถ drop ได้มีประสิทธิภาพกว่าการ traverse ตามลำดับ
    • อินเทอร์เฟซนี้ถูก implement ใน persistent collection และคอลเลกชันเชิงอัลกอริทึมอย่าง range, repeat
    • ฟังก์ชันใหม่ partitionv, partitionv-all, splitv-at มีประสิทธิภาพกว่าฟังก์ชันคู่เดิม และสร้าง vector partition แทน realized seq partition

การเข้มงวดนโยบาย Var interning

  • การ intern var ใน namespace ต่างจาก aliasing ตรงที่เป็นการสร้าง reference ที่เสถียร เพื่อให้ทุก reference ได้ object เดียวกัน
  • เดิมมีบางกรณีที่ interned var อาจถูกแทนที่ได้ และใน 1.12.0-alpha1 นโยบายนี้เข้มงวดขึ้น
    • หากเกิดสถานการณ์เช่นนี้ จะมีคำเตือนรูปแบบ "REJECTED: attempt to replace interned var #'some-ns/foo with #'other-ns/foo in some-ns, you must ns-unmap first"
  • นโยบายนี้จัดการสาเหตุรากของปัญหาที่ปรากฏเมื่อ Clojure 1.11.0 เพิ่มฟังก์ชันใหม่เข้า clojure.core โดยเฉพาะ abs
    • หากโค้ดที่คอมไพล์ด้วย Clojure เวอร์ชันก่อนหน้ามีชื่อ var ตรงกับชื่อฟังก์ชันที่เพิ่มใหม่ใน clojure.core เมื่อนำมาโหลดใน runtime 1.11.0 อาจกลายเป็น unbound ได้
    • นอกจาก CLJ-2711 แล้ว การแก้ไขเดิมในส่วนนี้คือ CLJ-1604 ก็ถูก rollback ด้วย

รายการเปลี่ยนแปลงทั้งหมด

  • รายการเปลี่ยนแปลงทั้งหมดของ Clojure 1.12.0 อยู่ใน changelog ทางการ

1 ความคิดเห็น

 
GN⁺ 2024-09-06
ความคิดเห็นบน Hacker News
  • เป็น รีลีสใหญ่ จริง ๆ และมีฟีเจอร์ใหม่เจ๋ง ๆ มากมาย
    โดยส่วนตัวชอบ add-libs ที่สุด ตอนนี้สามารถทำเดโมไฟล์เดียวหรือ minimal example สำหรับการจำลองปัญหาได้แล้ว ทำให้กำแพงในการแชร์โค้ดชิ้นเล็ก ๆ ที่รันได้ลดลงอย่างมาก
    ยังสามารถเดโมไลบรารี Java ได้โดยไม่ต้องมี boilerplate ของ Java ด้วย ลองเล่นโน่นนี่ใน REPL แล้วเอาโค้ดไปวางในที่อย่างคอมเมนต์ HN ได้ จากนั้นใคร ๆ ก็สามารถจำลอง “การตั้งค่า” เดียวกันแล้วรันได้เลย ไม่ต้อง clone repository ด้วยซ้ำ

    • ไม่รู้ว่ายังมีใครจำ Groovy ได้ไหม ใน Groovy มี annotation @Grab ที่ทำงานแทบจะเหมือนกับ add-libs ที่อธิบายไว้ และสะดวกมากสำหรับการเขียนสคริปต์
    • ตอนนี้ Java ก็มี REPL และรองรับ scripting แล้ว แต่ฟีเจอร์แบบ add-libs ยังไม่มีใน meta command ที่ใช้งานได้
  • คิดว่าจะเลื่อนรีลีสนี้ไปจนถึง Clojure/conj 2024 แม้จะไม่มีหลักฐานอะไรเป็นพิเศษ แต่เพราะ Clojure 1.10 ออกมาช่วง Clojure/conj 2021 และการประกาศเปิดให้ใช้ Datomic ฟรีก็เกิดขึ้นช่วงต้น Clojure/conj 2023
    ถึงอย่างนั้นก็ยังรอ spec2 อยู่ ตอนนี้เลี่ยงความแข็งตัวของ spec ด้วย Malli อยู่ แต่ก็ยังไม่ใช่ first-class citizen ใน Clojure สาเหตุหลักคือไม่สามารถตรวจ macro ได้ ซึ่งเป็นส่วนที่ตั้งใจไว้ตามการออกแบบ compiler ของ Clojure อย่างไรก็ตาม หากจัดการ Malli schema เป็นข้อมูล ก็พอเลียนแบบแนวคิดของ schema/select ได้
    ด้วยการเปลี่ยนแปลงเรื่อง functional interface ตอนนี้ส่ง function เข้าไปได้โดยตรง ไม่ต้องคอยดูแล utility macro อย่าง (defmacro ->Consumer [f] ...) อีกแล้ว

    • เห็น “maybe not” แล้วรู้สึกว่าจำเป็นต้องมี schema/select จริง ๆ เลยทำไลบรารีสำหรับ Malli ขึ้นมา: https://github.com/eval/malli-select
    • สงสัยว่า “ไม่สามารถตรวจ macro ได้” หมายความว่าอย่างไร สามารถติด s/fdef ให้ macro ด้วย spec และตรวจการเรียกตอน compile time ได้
      จริง ๆ แล้ว Clojure core เองก็ใช้ spec ตรวจการเรียก macro ด้วย ถ้าเรียกผิด บางครั้งจะเห็นร่องรอยนั้นใน stack trace เลย เลยสงสัยว่าหมายถึงอย่างอื่นหรือเปล่า
  • ชอบมากที่มีฟีเจอร์ใหม่ใส่มาเพียบ แต่โค้ดเดิมยังรันได้เหมือนเดิม ความพยายามต่อเนื่องในการหลีกเลี่ยง การทำให้ compatibility พัง นั้นโดดเด่นจริง ๆ

  • ถ้าอยากรู้จัก Clojure มากขึ้น ลองดูงานประชุม Clojure/conj ที่จัดวันที่ 23–25 ตุลาคมที่ Alexandria รัฐ Virginia: https://2024.clojure-conj.org

  • ดีใจที่มี add-libs และ sync-deps เข้ามา ตอนนี้ดูเหมือนแทบไม่มีเหตุผล—or อาจไม่มีเลย—ที่ต้องปิด session
    รีลีสนี้ดูมีขอบเขตค่อนข้างต่างจากรีลีสก่อน ๆ และมีของใส่เข้ามาเยอะ จึงน่าสนใจ แต่ก็หวังว่าด้วยความเร็วที่เพิ่มขึ้น จะไม่กลายเป็นก้อนที่พันกันยุ่งหลังผ่านไปอีกไม่กี่รีลีส

    • Rich Hickey และ Clojure Team เป็นผู้ออกแบบที่ระมัดระวังมาก จึงคิดว่าไม่ต้องกังวลมากเกินไป
    • เป็นการคาดเดาล้วน ๆ แต่เพราะนี่เป็นรีลีสแรกหลังจาก Rich Hickey ออกจาก nubank เขาอาจมีเวลาทุ่มความสนใจให้มากขึ้นก็ได้
  • การเปลี่ยนแปลงเรื่อง functional interface ใหญ่มาก Clojure ดีที่สุดเวลายืนอยู่ใกล้ Java ผ่าน interoperability ที่รอบคอบ และการเปลี่ยนแปลงครั้งนี้ช่วยอุดช่องว่างใหญ่ช่องหนึ่ง

  • สงสัยว่า spec เป็นอย่างไรบ้าง ถูกทิ้งไปแล้วหรือเปล่า มีข่าวอะไรที่น่าคาดหวังไหม

    • spec ยังคงมีอยู่และใช้งานอยู่ งานต่อยอดก็ทำไปมากแล้ว แต่ตอนนี้หยุดอยู่ระหว่างพิจารณาว่าจะทำอย่างไรกับหลาย ๆ ประเด็น
  • ดูเป็นรีลีสที่ค่อนข้างแข็งแรง และดีใจที่ Clojure ยังไปได้ดี

    • สงสัยว่าไปได้ดีจริงไหม กำลังประเมินว่าจะใช้กับโปรเจกต์ใหม่หรือไม่ และกำลังพิจารณาร่วมกับ Clara[0] อยู่ แต่ดูเหมือนจะไม่ mainstream เท่าเมื่อก่อน และ ecosystem ก็รู้สึกกระจัดกระจายกว่าที่เคย
      ไม่ได้ตั้งใจจะ trolling อยากเลือกใช้จริง ๆ และดูเหมือนเป็นการตัดสินใจที่ดีทางวิศวกรรม แต่ถ้าความนิยมและจำนวนผู้ร่วมพัฒนากำลังลดลงอย่างรวดเร็ว ก็อาจกลายเป็นอุปสรรคในอนาคตอันใกล้ได้
      [0] https://www.clara-rules.org/
  • การเปลี่ยนนักพัฒนาที่มีอยู่ให้เป็น นักพัฒนา Clojure ง่ายกว่าที่เคย
    ปัญหาใหญ่ในช่วงแรก[0] คือการอ่านโค้ด แต่ AI อย่าง ChatGPT หรือ Claude อธิบายโค้ด Clojure เดิมได้เก่งมาก ผลคือ onboarding นักพัฒนาอาจเร็วขึ้นมาก
    [0] ผ่านไปไม่กี่สัปดาห์ การอ่าน Clojure จะเป็นธรรมชาติ และจะลืมไปด้วยซ้ำว่าเมื่อก่อนเคยอ่านไม่ออก

  • มีการปรับปรุงเจ๋ง ๆ มากมาย ปกติเป็น ภาษาในตระกูล Lisp หลักที่มักหยิบมาใช้