ความคิดสะเปะสะปะของผมเกี่ยวกับภาษา Julia

ผมลองเขียน Julia ไปนิดนึงแน่นอนว่าเป็นโปรแกรมตัดคำ ซึ่งก็ไม่น่าจะตรงทางของ Julia เท่าไหร่ ไม่มีทำอะไรกับ vector matrix เลย แต่ก็มีสิ่งที่พบดังต่อไปนี้

index ของ Array เริ่มที่ 1 เหมือน Pascal และ Lua

เป็นภาษา dynamic ที่ระบุ type ได้ละเอียดมากประมาณว่าระบุแบบนี้ได้ Array{Tuple{Int, String, Char},1} สังเกตว่ามี 1 ด้วย คือบอกว่าเป็น Array 1 มิติ

อีกอย่างหนึ่งที่ไม่คิดว่าจะเจอในภาษา dynamic ถ้า type ที่ประกาศเป็น null (nothing) ได้ต้องใส่ Nullable ด้วยเช่น Nullable{Array{Tuple{Int, String, Char},1}}

มีคล้าย Generic ของ Java ด้วยแต่ใน Julia เรียก Parametric Composite Types ไม่แน่ใจว่าต่างกันแค่ไหน แต่ประกาศแบบนี้ได้ ตามคู่มือ

type Point{T}
   x::T
   y::T
end

และตอนเอาไปใช้ก็สามารถใส่ Point{Float64} Point{Int} อะไรแบบนี้ได้

พอเห็น type พวกนี้แล้วน่าจะพูดได้ว่าประกาศ type ได้ละเอียดกว่า Go ที่เป็นภาษาแบบ static type อีกถ้าไม่รวม pointer

ใช้ substring รู้สึกไม่ค่อยสะดวก สิ่งเคยเขียนใน Ruby ประมาณ text[s…e] พอมาแปลงเป็น Julia ก็จะกลายเป็น text[chr2ind(text, s):chr2ind(text, e)] รู้สึกวุ่นและมีโอกาสพลาดเพิ่มขึ้นเยอะ

ความเร็วของโปรแกรมตัดคำที่อุตส่าใช้ Just-in-time compilation ก็ดูเร็วสำหรับภาษา dynamic แต่ก็ยังห่างชั้นจากกลุ่มนำมาก

