|>
) สำหรับ JavaScript (เอกสารนี้ใช้ %
เป็นโทเค็นตัวยึดตำแหน่งสำหรับการอ้างอิงหัวข้อ นี่ แทบจะไม่ใช่ตัวเลือกสุดท้ายอย่างแน่นอน ดูการอภิปรายโทเค็นการปั่นจักรยานเพื่อดูรายละเอียด)
ในแบบสำรวจ State of JS 2020 คำตอบอันดับสี่ ของ "คุณรู้สึกว่า JavaScript ขาดหายไปในขณะนี้" เคยเป็น พนักงานเดินท่อ ทำไม
เมื่อเราดำเนิน การติดต่อกัน (เช่น การเรียกใช้ฟังก์ชัน) กับ ค่า ใน JavaScript ขณะนี้มีสองรูปแบบพื้นฐาน:
นั่นคือ three(two(one(value)))
กับ value.one().two().three()
อย่างไรก็ตาม สไตล์เหล่านี้มีความแตกต่างกันมากในด้านความสามารถในการอ่าน ความคล่องแคล่ว และการนำไปใช้
โดยทั่วไปแล้ว รูปแบบแรกคือ การซ้อน ซึ่งใช้ได้กับลำดับการดำเนินการใดๆ เช่น การเรียกใช้ฟังก์ชัน เลขคณิต ตัวอักษรอาร์เรย์/อ็อบเจ็กต์ await
และ yield
เป็นต้น
อย่างไรก็ตาม การซ้อนเป็น เรื่องยากที่จะอ่าน เมื่อมันลึกลงไป: โฟลว์ของการดำเนินการจะเลื่อน จากขวาไปซ้าย แทนที่จะอ่านโค้ดปกติจากซ้ายไปขวา หากในบางระดับมี ข้อโต้แย้งหลายข้อ การอ่านจะตี กลับไปมา ตาของเราต้อง ข้ามไปทางซ้าย เพื่อค้นหาชื่อฟังก์ชัน จากนั้นจะต้อง ข้ามไปทางขวา เพื่อค้นหาข้อโต้แย้งเพิ่มเติม นอกจากนี้ การแก้ไข โค้ดในภายหลังอาจเต็มไปด้วยอุปสรรค: เราต้องหา ตำแหน่งที่ถูกต้องเพื่อแทรก อาร์กิวเมนต์ใหม่ลงใน วงเล็บที่ซ้อนกันหลายอัน
พิจารณาโค้ดในโลกแห่งความเป็นจริงจาก React
console . log (
chalk . dim (
`$ ${ Object . keys ( envars )
. map ( envar =>
` ${ envar } = ${ envars [ envar ] } ` )
. join ( ' ' )
} ` ,
'node' ,
args . join ( ' ' ) ) ) ;
โค้ดในโลกแห่งความเป็นจริงนี้สร้างขึ้นจาก นิพจน์ที่ซ้อนกันอย่างลึกซึ้ง ในการอ่านการไหลของข้อมูล ดวงตาของมนุษย์จะต้อง:
ค้นหา ข้อมูลเริ่มต้น (นิพจน์ที่อยู่ด้านในสุด envars
)
จากนั้นสแกน กลับไปกลับมา ซ้ำๆ จาก ภายในสู่ภายนอก สำหรับการแปลงข้อมูลแต่ละครั้ง โดยแต่ละตัวจะมีตัวดำเนินการคำนำหน้าที่พลาดได้ง่ายทางด้านซ้ายหรือตัวดำเนินการต่อท้ายทางด้านขวา:
Object.keys()
(ด้านซ้าย).map()
(ด้านขวา).join()
(ด้านขวา)chalk.dim()
(ด้านซ้าย) จากนั้นconsole.log()
(ด้านซ้าย)เนื่องจากการซ้อนนิพจน์จำนวนมากเข้าด้วยกัน (บางนิพจน์ใช้ตัวดำเนินการ คำนำหน้า บางนิพจน์ใช้ตัวดำเนิน การ postfix และบางนิพจน์ใช้ตัวดำเนิน การ circumfix ) เราจึงต้องตรวจสอบ ทั้งด้านซ้ายและด้านขวา เพื่อค้นหา ส่วนหัว ของ แต่ละนิพจน์
รูปแบบที่สองคือ method chaining ใช้ได้ ก็ต่อ เมื่อค่ามีฟังก์ชันที่กำหนดให้เป็น วิธีการ สำหรับคลาสเท่านั้น นี่ เป็นการจำกัด การบังคับใช้ แต่ เมื่อ นำมาใช้ ต้องขอบคุณโครงสร้าง postfix โดยทั่วไปจึงใช้งานได้มากกว่าและอ่านและเขียน ได้ง่ายกว่า การเรียกใช้โค้ดจะไหล จากซ้ายไปขวา การแสดงออกที่ซ้อนกันอย่างลึกซึ้งจะ ไม่พันกัน อาร์กิวเมนต์ทั้งหมดสำหรับการเรียกใช้ฟังก์ชันจะ ถูกจัดกลุ่ม ตามชื่อของฟังก์ชัน และการแก้ไขโค้ดในภายหลังเพื่อ แทรกหรือลบ การเรียกใช้เมธอดเพิ่มเติมนั้นเป็นเรื่องเล็กน้อย เนื่องจากเราเพียงแค่ต้องวางเคอร์เซอร์ไว้ที่จุดเดียว จากนั้นจึงเริ่มพิมพ์หรือลบอักขระ ที่ต่อเนื่อง กันหนึ่งตัว
จริงๆ แล้ว ข้อดีของการผูกมัดวิธีการนั้น น่าสนใจมาก จน ไลบรารียอดนิยมบางแห่งบิดเบือน โครงสร้างโค้ดของตนโดยเฉพาะเพื่อให้สามารถ ผูกมัดวิธีการได้มากขึ้น ตัวอย่างที่โดดเด่นที่สุดคือ jQuery ซึ่งยังคงเป็น ไลบรารี JS ที่ได้รับความนิยมมากที่สุด ในโลก การออกแบบหลักของ jQuery นั้นเป็น über-object เดียวที่มีวิธีการมากมาย ซึ่งทั้งหมดนี้ส่งคืนประเภทวัตถุเดียวกันเพื่อให้เราสามารถ เชื่อมโยงต่อไปได้ มีแม้กระทั่งชื่อสำหรับรูปแบบการเขียนโปรแกรมนี้: อินเทอร์เฟซที่คล่องแคล่ว
น่าเสียดาย เพื่อความคล่องแคล่วทั้งหมด การผูกมัดวิธีการ เพียงอย่างเดียวไม่สามารถรองรับ ไวยากรณ์อื่นๆ ของ JavaScript ได้ เช่น การเรียกใช้ฟังก์ชัน เลขคณิต ตัวอักษรอาร์เรย์/อ็อบเจ็กต์ await
และ yield
เป็นต้น ด้วยวิธีนี้ การผูกมัดวิธีการยังคง ถูกจำกัด ใน การบังคับใช้
ผู้ดำเนินการไปป์พยายามที่จะรวม ความสะดวกสบาย และความง่ายของ วิธีการผูกมัดเข้า กับ การนำไปใช้ อย่างกว้างขวางของ การซ้อนนิพจน์
โครงสร้างทั่วไปของตัวดำเนินการไปป์ทั้งหมดคือ value |>
e1 |>
e2 |>
e3 โดยที่ e1 , e2 , e3 คือนิพจน์ทั้งหมดที่ใช้ค่าที่ต่อเนื่องกันเป็นพารามิเตอร์ จากนั้นตัวดำเนินการ |>
จะใช้เวทย์มนตร์ในระดับหนึ่งเพื่อ "ไปป์" value
จากด้านซ้ายไปทางด้านขวา
สานต่อโค้ดโลกแห่งความเป็นจริงที่ซ้อนกันอย่างลึกซึ้งจาก React:
console . log (
chalk . dim (
`$ ${ Object . keys ( envars )
. map ( envar =>
` ${ envar } = ${ envars [ envar ] } ` )
. join ( ' ' )
} ` ,
'node' ,
args . join ( ' ' ) ) ) ;
…เราสามารถ แก้ให้หายยุ่ง ได้โดยใช้ตัวดำเนินการไปป์และโทเค็นตัวยึด ( %
) แทนค่าของการดำเนินการก่อนหน้า:
Object . keys ( envars )
. map ( envar => ` ${ envar } = ${ envars [ envar ] } ` )
. join ( ' ' )
| > `$ ${ % } `
| > chalk . dim ( % , 'node' , args . join ( ' ' ) )
| > console . log ( % ) ;
ตอนนี้ ผู้อ่านที่เป็นมนุษย์สามารถ ค้นหา ข้อมูลเริ่มต้น ได้อย่างรวดเร็ว (สิ่งที่เป็นการแสดงออกที่อยู่ด้านในสุดที่สุด envars
) จากนั้นอ่าน เชิงเส้น จาก ซ้ายไปขวา การเปลี่ยนแปลงแต่ละครั้งของข้อมูล
อาจมีคนแย้งว่าการใช้ ตัวแปรชั่วคราว ควรเป็นวิธีเดียวที่จะแก้ให้หายยุ่งกับโค้ดที่ซ้อนกันลึก การตั้งชื่อตัวแปรทุกขั้นตอนอย่างชัดเจนทำให้เกิดสิ่งที่คล้ายกับการเชื่อมโยงเมธอดเกิดขึ้น พร้อมประโยชน์ที่คล้ายคลึงกันในการอ่านและการเขียนโค้ด
ตัวอย่างเช่น การใช้ตัวอย่างโลกแห่งความเป็นจริงที่ได้รับการดัดแปลงก่อนหน้านี้จาก React:
Object . keys ( envars )
. map ( envar => ` ${ envar } = ${ envars [ envar ] } ` )
. join ( ' ' )
| > `$ ${ % } `
| > chalk . dim ( % , 'node' , args . join ( ' ' ) )
| > console . log ( % ) ;
…เวอร์ชันที่ใช้ตัวแปรชั่วคราวจะมีลักษณะดังนี้:
const envarString = Object . keys ( envars )
. map ( envar => ` ${ envar } = ${ envars [ envar ] } ` )
. join ( ' ' ) ;
const consoleText = `$ ${ envarString } ` ;
const coloredConsoleText = chalk . dim ( consoleText , 'node' , args . join ( ' ' ) ) ;
console . log ( coloredConsoleText ) ;
แต่มีเหตุผลว่าทำไมเราจึงพบนิพจน์ที่ซ้อนกันอย่างลึกซึ้งในโค้ดของกันและกัน ตลอดเวลาในโลกแห่งความเป็นจริง แทนที่จะ เป็นบรรทัดของตัวแปรชั่วคราว และมีเหตุผลว่าทำไม อินเทอร์เฟซที่คล่องแคล่วตามเมธอดเชน ของ jQuery, Mocha และอื่น ๆ ยังคง ได้รับความนิยม
การเขียน โค้ดที่มีลำดับตัวแปรชั่วคราวแบบใช้ครั้งเดียวยาวๆ มักจะ น่าเบื่อและใช้คำ มากเกินไป อาจเป็นเรื่องที่น่าเบื่อและมีเสียงดังสำหรับมนุษย์ในการ อ่าน ด้วยซ้ำ
หาก การตั้งชื่อ เป็นหนึ่งใน งานที่ยากที่สุด ในการเขียนโปรแกรม โปรแกรมเมอร์จะ หลีกเลี่ยงการตั้งชื่อตัวแปรอย่างหลีกเลี่ยงไม่ได้ เมื่อพวกเขาเห็นว่าประโยชน์ของมันมีขนาดเล็ก
อาจมีคนแย้งว่าการใช้ ตัวแปรที่ไม่แน่นอน ตัวเดียวที่มีชื่อสั้นจะลดคำของตัวแปรชั่วคราวลง และบรรลุผลลัพธ์ที่คล้ายคลึงกันกับตัวดำเนินการไปป์
ตัวอย่างเช่น ตัวอย่างโลกแห่งความเป็นจริงที่ได้รับการดัดแปลงก่อนหน้านี้จาก React สามารถเขียนใหม่ได้ดังนี้:
let _ ;
_ = Object . keys ( envars )
. map ( envar => ` ${ envar } = ${ envars [ envar ] } ` )
. join ( ' ' ) ;
_ = `$ ${ _ } ` ;
_ = chalk . dim ( _ , 'node' , args . join ( ' ' ) ) ;
_ = console . log ( _ ) ;
แต่โค้ดแบบนี้ ไม่ธรรมดา ในโค้ดในโลกแห่งความเป็นจริง เหตุผลประการหนึ่งก็คือ ตัวแปรที่ไม่แน่นอนสามารถ เปลี่ยนแปลงได้โดยไม่คาดคิด ทำให้เกิดข้อบกพร่องแบบเงียบๆ ที่หายาก ตัวอย่างเช่น ตัวแปรอาจถูกอ้างอิงโดยบังเอิญในการปิด หรืออาจถูกกำหนดใหม่โดยไม่ตั้งใจภายในนิพจน์
// setup
function one ( ) { return 1 ; }
function double ( x ) { return x * 2 ; }
let _ ;
_ = one ( ) ; // _ is now 1.
_ = double ( _ ) ; // _ is now 2.
_ = Promise . resolve ( ) . then ( ( ) =>
// This does *not* print 2!
// It prints 1, because `_` is reassigned downstream.
console . log ( _ ) ) ;
// _ becomes 1 before the promise callback.
_ = one ( _ ) ;
ปัญหานี้จะไม่เกิดขึ้นกับตัวดำเนินการไปป์ ไม่สามารถกำหนดโทเค็นหัวข้อใหม่ได้ และโค้ดที่อยู่นอกแต่ละขั้นตอนจะไม่สามารถเปลี่ยนการเชื่อมโยงได้
let _ ;
_ = one ( )
| > double ( % )
| > Promise . resolve ( ) . then ( ( ) =>
// This prints 2, as intended.
console . log ( % ) ) ;
_ = one ( ) ;
ด้วยเหตุนี้ โค้ดที่มีตัวแปรที่ไม่แน่นอนจึงอ่านได้ยากเช่นกัน ในการพิจารณาว่าตัวแปรแสดงถึงอะไร ณ จุดที่กำหนด คุณต้อง ค้นหาขอบเขตก่อนหน้าทั้งหมด เพื่อหาตำแหน่งที่ มีการกำหนดตัวแปรใหม่
ในทางกลับกัน การอ้างอิงหัวข้อของไปป์ไลน์มีขอบเขตคำศัพท์ที่จำกัด และการผูกจะไม่เปลี่ยนรูปภายในขอบเขต ไม่สามารถมอบหมายใหม่โดยไม่ได้ตั้งใจ และสามารถนำมาใช้อย่างปลอดภัยในการปิด
แม้ว่าค่าหัวข้อจะเปลี่ยนไปตามแต่ละขั้นตอนของไปป์ไลน์ แต่เราสแกนเฉพาะขั้นตอนก่อนหน้าของไปป์ไลน์เพื่อให้เข้าใจได้ง่ายขึ้น ซึ่งนำไปสู่โค้ดที่อ่านง่ายกว่า
ข้อดีอีกประการหนึ่งของตัวดำเนินการไปป์เหนือลำดับของคำสั่งการมอบหมาย (ไม่ว่าจะมีตัวแปรชั่วคราวที่ไม่เปลี่ยนรูปหรือเปลี่ยนรูปไม่ได้ก็ตาม) ก็คือว่ามันเป็น นิพจน์
นิพจน์ไปป์คือนิพจน์ที่สามารถส่งคืนโดยตรง กำหนดให้กับตัวแปร หรือใช้ในบริบท เช่น นิพจน์ JSX
ในทางกลับกัน การใช้ตัวแปรชั่วคราวจำเป็นต้องมีลำดับของคำสั่ง
ไปป์ไลน์ | ตัวแปรชั่วคราว |
---|---|
const envVarFormat = vars =>
Object . keys ( vars )
. map ( var => ` ${ var } = ${ vars [ var ] } ` )
. join ( ' ' )
| > chalk . dim ( % , 'node' , args . join ( ' ' ) ) ; | const envVarFormat = ( vars ) => {
let _ = Object . keys ( vars ) ;
_ = _ . map ( var => ` ${ var } = ${ vars [ var ] } ` ) ;
_ = _ . join ( ' ' ) ;
return chalk . dim ( _ , 'node' , args . join ( ' ' ) ) ;
} |
// This example uses JSX.
return (
< ul >
{
values
| > Object . keys ( % )
| > [ ... Array . from ( new Set ( % ) ) ]
| > % . map ( envar => (
< li onClick = {
( ) => doStuff ( values )
} > { envar } < / li >
) )
}
< / ul >
) ; | // This example uses JSX.
let _ = values ;
_ = Object . keys ( _ ) ;
_ = [ ... Array . from ( new Set ( _ ) ) ] ;
_ = _ . map ( envar => (
< li onClick = {
( ) => doStuff ( values )
} > { envar } < / li >
) ) ;
return (
< ul > { _ } < / ul >
) ; |
มี ข้อเสนอที่แข่งขันกันสองข้อ สำหรับผู้ควบคุมท่อ: ท่อแฮ็กและท่อ F# (ก่อนหน้านั้น มี ข้อเสนอที่สามสำหรับ "การผสมผสานอย่างชาญฉลาด" ของข้อเสนอสองข้อแรก แต่ถูกถอนออกแล้ว เนื่องจากไวยากรณ์ของมันเป็นชุดที่เหนือกว่าของข้อเสนอข้อใดข้อหนึ่งอย่างเคร่งครัด)
ข้อเสนอไปป์ทั้งสองแตกต่างกัน เล็กน้อย ว่า "เวทย์มนตร์" คืออะไรเมื่อเราสะกดโค้ดเมื่อใช้ |>
ข้อเสนอ ทั้งสอง นำ แนวคิดภาษาที่มีอยู่มาใช้ซ้ำ: ไปป์แฮ็กจะขึ้นอยู่กับแนวคิดของ นิพจน์ ในขณะที่ไปป์ F# ขึ้นอยู่กับแนวคิดของ ฟังก์ชันเอกนารี
นิพจน์ การวางท่อและ ฟังก์ชันเอกนารี ของการวางท่อมี ความสัมพันธ์ กัน เล็กน้อย และเกือบจะสมมาตรกัน
ในรูปแบบไปป์ของ ภาษา Hack ทางด้านขวาของไปป์คือ นิพจน์ ที่มี ตัวยึดตำแหน่ง พิเศษ ซึ่งได้รับการประเมินโดยตัวยึดตำแหน่งเชื่อมโยงกับผลลัพธ์ของการประเมินการแสดงออกของด้านซ้าย นั่นคือเราเขียน value |> one(%) |> two(%) |> three(%)
ไปยัง value
ไปป์ผ่านฟังก์ชันทั้งสาม
ข้อดี: ทางด้านขวามือสามารถเป็น expression ใดก็ได้ และ placeholder สามารถไปได้ทุกที่ที่ตัวระบุตัวแปรปกติสามารถไปได้ เพื่อให้เราสามารถไพพ์ไปยังโค้ดใดๆ ที่เราต้องการ โดยไม่มีกฎพิเศษใดๆ :
value |> foo(%)
สำหรับการเรียกใช้ฟังก์ชันเอกนารีvalue |> foo(1, %)
สำหรับการเรียกใช้ฟังก์ชัน n-aryvalue |> %.foo()
สำหรับการเรียกเมธอดvalue |> % + 1
สำหรับเลขคณิตvalue |> [%, 0]
สำหรับตัวอักษรอาร์เรย์value |> {foo: %}
สำหรับตัวอักษรของวัตถุvalue |> `${%}`
สำหรับตัวอักษรเทมเพลตvalue |> new Foo(%)
สำหรับการสร้างวัตถุvalue |> await %
สำหรับการรอคอยสัญญาvalue |> (yield %)
สำหรับค่าตัวสร้างผลผลิตvalue |> import(%)
สำหรับการเรียกคำหลักที่มีลักษณะคล้ายฟังก์ชัน คอนดิชั่น: การส่งไพพ์ผ่าน ฟังก์ชันเอกนารี นั้น ละเอียดกว่าเล็กน้อย เมื่อใช้ไพพ์ Hack มากกว่าไพพ์ F# ซึ่งรวมถึงฟังก์ชันเอกนารีที่สร้างขึ้นโดย ไลบรารีที่เรียกใช้ฟังก์ชัน อย่าง Ramda ตลอดจนฟังก์ชันลูกศรเอกนารีที่ทำการ ทำลายล้างโครงสร้างที่ซับซ้อน บนอาร์กิวเมนต์: ท่อแฮ็กจะมีรายละเอียดมากขึ้นเล็กน้อยด้วยคำต่อท้ายการเรียกใช้ฟังก์ชัน ที่ชัดเจน (%)
(การทำลายโครงสร้างที่ซับซ้อนของค่าหัวข้อจะง่ายขึ้นเมื่อมีความคืบหน้าของนิพจน์ เนื่องจากคุณจะสามารถกำหนดตัวแปร/ทำลายโครงสร้างภายในตัวไปป์ได้)
ในรูปแบบไปป์ของ ภาษา F# ทางด้านขวาของไปป์คือนิพจน์ที่ต้อง ประเมินเป็นฟังก์ชันเอกนารี ซึ่งจากนั้นจะ ถูกเรียกโดยปริยาย โดยมีค่าของด้านซ้ายเป็น อาร์กิวเมนต์เดียว นั่นคือเราเขียน value |> one |> two |> three
ลงใน value
ไพพ์ผ่านฟังก์ชันทั้งสาม left |> right
กลายเป็น right(left)
สิ่งนี้เรียกว่าการเขียนโปรแกรมโดยปริยายหรือรูปแบบไร้จุด
ตัวอย่างเช่น การใช้ตัวอย่างโลกแห่งความเป็นจริงที่ได้รับการดัดแปลงก่อนหน้านี้จาก React:
Object . keys ( envars )
. map ( envar => ` ${ envar } = ${ envars [ envar ] } ` )
. join ( ' ' )
| > `$ ${ % } `
| > chalk . dim ( % , 'node' , args . join ( ' ' ) )
| > console . log ( % ) ;
…เวอร์ชันที่ใช้ไปป์ F# แทนไปป์แฮ็กจะมีลักษณะดังนี้:
Object . keys ( envars )
. map ( envar => ` ${ envar } = ${ envars [ envar ] } ` )
. join ( ' ' )
| > x => `$ ${ x } `
| > x => chalk . dim ( x , 'node' , args . join ( ' ' ) )
| > console . log ;
ข้อดี: ข้อจำกัดที่ด้านขวามือ ต้อง แก้ไขเป็นฟังก์ชันเอกนารีทำให้เราเขียนไพพ์ที่สั้นมาก เมื่อ การดำเนินการที่เราต้องการดำเนินการคือ การเรียกใช้ฟังก์ชันเอกนารี :
value |> foo
สำหรับการเรียกใช้ฟังก์ชันเอกนารี ซึ่งรวมถึงฟังก์ชันเอกนารีที่สร้างขึ้นโดย ไลบรารีที่เรียกใช้ฟังก์ชัน อย่าง Ramda เช่นเดียวกับฟังก์ชันลูกศรเอกนารีที่ทำการ ทำลายโครงสร้างที่ซับซ้อน บนอาร์กิวเมนต์: ไปป์ F# จะมี รายละเอียดน้อยกว่าเล็กน้อย เมื่อมีการเรียกใช้ฟังก์ชัน โดยนัย (no (%)
)
ข้อเสีย: ข้อจำกัดหมายความว่า การดำเนินการใด ๆ ที่ดำเนินการโดย ไวยากรณ์อื่น ๆ จะต้อง ละเอียดมากขึ้นเล็กน้อย โดย การล้อม การดำเนินการใน ฟังก์ชันลูกศร เอกนารี :
value |> x=> x.foo()
สำหรับการเรียกเมธอดvalue |> x=> x + 1
สำหรับเลขคณิตvalue |> x=> [x, 0]
สำหรับตัวอักษรอาร์เรย์value |> x=> ({foo: x})
สำหรับตัวอักษรของวัตถุvalue |> x=> `${x}`
สำหรับตัวอักษรเทมเพลตvalue |> x=> new Foo(x)
สำหรับการสร้างวัตถุvalue |> x=> import(x)
สำหรับการเรียกคำหลักที่มีลักษณะคล้ายฟังก์ชันแม้แต่การเรียกใช้ ฟังก์ชันที่มีชื่อ ก็ต้องมี การหุ้ม เมื่อเราต้องการส่งผ่าน มากกว่าหนึ่งอาร์กิวเมนต์ :
value |> x=> foo(1, x)
สำหรับการเรียกใช้ฟังก์ชัน n-ary ข้อเสีย: การดำเนินการ await
และ yield
ถูก จำกัดขอบเขต ไว้ที่ ฟังก์ชันที่มีอยู่ ดังนั้นจึง ไม่สามารถจัดการได้โดยฟังก์ชันเอกนารี เพียงอย่างเดียว หากเราต้องการรวมมันเข้ากับนิพจน์ไปป์ await
และ yield
จะต้องได้รับการจัดการเป็น กรณีไวยากรณ์พิเศษ :
value |> await
การรอคอยสัญญาและvalue |> yield
ตอบแทนสำหรับค่าตัวสร้างผลผลิต ทั้ง แฮ็คไพพ์และไพพ์ F# กำหนด ภาษีไวยากรณ์ เล็กน้อยสำหรับนิพจน์ที่แตกต่างกันตามลำดับ:
แฮ็คไพพ์ จะเก็บภาษีเฉพาะ การเรียกใช้ฟังก์ชันเอกนารี เล็กน้อยเท่านั้น และ
ไปป์ F# เก็บภาษี นิพจน์ทั้งหมดเล็กน้อย ยกเว้น การเรียกใช้ฟังก์ชันเอกนารี
ในข้อเสนอ ทั้งสอง ไวยากรณ์ภาษีต่อนิพจน์ที่ต้องเสียภาษีมี ขนาดเล็ก ( ทั้ง (%)
และ x=>
มี เพียงสามอักขระเท่านั้น ) อย่างไรก็ตาม ภาษีจะถูก คูณ ด้วย ความแพร่หลาย ของนิพจน์ที่ต้องเสียภาษีตามลำดับ ดังนั้นจึงอาจสมเหตุสมผลที่จะเรียกเก็บภาษีสำหรับนิพจน์ใดก็ตามที่ ใช้ได้น้อยกว่า และ เพิ่มประสิทธิภาพ ให้กับนิพจน์ใดก็ตามที่ ใช้บ่อยกว่า
การเรียกใช้ฟังก์ชัน Unary โดยทั่วไปแล้ว จะพบได้น้อยกว่า นิพจน์ ทั้งหมด ยกเว้น ฟังก์ชัน Unary โดยเฉพาะอย่างยิ่งการเรียกใช้ เมธอด และการเรียกใช้ ฟังก์ชัน n-ary จะได้รับ ความนิยม เสมอ ในความถี่ทั่วไป การเรียกใช้ฟังก์ชันเอก นารี เท่ากับหรือเกินกว่าสองกรณีนี้ เพียงอย่างเดียว ไม่ต้องพูดถึงไวยากรณ์ที่แพร่หลายอื่นๆ เช่น ตัวอักษรอาร์เรย์ ตัวอักษรวัตถุ และ การดำเนินการทางคณิตศาสตร์ คำอธิบายนี้มีตัวอย่างในโลกแห่งความเป็นจริงหลายตัวอย่างเกี่ยวกับความแตกต่างในด้านความชุกนี้
นอกจากนี้ ไวยากรณ์ใหม่ที่นำเสนออื่นๆ อีกหลายรูปแบบ เช่น การเรียกส่วนขยาย , การแสดงออก และ บันทึก/ทูเพิลตัวอักษร ก็มีแนวโน้มที่จะ แพร่หลาย ใน อนาคต เช่นกัน ในทำนองเดียวกัน การดำเนินการ ทางคณิตศาสตร์ ก็จะกลายเป็น เรื่องธรรมดามากขึ้น หาก TC39 สร้างมาตรฐานให้กับ ตัวดำเนินการที่โอเวอร์โหลด การคลายการแสดงออกของไวยากรณ์ในอนาคตเหล่านี้จะทำให้แฮ็กไพพ์มีความคล่องมากกว่าเมื่อเปรียบเทียบกับไพพ์ F#
ไวยากรณ์ของแฮ็กไพพ์ในการเรียกใช้ฟังก์ชันเอกนารี (เช่น (%)
เพื่อเรียกใช้ฟังก์ชันเอกนารีของด้านขวามือ) ไม่ใช่กรณีพิเศษ มันเป็นเพียง การเขียนโค้ดธรรมดาอย่างชัดเจน ใน แบบที่ปกติเราจะทำ โดยไม่มีไพพ์
ในทางกลับกัน ไปป์ F# กำหนดให้ เราต้อง แยกแยะ ระหว่าง “โค้ดที่แปลงเป็นฟังก์ชันเอกนารี” กับ “นิพจน์อื่นๆ” และอย่าลืมเพิ่ม wrapper ฟังก์ชันลูกศรรอบๆ ตัวพิมพ์หลัง
ตัวอย่างเช่น สำหรับแฮ็กไปป์ value |> someFunction + 1
เป็น ไวยากรณ์ที่ไม่ถูกต้อง และจะ ล้มเหลวก่อนกำหนด ไม่จำเป็นต้องรับรู้ว่า someFunction + 1
จะไม่ประเมินเป็นฟังก์ชันเอก แต่สำหรับไปป์ F# value |> someFunction + 1
ยังคงเป็นไวยากรณ์ที่ถูกต้อง - มันจะ ล้มเหลว ตอน รันไทม์ ช้า เพราะ someFunction + 1
ไม่สามารถเรียกได้
กลุ่มแชมป์ไปป์ได้นำเสนอท่อ F# สำหรับ Stage 2 ถึง TC39 สองครั้ง ไม่ประสบความสำเร็จ ในการก้าวเข้าสู่ขั้นที่ 2 ทั้งสองครั้ง ไปป์ F# ทั้งสอง (และแอปพลิเคชันฟังก์ชันบางส่วน (PFA)) ประสบปัญหาการตอบโต้อย่างมากจากตัวแทน TC39 รายอื่นหลายรายเนื่องจากข้อกังวลต่างๆ สิ่งเหล่านี้รวมถึง:
await
การตีกลับนี้เกิดขึ้นจาก นอก กลุ่มแชมป์ไปป์ ดู HISTORY.md สำหรับข้อมูลเพิ่มเติม
เป็นความเชื่อของกลุ่มแชมป์ไปป์ที่ว่าผู้ดำเนินการไปป์ใด ๆ ย่อมดีกว่าไม่มีเลย เพื่อทำให้นิพจน์ที่ซ้อนกันแบบลึกเป็นเส้นตรงได้อย่างง่ายดายโดยไม่ต้องอาศัยตัวแปรที่มีชื่อ สมาชิกของกลุ่มแชมป์เปี้ยนหลายคนเชื่อว่าท่อแฮ็คดีกว่าท่อ F# เล็กน้อย และสมาชิกกลุ่มแชมป์เปี้ยนบางคนเชื่อว่าท่อ F# ดีกว่าท่อแฮ็กเล็กน้อย แต่ทุกคนในกลุ่มแชมป์เปี้ยนต่างเห็นพ้องต้องกันว่าท่อ F# ต้องเผชิญกับการต่อต้านมากเกินไปที่จะผ่าน TC39 ได้ในอนาคตอันใกล้
เพื่อเน้นย้ำว่า มีแนวโน้มว่าความพยายามที่จะเปลี่ยนจากท่อ Hack กลับไปเป็นท่อ F# จะส่งผลให้ TC39 ไม่เห็นด้วยกับไปป์ใดๆ เลย ไวยากรณ์ของ PFA กำลังเผชิญกับการต่อสู้ที่ยากลำบากใน TC39 ในทำนองเดียวกัน (ดู HISTORY.md) สมาชิกหลายคนของกลุ่มแชมป์เปี้ยนไปป์คิดว่านี่เป็นโชคร้าย และพวกเขายินดีที่จะต่อสู้อีกครั้ง ในภายหลัง สำหรับการแยกมิกซ์ F#-pipe และไวยากรณ์ PFA แต่มีตัวแทนจำนวนไม่น้อย (รวมถึงผู้ติดตั้งโปรแกรมเบราว์เซอร์) ที่อยู่นอก Pipe Champion Group ซึ่งโดยทั่วไปไม่สนับสนุนการเขียนโปรแกรมโดยปริยาย (และไวยากรณ์ PFA) โดยไม่คำนึงถึงไปป์แฮ็ก
(มีข้อกำหนดฉบับร่างอย่างเป็นทางการ)
การอ้างอิงหัวข้อ %
เป็น ตัวดำเนินการที่เป็นค่าว่าง โดยจะทำหน้าที่เป็นตัวแทนสำหรับ ค่าหัวข้อ และมี การกำหนดขอบเขตทางคำศัพท์ และ ไม่เปลี่ยนรูป
%
ไม่ใช่ตัวเลือกสุดท้าย ( โทเค็น ที่แม่นยำสำหรับการอ้างอิงหัวข้อ ไม่ใช่ที่สิ้นสุด %
อาจเป็น ^
หรือโทเค็นอื่น ๆ แทน เราวางแผนที่จะ เปลี่ยน โทเค็นจริงที่จะใช้ก่อนที่จะก้าวไปสู่ขั้นที่ 3 อย่างไรก็ตาม %
ดูเหมือนจะมีปัญหาทางวากยสัมพันธ์น้อยที่สุด และ นอกจากนี้ยังมีลักษณะคล้ายกับตัวยึดตำแหน่งของ สตริงรูปแบบ printf และ ตัวอักษรฟังก์ชัน #(%)
ของ Clojure
ตัวดำเนินการไปป์ |>
เป็น ตัวดำเนินการมัด ที่สร้าง นิพจน์ไปป์ (หรือที่เรียกว่า ไปป์ไลน์ ) โดยจะประเมินด้านซ้าย ( ส่วนหัวไปป์ หรือ ไปป์อินพุต ) ผูก ค่าผลลัพธ์ ( ค่าหัวข้อ ) เข้ากับ การอ้างอิงหัวข้อ อย่างไม่เปลี่ยนรูป จากนั้นประเมินทางด้านขวามือ ( ตัวไปป์ ) ด้วยการเชื่อมโยงนั้น ค่าผลลัพธ์ของด้านขวามือจะกลายเป็นค่าสุดท้ายของนิพจน์ไปป์ทั้งหมด ( เอาต์พุตไปป์ )
ลำดับความสำคัญของตัวดำเนินการไปป์จะ เหมือนกับ :
=>
;=
, +=
ฯลฯ ;yield
และ yield *
; มัน เข้มงวด กว่าตัวดำเนินการลูกน้ำ ,
มัน หลวม กว่าตัวดำเนินการ อื่นทั้งหมด
ตัวอย่างเช่น v => v |> % == null |> foo(%, 0)
จะจัดกลุ่มเป็น v => (v |> (% == null) |> foo(%, 0))
,
ซึ่งจะเทียบเท่ากับ v => foo(v == null, 0)
ตัวไปป์ ต้อง ใช้ค่าหัวข้อ อย่างน้อยหนึ่งครั้ง ตัวอย่างเช่น value |> foo + 1
เป็น ไวยากรณ์ที่ไม่ถูกต้อง เนื่องจากเนื้อหาของค่าไม่มีการอ้างอิงหัวข้อ การออกแบบนี้เป็นเพราะ การละเว้น หัวข้อที่อ้างอิงจากเนื้อหาของไปป์นิพจน์เกือบจะเป็นข้อผิดพลาดของโปรแกรมเมอร์ โดยไม่ ได้ตั้งใจ
ในทำนองเดียวกัน การอ้างอิงหัวข้อ จะต้อง มีอยู่ในตัวไปป์ การใช้การอ้างอิงหัวข้อภายนอกตัวไปป์ก็ถือเป็น ไวยากรณ์ที่ไม่ถูกต้อง เช่นกัน
เพื่อป้องกันการจัดกลุ่มที่สับสน ไวยากรณ์ ไม่ถูกต้อง ที่จะใช้ตัวดำเนินการ อื่น ที่มี ความสำคัญใกล้เคียงกัน (เช่น ลูกศร =>
ตัวดำเนินการแบบมีเงื่อนไขแบบไตรภาค ?
:
ตัวดำเนินการที่ได้รับมอบหมาย และตัวดำเนินการ yield
) เป็น ส่วนหัวหรือเนื้อหาไปป์ เมื่อใช้ |>
กับโอเปอเรเตอร์เหล่านี้ เราต้องใช้ วงเล็บ เพื่อระบุอย่างชัดเจนว่าการจัดกลุ่มใดถูกต้อง ตัวอย่างเช่น a |> b ? % : c |> %.d
เป็นไวยากรณ์ที่ไม่ถูกต้อง ควรแก้ไขเป็น a |> (b ? % : c) |> %.d
หรือ a |> (b ? % : c |> %.d)
สุดท้ายนี้ การผูกหัวข้อ ภายในโค้ดที่คอมไพล์แบบไดนามิก (เช่น ด้วย eval
หรือ new Function
) ไม่สามารถ นำมาใช้ ภายนอก โค้ดนั้นได้ ตัวอย่างเช่น v |> eval('% + 1')
จะส่งข้อผิดพลาดทางไวยากรณ์เมื่อมีการประเมินนิพจน์ eval
ที่รันไทม์
ไม่มีกฎพิเศษอื่นๆ
ผลลัพธ์ตามธรรมชาติของกฎเหล่านี้ก็คือ หากเราจำเป็นต้องแทรก ผลข้างเคียง ที่อยู่ตรงกลางของห่วงโซ่ของนิพจน์ไปป์ โดยไม่ต้องแก้ไขข้อมูลที่ส่งผ่าน เราสามารถใช้ นิพจน์ลูกน้ำ เช่น ด้วย value |> (sideEffect(), %)
. ตามปกติ นิพจน์ลูกน้ำจะประเมินทางด้านขวามือ %
โดยพื้นฐานแล้วจะส่งผ่านค่าหัวข้อโดยไม่ต้องแก้ไข สิ่งนี้มีประโยชน์อย่างยิ่งสำหรับการแก้ไขข้อบกพร่องอย่างรวดเร็ว: value |> (console.log(%), %)
การเปลี่ยนแปลงเพียงอย่างเดียวกับตัวอย่างดั้งเดิมคือการแยกส่วนและการลบความคิดเห็น
จาก jquery/build/tasks/sourceMap.js:
// Status quo
var minLoc = Object . keys ( grunt . config ( "uglify.all.files" ) ) [ 0 ] ;
// With pipes
var minLoc = grunt . config ( 'uglify.all.files' ) | > Object . keys ( % ) [ 0 ] ;
จาก node/deps/npm/lib/unpublish.js:
// Status quo
const json = await npmFetch . json ( npa ( pkgs [ 0 ] ) . escapedName , opts ) ;
// With pipes
const json = pkgs [ 0 ] | > npa ( % ) . escapedName | > await npmFetch . json ( % , opts ) ;
จากขีดล่าง js:
// Status quo
return filter ( obj , negate ( cb ( predicate ) ) , context ) ;
// With pipes
return cb ( predicate ) | > _ . negate ( % ) | > _ . filter ( obj , % , context ) ;
จาก ramda.js