Clojure 1.12.0 เปิดตัว
(clojure.org)- 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และฟังก์ชันจัดการ JavaStream - ด้านประสิทธิภาพและความเข้ากันได้ มีการเพิ่ม
PersistentVectorspliterator, การจัดการ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=fullJDK 21 อาจแสดงคำเตือนเกี่ยวกับสถานการณ์นี้ - ใน 1.12,
lazy-seqและdelayใช้ lock แทนบล็อกsynchronized
- ก่อน 1.12,
- มีการรวมการแก้ไขด้านความปลอดภัย 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.238core.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 เข้า namespaceuserโดยอัตโนมัติ - ใน REPL อื่น ๆ อาจต้องใช้
(require '[clojure.repl.deps :refer :all])
- วิธีที่ถูกต้องสำหรับ build และดูแลโค้ด production ยังคงเป็นการใช้
- การ 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
- classpath ของเครื่องมือถูกกำหนดใน
API สำหรับรันโปรเซสภายนอก
- นอกจาก namespace
clojure.java.shellเดิมแล้ว ยังมีเส้นทางที่ใช้ API ใหม่ของ Java สำหรับข้อมูลโปรเซส การควบคุม และการ redirect I/O - Clojure 1.12 เพิ่ม namespace ใหม่
clojure.java.process- ใช้ API ใหม่ที่เกี่ยวข้องกับโปรเซสของ Java
- ออกแบบให้ใช้งานง่ายกว่าวิธีเดิม
- ฟังก์ชันหลักมีดังนี้
การขยายการทำงานร่วมกับ 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
- ก่อนหน้านี้ หากต้องการส่ง Java method ให้
- ไวยากรณ์ qualified method ระบุคลาสและเมธอด
Classname/method: ค่า Clojure function ที่เรียก static methodClassname/.method: ค่า Clojure function ที่เรียก instance methodClassname/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-tagscompiler ต้องสามารถ 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 ให้ชื่อใน
letbinding
- functional interface ของ Java มี
- การทำงานร่วมกับ
Supplierก็ดีขึ้นด้วย- ก่อนหน้านี้ หากต้องเรียกเมธอดที่รับ
Supplierซึ่งจัดหาค่า ต้องเขียน adapter ด้วยreify - ตอนนี้ implementation ของ
IDerefใน Clojure เช่นdelay,future,atomimplement อินเทอร์เฟซ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
- CLJ-2713 เพิ่มอินเทอร์เฟซภายใน
การเข้มงวดนโยบาย 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 1.12.0 อยู่ใน changelog ทางการ
1 ความคิดเห็น
ความคิดเห็นบน Hacker News
เป็น รีลีสใหญ่ จริง ๆ และมีฟีเจอร์ใหม่เจ๋ง ๆ มากมาย
โดยส่วนตัวชอบ add-libs ที่สุด ตอนนี้สามารถทำเดโมไฟล์เดียวหรือ minimal example สำหรับการจำลองปัญหาได้แล้ว ทำให้กำแพงในการแชร์โค้ดชิ้นเล็ก ๆ ที่รันได้ลดลงอย่างมาก
ยังสามารถเดโมไลบรารี Java ได้โดยไม่ต้องมี boilerplate ของ Java ด้วย ลองเล่นโน่นนี่ใน REPL แล้วเอาโค้ดไปวางในที่อย่างคอมเมนต์ HN ได้ จากนั้นใคร ๆ ก็สามารถจำลอง “การตั้งค่า” เดียวกันแล้วรันได้เลย ไม่ต้อง clone repository ด้วยซ้ำ
คิดว่าจะเลื่อนรีลีสนี้ไปจนถึง 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] ...)อีกแล้วจริง ๆ แล้ว 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
รีลีสนี้ดูมีขอบเขตค่อนข้างต่างจากรีลีสก่อน ๆ และมีของใส่เข้ามาเยอะ จึงน่าสนใจ แต่ก็หวังว่าด้วยความเร็วที่เพิ่มขึ้น จะไม่กลายเป็นก้อนที่พันกันยุ่งหลังผ่านไปอีกไม่กี่รีลีส
การเปลี่ยนแปลงเรื่อง functional interface ใหญ่มาก Clojure ดีที่สุดเวลายืนอยู่ใกล้ Java ผ่าน interoperability ที่รอบคอบ และการเปลี่ยนแปลงครั้งนี้ช่วยอุดช่องว่างใหญ่ช่องหนึ่ง
สงสัยว่า spec เป็นอย่างไรบ้าง ถูกทิ้งไปแล้วหรือเปล่า มีข่าวอะไรที่น่าคาดหวังไหม
ดูเป็นรีลีสที่ค่อนข้างแข็งแรง และดีใจที่ Clojure ยังไปได้ดี
ไม่ได้ตั้งใจจะ trolling อยากเลือกใช้จริง ๆ และดูเหมือนเป็นการตัดสินใจที่ดีทางวิศวกรรม แต่ถ้าความนิยมและจำนวนผู้ร่วมพัฒนากำลังลดลงอย่างรวดเร็ว ก็อาจกลายเป็นอุปสรรคในอนาคตอันใกล้ได้
[0] https://www.clara-rules.org/
การเปลี่ยนนักพัฒนาที่มีอยู่ให้เป็น นักพัฒนา Clojure ง่ายกว่าที่เคย
ปัญหาใหญ่ในช่วงแรก[0] คือการอ่านโค้ด แต่ AI อย่าง ChatGPT หรือ Claude อธิบายโค้ด Clojure เดิมได้เก่งมาก ผลคือ onboarding นักพัฒนาอาจเร็วขึ้นมาก
[0] ผ่านไปไม่กี่สัปดาห์ การอ่าน Clojure จะเป็นธรรมชาติ และจะลืมไปด้วยซ้ำว่าเมื่อก่อนเคยอ่านไม่ออก
มีการปรับปรุงเจ๋ง ๆ มากมาย ปกติเป็น ภาษาในตระกูล Lisp หลักที่มักหยิบมาใช้