OOP ใน Julia เน้นไปที่ polymorphism ถ้าดูผิว ๆ ก็จะเหมือนเรียก function ธรรมดา เช่น build(10,5), build(30.4,10.5), build(12,3.4), build(Point(10.2, 2.4) พวกนี้สามารถไปเรียกคนละ method กันได้หมดเลยถาม type ที่ระบุไว้ตอนประกาศ method; นอกจากนั้น method จะเขียน build(10, 5) อะไรแบบนี้ถูกแล้ว ไม่มีมาเขียน 5.build(10) เหมือนในภาษา OOP ส่วนมาก เพราะว่ามันมาแล้ว multiple dispatching คล้าย ๆ CLOS ใน Common Lisp คือ type ของทุก argument มีส่วนเลือก method หมดเลย อาจจะเป็นข้อดีทางอ้อม ๆ คือมันเป็นระเบียบดี ไม่มีแบบจะ len(x) ดีหรือ x.len() ดี

แต่ type ที่ว่ามาเยอะแยะจะไม่ใส่เลยก็ได้

ถ้าโอกาสหน้าเขียนพวก vector matrix หรือคำนวณตัวเลขเยอะ ๆ จะกลับมาลอง Julia อีกที

Go และ Java เดี๋ยวนี้

ผมดู benchmark แบบเน้น CPU มา และ benchmark ของ Web framework ก็เห็นว่าความเร็วไม่ได้ทิ้งกัน บางที Java เร็วกว่าด้วยซ้ำทั้ง ๆ ที่ไม่ได้ build เป็น native code แต่แรกเหมือน Go  แม้กระทั่งโปรแกรมตัดคำเขียน Kotlin เองที่รันบน JVM เหมือน Java พอ optimize พอกันแล้วความเร็วไม่ได้ต่างจาก Go เท่าไหร่  โดยสรุปแล้วทำให้ข้ออ้างว่า Go แปลเป็น machine code แล้วจะทำงานเร็วเป็นการทั่วไปใช้กับ Java ไม่ได้

ความยากง่ายในการเขียน Go ดูจะได้เปรียบร้องมีสิ่งที่ต้องเรียนรู้น้อยกว่า แต่ก็มีประเด็นให้คิดเรื่อง pointer เพิ่มเข้ามา ส่วน Java ได้เปรียบตรงอยู่มานานมีหลายอย่างที่คนคุ้นเคย แต่ว่าภาษาก็มีของใหม่เพิ่มเข้ามาเรื่อย ๆ จนบางทีงง

ประเด็นเรื่อง composition over inheritance ที่เป็นอีกจุดขายของ Go ใน Java 8 ก็เหมือนจะพยายามแก้ลำด้วย interface ที่มี default method ได้จนเกือบ ๆ จะเป็น trait แต่ยังไม่เป็น นี่มันทำให้ Java ใช้ท่า composition ผ่าน interface แบบนี้ได้

Channel เดี๋ยวนี้ในภาษาอื่น ๆ ก็มันจะมีสิ่งทดแทนที่ใช้งานได้คล้าย ๆ กัน

แต่ Go ก็ยังมีข้อดีกว่า Java ที่ผมประจักษ์ได้ คือ:

  1. compile ไวมาก ๆ
  2. โปรแกรม start ไวมาก กรณีนี้ Java ก็พยายามสู้เหมือนกันใน Java 9 แต่ผมเดาว่าคงยังสู้ Go ไม่ได้
  3. ไม่ต้องใช้ IDE ที่ซับซ้อนก็ได้ ที่ผ่านมาผมใช้ Emacs หรือ vscode ก็รู้สึกพอแล้ว

หมายเหตุ: น่าจะมีข้อดีอื่น ๆ ของ Go อีกที่ผมไม่ได้ศึกษา เช่น การใช้หน่วยความจำ GC ที่ latency น้อย

Julia กับ JIT

เรื่องนี้เคยโพสไปแล้วนะครับ เกี่ยว Julia และ Just-in-time compilation (JIT) จริง ๆ ก็มี compiler หลายภาษาที่ใช้เทคนิคนี้ครับ แต่ว่าชอบ Julia ตรงมันดึงเอา native code (แต่แสดงเป็นภาษา assembly ) ให้ดูได้สะดวกดี

ผมอ่าน Wikipedia มา ได้ความว่าคนที่ตีพิมพ์เรื่อง JIT ในคนแรก ๆ ก็คือเจ้าเดิมครับ John McCarthy ที่เป็นคนสร้างภาษา Lisp ในงานนี้ แต่ก็ต่างกับตอนเยอะเหมือนกัน เขาว่าแบบที่ใส่ memory แบบเดี๋ยวนี้ภาษาแรก ๆ คือ Smalltalk 80

ที่ไม่ได้แสดงให้ดูคือ Julia เป็นภาษา dynamic ดังนั้น f(x) นี่ใส่ f(1.8) ก็ได้ ได้ทั้ง float ทั้ง integer สมมุติว่าเราใส่ @code_native(f(1.8)) มันก็จะ gen native code ออกมาอีกแบบสำหรับ float แล้วสมมุติว่า f ถูกเรียกซ้ำ ๆ มันก็จะทำงานได้ไวเหมือนกับว่า compile เอาไว้แต่แรก

เทียบความเร็วโปรแกรมตัดคำที่เขียนด้วยภาษาต่าง ๆ

ผมลองตัดคำบนเอกสารภาษาไทยขนาด 20MB บนเครื่องบน Intel® Core™ i3-4030U CPU @ 1.90GHz × 4 ที่ลง Ubuntu 16.04 ใช้ JVM คือ openjdk version “1.8.0_111”

ใช้ data structure แบบเดียวกันหมด algorithm หลัก ๆ ก็คล้าย ๆ กันถ้าผมไม่พลาดอะไรไป

ผลออกมาโปรแกรม chamkho ที่เขียนด้วย Rust มันก็เร็วจริงตามคาด แต่ที่น่าแปลกใจคือ yaito ที่ใช้ Kotlin ผสม Clojure นิด ๆ เร็วกว่าใช้ Go เกือบเท่าตัว

update1: หลังจากที่คุณ @iporsut patch mapkha ไปตอนนี้เวลาดีขึ้นเยอะแล้วจาก 12 วินาที ลดมาเหลือ 9-10 วินาที แต่ว่าอันดับยังไม่เปลี่ยน

update2: หลังจากที่คุณ @iporsut patch ครั้งที่ 2, อันดับก็เปลี่ยนแล้วครับ mapkha ที่ใช้ Go ไวขึ้นแบบเหลือแค่ 5 วินาที ตามหลัง Rust มาติด ๆ

update3: ผมลองแก้ yaito ที่เขียนด้วย Kotlin เป็นหลักตามแบบคุณ @iporsut ที่นี้พบว่าอันดับเปลี่ยนอีกแล้ว yaito ตอนนี้เร็วที่สุด เหลือ 4.311 วินาทีครับ เร็วกว่า chamkho ที่เขียนด้วย Rust อีก แต่ยังไม่ได้ optimize chamkho

update4: หลังจากที่คุณ @iporsut tune การอ่านไฟล์ และผมเพิ่ม test case และแก้ bug ของ mapkha ไปครับ

update5: มีจุดน่าสังเกตอีกอย่างคือ JVM บางทีใช้ CPU มากกว่า 1 core ครับ ผมใช้คำสั่ง time แล้วเอาเวลา real มาเปรียบเทียบ มันจะขนาดมิติว่าถ้ารันหลาย ๆ งานไป

update6: ปรับ chamkho ที่เขียนด้วย Rust ให้ใช้วิธีคล้าย mapkha และ yaito พบว่า chamkho กลับมาเร็วที่สุดอีกครั้ง โดยใช้เวลา 3.284 วินาที

update7: คุณ @iporsut ปรับ mapkha ที่เขียนด้วย Go ให้ไม่ต้องสร้าง EdgeBuilder ใหม่บ่อย ๆ

update8: เพิ่ม Julia

update9: เพิ่ม JS

update10: เพิ่มภาษา Crystal

หากท่านใดต้องปรัปรุงโปรแกรมได้อีก ก็ขอเรียนเชิญ pull request มาได้เลยครับ มี project ทั้งบน gitlab และ github ชื่อเดียวกันครับ

ตารางทดสอบรอบที่ 1

โปรแกรม ภาษาโปรแกรม ครั้ง 1 (วินาที) ครั้ง 2 (วินาที) ครั้ง 3 (วินาที) เฉลี่ย (วินาที)
mapkha golang 1.7.4 12.733 12.651 12.441 12.608
mapkha (patch โดยคุณ @iporsut) golang 1.7.4 9.983 10.007 9.979 9.989
mapkha (patch โดยคุณ @iporsut ครั้งที่ 2) golang 1.7.4 5.062 4.938 5.014 5.005
mapkha (update 4) golang 1.7.4 5.415 5.405 5.416 5.412
mapkha (update 7) golang 1.7.4 5.431 5.449 5.385 5.422
yaito kotlin 1.0.6 มี clojure 1.8.0 นิดหน่อย 6.547 6.743 6.628 6.639
yaito ที่แก้ลอกเลียนคุณ @iporsut kotlin 1.0.6 มี clojure 1.8.0 นิดหน่อย 4.287 4.249 4.399 4.312
chamkho rust nightly 2017-01-08 4.826 4.829 4.85 4.835
chamkho update 6 rust nightly 2017-01-08 3.366 3.247 3.241 3.284
wordcut-clj clojure 1.8.0 มี kotlin 1.0.6 นิดหน่อย 63.502 67.561 67.303 66.122
wordcutpy python 3.5.2 50.624 50.803 50.869 50.765
wordcut.jl (update 8) Julia 0.5.0 38.316 38.112 38.237 38.221
prasae (update 9) node.js v6.5.0 49.349 49.084 49.901 49.445
kachet (update 10) Crystal 0.20.5 5.637 5.679 5.649 5.655

ป.ล. ขอบคุณท่าน @iporsut ที่ refactor Mapkha ให้สวยงามขึ้นครับ

ลอง Kotlin: กรณีโปรแกรมตัดคำ

ตัดคำเขียนด้วย Kotlin เกือบ ๆ หมดครับ มีส่วนอ่านไฟล์กับจัดการพวก command line argument ใช้ Clojure เพราะว่าไม่น่าจะมีผลอะไรกับความเร็วมาก

ชื่อโปรแกรมนึกไม่ออกจะเอาชื่อสถานที่ในระยองมาตั้งก็ยกมาแทบหมดแล้ว เห็นมี LexTo บน JVM ก็เลยตั้งชื่ออันนี้ว่า yaito แล้วกัน ในการพัฒนาก็ไม่ได้เกี่ยวอะไรกันเลย; yaito นี่พยายามเอา wordcut-py กับ wordcut-clj ยำกัน

ตอนใช้จริงๆ โหลด yaito-cliก็พอ ถ้า code สำคัญ ๆ อยู่ที่ yaito

หลังจากลองเขียนไปก็มีประเด็นมาเล่าเกี่ยวกับ Kotlin อยู่บ้าง

  • เขียนแบบข้างล่างเจ๊ง
    if (link?.isBetterThan(selectedLink)) {
    

    เพราะว่าเขียนข้างบนถ้า link เป็น null แล้ว ?.isBetterThan ก็จะ ตอบ null มาด้วยแล้ว แต่ if ต้องการ true/false

    ท่าแก้ท่านหึ่งคือเขียนแบบนี้ เพราะว่าผมมั่นใจว่า link ไม่เป็น null แน่ ๆ

    if (link?.isBetterThan(selectedLink)!!) {
    

    แต่ว่าใช้ link!!.isBetterThan แต่แรกไปเลยดีกว่า ?

  • เวลามี val path: Array ใน data class แล้วมันเตือน จริง ๆ คงเตือน array ทั้งหมด แต่ผมยังงง ๆ อยู่ว่าเดือนทำไม
  • ใน PatLinkBuilder ทีแรกเขียนแบบนี้
     var s = null
    

    แล้วพอพยายามเขียนให้ s = context.i เจ๊งเลย ต้องเปลี่ยนไปแบบข้างล่าง

     var s: Int? = null
    

    ก็เป็นอันใช้ได้

  • ลองเขียนแบบ
    open class PatLinkBuilder(val isAcceptable) : LinkBuilder
    

    แล้วเอาไปใช้แบบนี้

    class SpaceLinkBuilder: PatLinkBuilder( { it == ' '} )
    

    พบว่าเจ๊งต้องระบบ type ของ isAcceptable ถึงใช้ไ้ด

    open class PatLinkBuilder(val isAcceptable: (Char) -> Boolean) : LinkBuilder
    
  • ทีแรกเขียนแบบนี้
     val linkBuilder = arrayOf(DixLinkBuilder(dix)),
    

    แล้วเอา linkBuilder ไปใส่ให้ buildPath ก็เจ๊งเพราะมัน infer type ให้เป็น DixLinkBuilder array แต่เวลาใช้เจอ LinkBuilder array เวลาใช้งานก็ต้องไประบุ type ของ linkBuilder อยู่ดี

  • ชอบอันนี้
    return Link(s = context.leftBoundary,
                        linkType = LinkType.UNK,
                        wordCount = source.wordCount + 1,
                        unkCount = source.unkCount + 1)
    

    เวลาเจอ argument เยอะ ๆ มันจะพลาดได้นี่ใส่เป็น keyboard แทนได้ ต่อให้ check type มันก็พลาดง่าย ๆ อยู่ดี มี integer เพียบ

สรุปคือเรื่องจัดการ null ก็มึน ๆ นิดหน่อยแต่ก็แก้ได้ไม่ยากเท่าไหร่เพราะ IntelliJ IDEA มันเก่ง ถ้าใช้ Emacs เขียนท่าจะยาก; type inference มันใช้ได้ในกรณีง่าย ๆ กรณีไม่ง่ายก็ตามมาใส่เองอยู่ดี; ใส่ keyword ได้เจ๋งดี แต่ Python ก็มี; แต่รวม ๆ แล้วก็เขียนเสร็จไว ติดแต่ละประเด็นไม่นาน

ส่วะประสิทธิภาพออกมาประทับใจมาก จากเมื่อวานลอง wordcut-clj ซึ่งใช้ Clojure เยอะหน่อย ตัดคำบนไฟล์ 21 MB; จาก wordcut-clj ใช้เวลาประมาณ 1 นาที 8 วินาที ขนาดว่าใช้ prefixtree ที่ด้วย Kotlin แล้ว, ตัวใหม่ที่เขียนนี้ไวขึ้นเยอะตัดเสร็จใน 7.1 วินาที คือเร็วสูสี Rust เลย

เขียน Clojure ไปเรียก Kotlin อีกที

wordcut-clj เป็นโปรแกรมตัดตำที่เขียนด้วย Clojure ผมอยากจะเปลี่ยนจากใช้ sorted list ไปใช้ prefix-tree ที่เขียนด้วย Kotlin แทน

ขั้นแรกผมเอา prefixtree ไปใส่ใน Clojars ก่อนเลยจะได้เรียกมากใช้ง่าย ๆ เพิ่งทราบว่า push ด้วย Maven ก็ได้

แล้วก็เรียกมาใช้ผ่านทาง dep ปกติ

ใน Clojure ก็ import เข้ามาได้เหมือนเขียนด้วย Java เลย

(import net.veerkesto.PrefixTree)

แต่ว่าก็จะมีส่วนที่งง ๆ หน่อยคือ Pair ในเอกสารของ Kotlin บอกว่าอยู่ใน stdlib.kotlin แต่พอพยายามโหลดใช้ไม่ได้เป็น kotlin.Pair ได้เลย

(import kotlin.Pair)

ตอนเรียกใช้ data class ใน Kotlin ผมก็มั่ว ๆ เอาเวลาจะดึง childId ก็สั่งแบบนี้

(.getChildId child)

แต่พอพยายามจะ (.getIsFinal child) มันทำไม่ได้ พบว่าใช้

(.isFinal child)

แบบนี้ได้เลย ที่เหลือก็ตรง ๆ เหมือนเรียก Java ทั้งหมด ดู code เต็ม ๆ ได้ที่ gitlab ครับ

ตัดข้อความประมาณ 20MB ก็ใช้เวลาพอ ๆ กับ Python คือประมาณ 1 นาที 5-7 วินาที ซึ่งเร็วกว่า Python อยู่สัก 1-2 วินาที แน่นอนว่าช้ากว่าใช้ Rust มาก

ลองทำ maven repository ดู

ผมพยายามจะเอา .jar ขึ้น repository แบบ

lein deploy clojars

แต่ไม่รู้ว่า maven มันทำอย่างไร ก็เลยลองทำ repository ใช้เองดู ในเครื่องผมมี openjdk8 อยู่แล้ว

  1. ก็ไปเอา Apache Archiva มาลงได้เลย พอแตก zip ออกมาได้ก็ cd เข้า bin ไปรับ ./archiva console
  2. เข้า web port 8080
  3. สร้าง admin
  4. สร้าง folder ใน repositories/snapshots ตามชื่อ group-id, artifact-id, version เช่น veer66/prefixtree/1.0-SNAPSHOT
  5. เอาไฟล์ .jar เช่น prefixtree-1.0-SNAPSHOT.jar ไปใส่ใน veer66/prefixtree/1.0-SNAPSHOT ข้างบน
  6. เข้าไปเว็บสั่ง scan directory, สั่งทำ index ใน http://localhost:8080/#repositorylist

เสร็จแล้ว ผมก็ใช้ได้นะครับ แต่แน่นอนว่าผมมั่ว ๆ เอา ถ้าทำตามอาจจะมีรูรั่วหรือความเจ๊งอะไรเกิดขึ้นก็ได้ ซึ่งผมไม่สามารถรับผิดชอบได้

repo1

ไฟล์พวก .xml .md5 .sha1 นี่ Archiva มัน gen ให้เองครับ

ป.ล. คิดว่าตอนชาวบ้านเอาไปใช้วุ่นวายตาย ผมกะว่าพยายาม build Kotlin source code ใน lein แล้วเอาขึ้น clojars ดีกว่า 😛

ลอง Kotlin

พอดีมีดราม่าที่คุณ @iporsut ยกขึ้นมา ก็เลยไปอ่าน ๆ ดูก็รู้สึกว่าน่าสนใจดี

ก็เลยลอง port prefixtree จาก Rust มาลง Kotlin ดู code อยู่ที่ gitlab

เขียนง่ายถ้าเอาแบบตื้น ๆ ดู reference ก็เข้าใจเลย

เทียบกับ Scala ผมว่า Kotlin ง่ายกว่ามาก ๆ เพราะป่านนี้ผมยังไม่รู้ว่าจะเริ่มเขียน Scala อย่างไรเลย

เทียบกับ Java แล้วคิดว่าน่าจะยากง่าย ๆ พอ ๆ กัน ในคะแนน kotlin ตรงเขียน main แล้ว println ได้เลย แต่ตรงพวก data class รู้สึกว่ามันต้องเรียนรู้มากกว่า ๋Java

ข้อดีคือมันเขียนสั้นดี ไม่โดนหันเหความสนใจเพราะรายละเอียดที่มากเกินไป

Tomee 7.0.2, JSON date serialization, and workaround

When @Produces({ MediaType.APPLICATION_JSON }) and @XmlRootElement are put on a service and model classes respectively, Tomee serializes an entity to JSON automatically.

However, when java.util.Date is serialized, then the result was very strange, I expected this

"2012-01-02T11:12+07"

but what I actually got from Tomee 7.0.2 was

"25550102111200+0700"

So I tested with Tomee 1.7.4 instead of 7.0.2; and it works. Apache web site mentioned that Johnzon is used in Tomee 7.0.2, so I thought maybe the problem is there. I added a customized JohnzonConverter as follow:

In model:

@XmlRootElement
@Entity
public class Word {
...
    @JohnzonConverter(MyDateConverter.class)
    @Temporal(TemporalType.TIMESTAMP)
    private Date t1;
 ...
}

MyDateConverter.java:

import org.apache.johnzon.mapper.Converter;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import static java.util.Locale.ENGLISH;

public class MyDateConverter implements Converter<Date> {

    @Override
    public String toString(Date date) {
        return new SimpleDateFormat("yyyy-MM-dd'T'HH:mmX", ENGLISH).format(date);
    }

    @Override
    public Date fromString(String s) {
        try {
            return new SimpleDateFormat("yyyy-MM-dd'T'HH:mmX", ENGLISH).parse(s);
        } catch (ParseException e) {
            throw new IllegalArgumentException(e);
        }
    }
}

Then even Tomee 7.0.2 works in the way I expected.

P.S. I added this to pom.xml too

<dependency>
  <groupId>org.apache.johnzon</groupId>
  <artifactId>johnzon-mapper</artifactId>
  <version>1.0.0</version>
  <scope>provided</scope>
</dependency>