ฟีเจอร์แปลกตาของ Raku
(buttondown.com)- Raku เป็นเหมือน ห้องทดลองฟีเจอร์ภาษา ที่ทดลองไอเดียหลายอย่างโดยตรง มากกว่าจะเป็นการออกแบบหนึ่งเดียวที่ขัดเกลาจนสมบูรณ์ จึงทำให้เห็นการทดลองที่ภาษาอื่นอาจนำไปใช้ได้รวมอยู่ในที่เดียว
- Junctions เผยแพร่การดำเนินการไปยังชุดของค่าที่เป็นไปได้ แล้วพับกลับด้วยตัวระบุปริมาณแบบบูลีนอย่าง
all·any·oneทำให้เขียนเงื่อนไขซับซ้อนได้สั้นลง *ของ Whatevers ใช้กับช่วงไม่สิ้นสุด, ฟังก์ชันนิรนาม และการทำดัชนีตามความยาวของลิสต์ ทำให้เกิดสไตล์โค้ดที่กระชับแต่ไม่คุ้นตา- Regex ของ Raku ยอมทิ้งความเข้ากันได้กับ PCRE เพื่อแลกกับฟีเจอร์อำนวยความสะดวก เช่น ไวยากรณ์ที่ประกอบกันได้, ตัวคั่นค่า, lookahead ที่อ่านง่าย และการละเว้นช่องว่างเป็นค่าเริ่มต้น
- เมื่อดูตั้งแต่ Hyperoperators, Pair Syntax, Slangs, RakuAST ไปจนถึง Rakudo Star จะเห็นว่า Raku เป็นภาษาที่ทดลองอย่างกว้างขวาง ตั้งแต่ไวยากรณ์ไปจนถึงวิธีการจัดจำหน่าย
เหตุผลที่ Raku ดูเหมือนห้องทดลองฟีเจอร์ภาษา
- หลังจากใช้งาน Raku จริงมานานกว่า 1 ปี Raku ดูเป็นภาษาที่ใกล้เคียงกับ “ห้องทดลองสำหรับฟีเจอร์ภาษา”
- โมเดล concurrency 5 แบบ และวิธีทำงานเดียวกันหลายวิธี แสดงให้เห็นลักษณะการทดลองแบบ “ดูว่าจะเกิดอะไรขึ้น”
- เหตุผลที่หลายฟีเจอร์มีปฏิสัมพันธ์กันอย่างประหลาดและมี edge case มาก ก็เพราะการขัดเกลาฟีเจอร์เหล่านั้นให้ทำงานร่วมกันได้นั้นยากกว่าการรันการทดลอง 100 อย่างมาก
- ฟีเจอร์ที่กล่าวถึงที่นี่คือการทดลองแบบ Raku ที่สักวันหนึ่งอาจเห็นได้ในภาษาโปรแกรมอื่นด้วย
Junctions: การซ้อนทับของค่าที่เป็นไปได้และตัวระบุปริมาณแบบบูลีน
- Junctions แทน การซ้อนทับของค่าที่เป็นไปได้ และเมื่อใช้การดำเนินการกับ junction การดำเนินการเดียวกันจะถูกนำไปใช้กับค่าทั้งหมดภายใน
2|10กลายเป็นany(2, 10)2&10 + 3กลายเป็นall(5, 13)(1&2) + (10^20)ถูกประกอบเป็นแบบall(one(11, 21), one(12, 22))
- การใช้งานหลักของ Junctions คือการแสดง สูตรบูลีน อย่าง
all,any- ไม่มีวิธีแยกโครงสร้าง junction
- หากต้องการใช้งานจริง ต้องพับให้เป็นบูลีนก่อน
soบังคับแปลง junction ให้เป็นบูลีนso (1&2) + (10^20) < 15ได้Trueso (1&2) + (10^20) > 0ได้False
list.allของ Raku ไม่ใช่เมธอดที่รับอาร์กิวเมนต์ แต่เป็น เมธอดไร้อาร์กิวเมนต์ ที่เปลี่ยนลิสต์ให้เป็น junction<1 2 3>.allกลายเป็นall(1, 2, 3)- เมื่อใช้
is-prime($x)จะถูกนำไปใช้กับแต่ละค่า เช่นall(False, True, True)
- เมื่อประกอบ Junctions เข้าด้วยกัน ก็สามารถสร้างพฤติกรรมแบบ
uniqueได้- สามารถแสดง “สมาชิกทุกตัวเท่ากับสมาชิกหนึ่งตัวพอดีหรือไม่” ด้วย
{.all == .one} <1 2 3 7>เป็นจริง และ<1 2 3 7 2>เป็นเท็จ
- สามารถแสดง “สมาชิกทุกตัวเท่ากับสมาชิกหนึ่งตัวพอดีหรือไม่” ด้วย
Whatevers: ช่วงไม่สิ้นสุดและฟังก์ชันนิรนามที่สร้างด้วย *
*ของ Whatevers คือ สัญลักษณ์ whatever ที่รับหลายบทบาทใน Raku- ในช่วงหรือซีเควนซ์
*หมายถึงไม่มีขอบเขต1..*คือ1..Inf(2,4,8...*)[17]คือ262144
- นิพจน์ที่มี
*จะถูกยกขึ้นเป็นฟังก์ชันนิรนาม- พฤติกรรมนี้เรียกว่า whatever-priming
- ผลลัพธ์คือ
WhateverCodeและนอกจากชนิดแล้วก็แยกไม่ออกจากฟังก์ชันอื่น (* + 10)(2)ได้12(^10).map(* % 2)สร้างแพตเทิร์น0 1 0 1 ...
- หากมี
*สองตัวในนิพจน์ แต่ละตัวจะเป็นอาร์กิวเมนต์ตามตำแหน่งคนละตัว(2, 30, 4, 50).map(* + *)คืนค่า(32, 54)- พฤติกรรมนี้มีประโยชน์กับการนิยาม Fibonacci ที่ซับซ้อน แต่ก็ไม่แน่ชัดว่าดีกว่าวิธีที่ให้
*แต่ละตัวเป็นค่าเดียวกันหรือไม่
- เมธอดจำนวนมากของ Raku รับฟังก์ชันได้ ทำให้ priming ถูกใช้ได้กว้าง
l[*-1]ดูเหมือนดัชนีติดลบ แต่จริง ๆ แล้ว[]ส่งความยาวของลิสต์ให้ฟังก์ชันเพื่อคำนวณดัชนีที่จะเข้าถึง- หากความยาวลิสต์เป็น 10,
l[*-1]จะเป็นl[10-1]หรือสมาชิกตัวสุดท้าย l.head(2)หมายถึงสมาชิกสองตัวแรก ส่วนl.head(*-2)หมายถึงทั้งหมด ยกเว้นสมาชิกสองตัวสุดท้าย
- หากส่งฟังก์ชันอื่นให้
[]ก็สามารถทำโครงสร้างอย่าง ring buffer ได้ง่าย@x[95 % *]--คำนวณตำแหน่งโดยใช้ความยาวของลิสต์
Regex: ไวยากรณ์ที่ประกอบกันได้โดยทิ้งความเข้ากันได้กับ PCRE
- Raku ใช้ ไวยากรณ์ regex ใหม่ ที่ต่างจาก POSIX regex หรือ PCRE
- POSIX regex เป็นโครงสร้างที่มี backslash และเครื่องหมายวรรคตอนจำนวนมาก
- PCRE เข้ากันได้ย้อนหลังกับ POSIX และสืบทอดโครงสร้าง backslash·เครื่องหมายวรรคตอนที่ซับซ้อนกว่าเดิม
- Perl 6 ตัดความเข้ากันได้ย้อนหลังและนำไวยากรณ์ regex ใหม่มาใช้
- จุดปรับปรุงที่เด่นที่สุดคือ ความสามารถในการประกอบกัน
- ในหลายภาษา การรวม regex ทำโดยนำสตริง regex มาต่อกัน
- Raku สามารถแทรก regex อื่น เช่น
/< foo >+/เพื่อแมตช์ regexfooหนึ่งครั้งขึ้นไป โดยไม่ให้หลุดออกไปยัง regex ชั้นบน
- สามารถแบ่ง regex ที่ซับซ้อนออกเป็นส่วนย่อยที่เล็กลงและอ่านง่ายขึ้นได้
- วิธีนี้เป็นรากฐานที่ทำให้ Raku รองรับ ไวยากรณ์การพาร์ส ในฐานะฟีเจอร์ของภาษาในตัว
- เมื่อยอมตัดความเข้ากันได้ย้อนหลัง ก็มีการเพิ่ม ฟีเจอร์อำนวยความสะดวก หลายอย่าง
\d+ % ','แมตช์1,1,2,1,1,4แต่ไม่แมตช์1,หรือ12- lookahead และ non-capturing group ไม่ใช่ชุดสัญลักษณ์ที่อ่านยาก
r1 && r2แมตช์เฉพาะสตริงที่ตรงทั้งr1และr2- สามารถหยุด backtracking ได้ด้วย
: - โดยค่าเริ่มต้น ช่องว่างจะถูกละเว้น และต้องเปิดใช้อย่างชัดเจนในแพตเทิร์นการแมตช์
- ไวยากรณ์ regex ของ Raku สามารถมองได้ว่าเป็นฟีเจอร์ที่อาจปรากฏในภาษาอื่นได้ โดยแยกจากความสามารถในการประมวลผลจริง
Hyperoperators: mapping แบบทั่วไปที่ลงไปถึงลิสต์ซ้อน
- รูปแบบพื้นฐานของ Hyperoperators คือ
l>>.methodซึ่งคล้ายกับmapแต่ ลงไปในลิสต์ย่อยแบบ recursive[1, [2, 3], 4]>>.succได้[2 [3 4] 5]
- ใน Raku การเรียกฟังก์ชันรูปแบบ
f(list, *args)สามารถเปลี่ยนเป็นรูปแบบเมธอดlist.&f(* args)ได้- ดังนั้น
>>.จึงทำงานเหมือนตัวดำเนินการ mapping แบบทั่วไป - สามารถใช้ร่วมกับ Whatever ได้ เช่น
[1, [2, 3], 4]>>.&(*+1)
- ดังนั้น
- binary hyperoperator แบบ
l1 << op >> l2จะ นำopไปใช้กับแต่ละสมาชิก ของสองลิสต์- ลิสต์ที่สั้นกว่าจะวนซ้ำจนกว่าลิสต์ที่ยาวกว่าจะจบ
[1, 2, 3, 4, 5] <<+>> [10, 20]ได้[11 22 13 24 15]- รูปแบบ
<< op<<หรือ>>op>>จะวนซ้ำเฉพาะจนกว่าลิสต์ด้านซ้ายหรือด้านขวาจะจบเท่านั้น
- ใช้ได้กับค่าเดี่ยวและลิสต์ซ้อนด้วย
[1, 2, 3, 4, 5] <<+>> 10ได้[11 12 13 14 15][1, [2, 3], 4, 5] <<+>> [10, 20]ได้[11 [22 23] 14 25]
- hyperoperator สำหรับ hash สองตัว ยังมีพฤติกรรมแยกต่างหากที่นำ
opไปใช้กับ union, intersection และ hash difference - ชื่อ Hyperoperator มาจากการที่ทุกการดำเนินการถูก ประเมินแบบขนาน
- ลูปแบบระบุชัดก็สามารถทำให้ขนานได้ด้วย prefix
hyper
- ลูปแบบระบุชัดก็สามารถทำให้ขนานได้ด้วย prefix
Pair Syntax: หน่วยพื้นฐานของ hash และการส่ง flag อย่างกระชับ
- Pair คือหน่วยพื้นฐานที่ประกอบเป็น hash ของ Raku
- hash คือกลุ่มของคู่
key => value - pair เป็น type พื้นฐาน และ hash คือคอลเลกชันของ pair
- hash คือกลุ่มของคู่
- Raku มี colon syntax จำนวนมากสำหรับระบุ pair แบบสั้น
my $x = 3; :$xคือx => 3:a<$x>คือa => "$x":a($x)คือa => 3:3aคือa => 3
- รูปย่อที่สำคัญที่สุดคือ
:keyและ:!key- แมปเป็น
key => Trueและkey => Falseตามลำดับ - ใช้เป็นวิธีที่กระชับในการใส่ flag ให้เมธอด
- แมปเป็น
- เมธอด
matchรับ colon pair หลายตัวเหมือน flag- ใช้รูปแบบอย่าง
:continue(:$c),:pos(:$p),:global(:$g),:overlap(:$ov) :f(:$foo)ใน signature จะนิยามพารามิเตอร์$fooพร้อมสร้าง alias:f- ผู้เรียกสามารถตั้ง flag เดียวกันได้ด้วย
:fหรือ:foo
- ใช้รูปแบบอย่าง
- อาร์กิวเมนต์ colon pair มีความยืดหยุ่นทั้งลำดับและตำแหน่ง
"abab".match(/../, :g)ทำการแมตช์แบบ global"abab".match(/../, :g, :ov)ทำการแมตช์แบบ overlap- สามารถสลับลำดับและเขียนติดกันได้ เช่น
"abab".match(:g:ov, /../) - สามารถวางอาร์กิวเมนต์คีย์เวิร์ดหลังการเรียกฟังก์ชันได้ เช่น
"abab".match(:g, /../):ov:2nd
- ไวยากรณ์แบบนี้ช่วยระบุตัวเลือกของเมธอดได้สั้นกว่า
match(global=True, overlap=True)มาก
Slangs, RakuAST, Rakudo Star
- Slangs คือฟีเจอร์สำหรับเปลี่ยนไวยากรณ์ของ Raku
- ใช้ได้กับงานอย่างการแก้ไวยากรณ์ลูป, เปลี่ยน identifier, เพิ่ม actor, เพิ่มซีเควนซ์ DNA
- ตัวอย่างที่ยกมาคือ Slang::Otherwise, Slang::Piersing, OO::Actors, BioInfo
- RakuAST คือฟีเจอร์สำหรับพาร์สนิพจน์ Raku เป็น AST ภายใน Raku เอง
Q/my $x; $x++/.ASTแสดงการประกาศตัวแปรและการดำเนินการเพิ่มค่าแบบ postfix ในรูปRakuAST::StatementList- มีตัวอย่างการส่งออกโค้ด Raku เป็นสัญกรณ์แบบภาษาอื่น เช่น
Q/my $x; put $x/.AST.DEPARSE("NL")
- Raku มีชุดติดตั้ง Rakudo Star
- distribution นี้ติดตั้ง โมดูล third-party ที่คัดเลือกไว้ล่วงหน้าให้
- เป็นเหมือนทางประนีประนอมระหว่างภาระในการดูแล standard library ขนาดใหญ่ กับภาระที่ผู้ใช้ต้องหาแพ็กเกจที่เหมาะสมจาก ecosystem เอง
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
ก่อนหน้านี้ผมเคยทำสิ่งที่คล้ายกับฟีเจอร์ regular expression แบบประกอบกันได้ ที่เคยพูดถึงกันที่นี่ เป็น implementation แยกต่างหากสำหรับ JavaScript และแม้จะผ่านมาหลายปีแล้ว มันก็ยังเป็นหนึ่งในไลบรารีที่ผมหยิบไปใช้ในโปรเจกต์อื่นบ่อยที่สุด
มันทำให้รู้สึกใกล้เคียงขึ้นอีกนิดกับการมี parser เป็น first-class citizen อยู่ในภาษา และมีตัวอย่างการใช้สิ่งนี้ทำ parser สำหรับ media type ด้วย regular expression: https://runkit.com/tolmasky/media-type-parsing-with-template...
บน npm คือ
templated-regular-expression, GitHub: https://github.com/tolmasky/templated-regular-expressionแน่นอนว่าภาษาโปรแกรมมิงควรมี parser ของจริง และไม่ควรใช้ regular expression เป็น parser แต่ถ้ายังไงก็ต้องใช้ regular expression อยู่แล้ว การแบ่งมันเป็นชิ้นเล็ก ๆ ได้ก็ยอดเยี่ยมจริง ๆ
regular expression ของ Raku เมื่อใช้ร่วมกับ grammar จะทรงพลังกว่ามาก และถ้าเขียนดี ๆ ก็เข้าใจง่ายกว่า “parser ของจริง” ใด ๆ ด้วยซ้ำ ถ้าจะ parse Raku ด้วย parser ทั่วไป จำเป็นต้องสามารถเพิ่มและลบรายการใน parser ระหว่างการ parse ได้ ซึ่ง parser ของ Raku จัดการเรื่องนี้โดย subclass grammar ปัจจุบัน เพื่อเพิ่ม·ลบรายการภายใน lexical scope ปัจจุบัน แล้วเมื่อ scope จบลงก็ย้อนกลับไปใช้ grammar เดิม
ใน Raku regular expression คืออีก syntax หนึ่งสำหรับเขียนโค้ด และต่างจาก syntax หลักกับพฤติกรรมพื้นฐานเพียงเล็กน้อยเท่านั้น มันสามารถมีพารามิเตอร์และตัวแปรได้ ถ้างานใดไม่เหมาะกับ syntax ของ regular expression ก็สามารถแทรก syntax ปกติของ Raku เข้าไปทำสิ่งที่ต้องการ แล้วกลับมายัง syntax ของ regular expression ได้
ตัวอย่างเช่น สามารถ match
Aหนึ่งตัวขึ้นไป ตามด้วยBจำนวนเท่ากัน และCจำนวนเท่ากันได้ด้วย ใน syntax regular expression ส่วนใหญ่ ถ้าจะ matchAกับBให้มีจำนวนเท่ากันต้องใช้ recursion ตรงกลาง แต่ถ้าทำแบบนั้นจะจัดการCในแบบเดียวกันได้ยาก และอ่านยากขึ้นมาก วิธีของ Raku ไม่ต้องใช้ backtracking หรือ recursion จึงอาจรันได้ค่อนข้างเร็วถ้าห่อด้วย grammar ก็จะได้ parse tree ที่สมบูรณ์ และแม้จะทำได้โดยไม่ใช้ grammar แต่ใช้ grammar จะง่ายกว่า ตัวอย่าง parser จริงที่มักแนะนำคือ JSON::TINY::Grammar: https://github.com/moritz/json/blob/master/lib/JSON/Tiny/Gra...
พูดตามตรง ส่วนสำคัญไม่น้อยของการออกแบบ “parser ของจริง” ดูเหมือนเป็นผลพลอยได้จากข้อจำกัดด้าน RAM ของคอมพิวเตอร์ยุคแรก ๆ ขั้นตอน tokenization ที่แยกออกมาต่างหากมีไว้เพื่อลด RAM ที่ซอร์สโค้ดใช้ เพื่อเหลือหน่วยความจำไว้สำหรับการวิเคราะห์ความหมายและการคอมไพล์ในขั้นต่อไป และผมไม่คิดว่ามันทำให้ขั้นตอนถัด ๆ ไปง่ายขึ้นมากขนาดนั้น โมดูล JSON::Tiny ข้างต้นสร้างโครงสร้างข้อมูล native ของ Raku ด้วย action class ระหว่างที่ grammar กำลัง parse ดังนั้นจึงเหมือนกับ parse ไปพร้อมกับ compile ไปด้วย
ใช้ Raku ใน production อยู่ การประมวลผลข้อความ นี่เป็นภาษาที่ดีที่สุด และประสบการณ์ในการสร้าง parser ก็ดีมาก
น่าแปลกใจที่มันไม่ได้เป็นภาษาระดับท็อปสำหรับสร้าง LLM text pipeline
ว้าว ผมอยากออกจากวงการก่อนถึงวันที่ต้องมาดูแล codebase ของ Raku เลย
ถ้าดูหนังสือ Raku สำหรับผู้เริ่มต้น จะมีหลายส่วนที่ให้ความรู้สึกเหมือน Python ที่ติด syntax ตระกูล C เข้าไป เป็น syntax ที่เน้นวงเล็บปีกกา แต่ทั้งความใช้งานง่าย โครงสร้างข้อมูล built-in และฟีเจอร์เชิงวัตถุล้วนอยู่ในระดับสูง
ถ้ารู้จักภาษาสคริปต์ระดับสูงอื่น ๆ โค้ด Raku ของสคริปต์ที่คล้ายกันน่าจะอ่านได้ค่อนข้างง่าย ผมไม่คิดว่าคนส่วนใหญ่จะใช้ฟีเจอร์แปลกมาก ๆ ในโค้ดประจำวัน และ Raku แม้จะยืดหยุ่นกว่า แต่โดยปกติโค้ดก็ไม่ได้ดูเข้าใจยาก
ใน Raku จะเห็นจุดนั้นได้จากความสามารถในการนิยาม keyword argument แบบย่อ เช่น
:global(:$g)หรือความสามารถในการเรียก global regex match อย่างmatch(/foo/, :g)โดยถือว่าค่าเป็นTrueใน Perl ก็มีอะไรแบบนี้เยอะ และทั้งหมดมีไว้เพื่อให้ใช้ได้เร็วขึ้นและสนุกขึ้น แต่สำหรับมือใหม่จะอ่านยากลงต่อไปคงกลายเป็นช่องว่างระหว่างรุ่น โปรแกรมเมอร์รุ่นเก่าทั้งรุ่นอาจรู้สึกเหมือนถูกกันออกจากสภาพแวดล้อมการทำงานที่สำหรับคนรุ่นปัจจุบันถือเป็นเรื่องปกติ
ฟีเจอร์บางอย่างคุ้น ๆ อยู่ครึ่งหนึ่ง Hyper ฟังดูเหมือนเป็นเวอร์ชันที่ทำแบบฉับพลันกว่าอะไรสักอย่างในสาย recursion-schemes และ
*ที่เห็นตรงนี้ก็คล้ายกับ_ของ Scala อยู่บ้าง_ใช้กับ lambda ได้ดี จนอยากให้ทุกภาษารับสิ่งคล้าย ๆ กันไปใช้> (2, 30, 4, 50).map(* + *) returns (32, 45)ไม่ควรเป็นreturns (32, 54)เหรอ? เพราะพจน์ที่สองคือ 4+50หรือบางทีอาจเป็นผลจาก การแปลนำหน้า ที่มาจากบางประเทศที่พูดแบบ “four and fifty” เช่น vierenvijftig แทน fifty-four ในภาษาอังกฤษ
(32 54)น่าจะเป็นคนเขียนเผลอพิมพ์ผิดถ้าอย่างนั้น ตอนนี้ Perl ก็เป็นเหมือนยาเริ่มต้นสำหรับไปสู่ ภาษาในตระกูล APL แล้วสินะ?
ตัวอย่างเช่น สามารถใช้ reduce meta operator
[ ]ร่วมกับ infix+เพื่อ “รวมทั้งหมด” ได้:[+] 1, 2, 3Raku ยังมีปัญหาใหญ่เรื่อง ความเร็ว อยู่ การแยกวิเคราะห์ไฟล์ล็อกด้วย regex เป็นจุดแข็งของ Perl แต่แม้แต่ Raku รุ่นล่าสุด หากไม่นับเวลาเริ่มต้น ก็ยังใช้เวลานานกว่า Python 3.13 ถึง 6.5 เท่า
น่าทึ่งจริง ๆ ที่
(2,4,8...*)[17]กลายเป็น262144หากไม่นับ
Whateverความใหม่หลังจาก Perl6/Raku น่าจะอยู่ที่ lazy evaluation(1,3,9...*)[4,5]จะได้(81 243)และ(1,3,9...*)[(1..3)]จะได้(3 9 27)ยังซ้อนกันได้ด้วย เช่นนิพจน์อย่าง
(1,2,4...*)[(1,2,4...*)[1,2,3]]ก็ทำได้เป็นการคัดฟีเจอร์ที่น่าสนใจของ Raku มาพูดถึง
ผมมองว่าฟีเจอร์สำคัญที่สุดของ Raku คือ อินเทอร์เฟซบรรทัดคำสั่ง และ grammar
การรองรับอินเทอร์เฟซบรรทัดคำสั่งเป็นฟีเจอร์ที่พบได้ทั่วไป เช่นมีการใช้งานและกรณีนำ
docoptมาใช้ แต่ใน Raku ฟีเจอร์นี้มีมาในตัวและใช้งานได้ดีส่วน grammar เป็นฟีเจอร์ที่พบได้ยาก เพราะภาษาโปรแกรมมิงมี grammar เป็น องค์ประกอบชั้นหนึ่ง และทำให้สามารถสร้างและประกอบ grammar ด้วยการเขียนโปรแกรมเชิงวัตถุได้
ผมติดตามโปรเจกต์นี้มาหลายปีและก็คิดว่าน่าสนใจ แต่ก็น่าเสียดายจริง ๆ ที่ Perl 6 ดูเหมือนจะหลงทางไปไกลเกินไปใน การเดินทางสู่โลกหลังกระจก นี้