Toán Tử Ống (|>
) cho JavaScript
Tại sao cần toán tử ống
Trong cuộc khảo sát State of JS 2020, câu trả lời phổ biến thứ tư cho “What do you feel is currently missing from JavaScript?” là một toán tử ống. Tại sao lại như vậy?
Khi chúng ta thực hiện các hoạt động liên tiếp (ví dụ, các cuộc gọi hàm) trên một giá trị trong JavaScript, hiện tại có hai phong cách cơ bản:
- truyền giá trị như một đối số cho hoạt động ( lồng nhau các hoạt động nếu có nhiều hoạt động),
- hoặc gọi hàm như một phương thức trên giá trị ( chuỗi thêm các cuộc gọi phương thức nếu có nhiều phương thức).
Tức là, three(two(one(value)))
so với value.one().two().three()
. Tuy nhiên, những phong cách này khác nhau rất nhiều về khả năng đọc, mạch lạc và khả năng áp dụng.
Lồng sâu khó đọc
Phong cách đầu tiên, lồng nhau, nói chung có thể áp dụng cho bất kỳ chuỗi hoạt động nào: cuộc gọi hàm, phép tính, biểu thức mảng/đối tượng, await
và yield
, vv.
Tuy nhiên, lồng nhau khó đọc khi trở nên sâu: dòng thực thi di chuyển từ phải qua trái, thay vì đọc từ trái qua phải như trong mã lệnh bình thường. Nếu có nhiều đối số ở một số cấp độ, việc đọc ngay cả bị nhảy múa : mắt chúng ta phải nhảy sang trái để tìm tên hàm, sau đó chúng phải nhảy sang phải để tìm các đối số bổ sung. Hơn nữa, việc chỉnh sửa mã sau đó có thể gây rối: chúng ta phải tìm vị trí đúng để chèn đối số mới trong nhiều dấu ngoặc lồng nhau.
Ví dụ thực tế
Xem xét real-world code from React này.
console.log(
chalk.dim(
`$ ${Object.keys(envars)
.map(envar =>
`${envar}=${envars[envar]}`)
.join(' ')
}`,
'node',
args.join(' ')));
Mã lệnh thực tế này được tạo thành từ biểu thức lồng sâu. Để đọc luồng dữ liệu của nó, mắt người cần phải:
Tìm dữ liệu ban đầu (biểu thức nội bộ nhất, envars
).
Và sau đó quét ngược và tới lùi lặp đi lặp lại từ bên trong ra ngoài cho mỗi quá trình chuyển đổi dữ liệu, mỗi thứ là một toán tử tiếp đầu ngược dễ bị bỏ sót bên trái hoặc toán tử hậu tố bên phải:
. Object.keys()
(bên trái),
. .map()
(bên phải),
. .join()
(bên phải),
. Một chuỗi mẫu (cả hai bên),
. chalk.dim()
(bên trái), sau đó
. console.log()
(bên trái).
Do việc lồng sâu nhiều biểu thức (một số trong số đó sử dụng toán tử tiếp đầu ngược, một số trong số đó sử dụng toán tử hậu tố, và một số trong số đó sử dụng toán tử định vị), chúng ta phải kiểm tra cả hai bên trái và phải để tìm đầu của mỗi biểu thức.
Chaining phương thức bị hạn chế
Phong cách thứ hai, chuỗi phương thức, chỉ có thể sử dụng nếu giá trị có các hàm được chỉ định là phương thức cho lớp của nó. Điều này hạn chế khả năng áp dụng của nó. Nhưng khi nó áp dụng, nhờ cấu trúc hậu tố của nó, nó thường có thể sử dụng và dễ đọc và viết hơn. Luồng thực thi mã chạy từ trái qua phải. Các biểu thức lồng sâu được tháo rời. Tất cả các đối số cho một cuộc gọi hàm được nhóm với tên của hàm. Và việc chỉnh sửa mã sau này để chèn hoặc xóa thêm cuộc gọi phương thức là đơn giản, vì chúng ta chỉ cần đặt con trỏ của mình ở một vị trí, sau đó bắt đầu gõ hoặc xóa một dãy ký tự liền kề.
Thực tế, những lợi ích của việc chuỗi phương thức vô cùng hấp dẫn, đến mức một số thư viện phổ biến biến dạng cấu trúc mã của họ một cách đặc biệt để cho phép nhiều chuỗi phương thức hơn. Ví dụ tiêu biểu nhất là jQuery, mà vẫn là thư viện JS phổ biến nhất trên thế giới. Thiết kế cốt lõi của jQuery là một đối tượng “über” duy nhất với hàng chục phương thức, tất cả các phương thức đều trả về cùng loại đối tượng để chúng ta có thể tiếp tục chuỗi phương thức. Thậm chí có một tên cho phong cách lập trình này: fluent interfaces.
Rất tiếc, mặc dù có tính mạch lạc, chuỗi phương thức một mình không thể chứa được cú pháp khác của JavaScript: cuộc gọi hàm, phép tính, biểu thức mảng/đối tượng, await
và yield
, vv. Bằng cách này, chuỗi phương thức vẫn hạn chế trong khả năng áp dụng.
Toán tử ống kết hợp cả hai thế giới
Toán tử ống cố gắng kết hợp sự thuận tiện và dễ dàng của chuỗi phương thức với khả năng áp dụng rộng rãi của lồng biểu thức.
Cấu trúc chung của tất cả các toán tử ống là value |>
e1 |>
e2 |>
e3, trong đó e1, e2, e3 đều là các biểu thức nhận các giá trị liên tiếp làm tham số của chúng. Toán tử |>
sau đó thực hiện một mức độ ma thuật để “ống” giá trị từ phía bên trái vào phía bên phải.
Ví dụ thực tế , tiếp tục
Tiếp tục với real-world code from React lồng sâu này:
console.log(
chalk.dim(
`$ ${Object.keys(envars)
.map(envar =>
`${envar}=${envars[envar]}`)
.join(' ')
}`,
'node',
args.join(' ')));
…chúng ta có thể tháo rời nó như vậy bằng cách sử dụng một toán tử ống và một ký tự trình giữ chỗ (%
) đại diện cho giá trị của hoạt động trước đó:
Object.keys(envars)
.map(envar => `${envar}=${envars[envar]}`)
.join(' ')
|> `$ ${%}`
|> chalk.dim(%, 'node', args.join(' '))
|> console.log(%);
Bây giờ, người đọc có thể nhanh chóng tìm thấy dữ liệu ban đầu (đã là biểu thức nội bộ nhất, envars
), sau đó đọc từ trái qua phải , mỗi quá trình biến đổi dữ liệu.
Biến tạm thời thường khá phiền toái
Một lập luận có thể rằng sử dụng biến tạm thời nên là cách duy nhất để giải quyết mã lồng sâu. Việc đặt tên rõ ràng cho biến của từng bước gây ra một điều tương tự như chuỗi phương thức, với những lợi ích tương tự về đọc và viết mã.
Ví dụ thực tế , tiếp tục
Ví dụ, sử dụng real-world example from React đã được chỉnh sửa trước đó của chúng ta:
Object.keys(envars)
.map(envar => `${envar}=${envars[envar]}`)
.join(' ')
|> `$ ${%}`
|> chalk.dim(%, 'node', args.join(' '))
|> console.log(%);
…phiên bản sử dụng biến tạm thời sẽ trông như sau:
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);
Tuy nhiên, có những lý do tại sao chúng ta thường xuyên gặp các biểu thức lồng sâu trong mã của nhau thường xuyên trong thế giới thực , thay vì sử dụng dãy biến tạm thời. Và có lý do tại sao cách tiếp đầu ngược dựa trên chuỗi phương thức fluent interfaces của jQuery, Mocha, và những thư viện khác vẫn phổ biến.
Đôi khi chỉ đơn giản là quá phiền phức và rườm rà khi viết mã với một dãy biến tạm thời một lần. Có thể sẽ phiền phức và gây nhiễu loạn cho con người khi đọc mã, nó cũng không khác.
Nếu naming is one of the most difficult tasks in programming, thì các lập trình viên sẽ tự động tránh đặt tên biến khi họ cảm nhận lợi ích của việc đặt tên tương đối nhỏ.
Sử dụng lại biến tạm thời dễ bị đột biến không mong đợi
Một lập luận có thể là việc sử dụng một biến có thể thay đổi duy nhất với tên ngắn sẽ giảm bớt sự rườm rà của biến tạm thời, đạt được kết quả tương tự như với toán tử ống.
Ví dụ thực tế , tiếp tục
Ví dụ, real-world example from React đã được chỉnh sửa trước đó của chúng ta có thể được viết lại như sau:
let _;
_ = Object.keys(envars)
.map(envar => `${envar}=${envars[envar]}`)
.join(' ');
_ = `$ ${_}`;
_ = chalk.dim(_, 'node', args.join(' '));
_ = console.log(_);
Nhưng mã như thế này không phổ biến trong mã thực tế. Một nguyên nhân là biến có thể thay đổi một cách không mong đợi , gây ra lỗi im lặng khó tìm thấy. Ví dụ, biến có thể bị tham chiếu tình cờ trong một closure. Hoặc nó có thể bị gán lại một cách nhầm lẫn trong một biểu thức.
Mã ví dụ
// 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(_);
Vấn đề này sẽ không xảy ra với toán tử ống. Ký hiệu chủ đề không thể được gán lại và mã bên ngoài từng bước không thể thay đổi khả năng ràng buộc của nó.
let _;
_ = one()
|> double(%)
|> Promise.resolve().then(() =>
// This prints 2, as intended.
console.log(%));
_ = one();
Vì lý do này, mã với biến có thể thay đổi cũng khó đọc hơn. Để xác định biến đại diện cho cái gì tại bất kỳ điểm nào, bạn phải tìm trong phạm vi phía trước hoàn toàn các vị trí mà nó được gán lại.
Ngược lại, tham chiếu chủ đề của một đường ống có phạm vi từ vựng hạn chế và ràng buộc của nó là bất biến trong phạm vi của nó. Nó không thể được gán lại tình cờ và có thể được an toàn sử dụng trong các closure.
Mặc dù giá trị chủ đề cũng thay đổi với mỗi bước của đường ống, chúng ta chỉ quét bước trước đó của đường ống để hiểu nó, dẫn đến mã dễ đọc hơn.
Biến tạm thời phải được khai báo trong các câu lệnh
Lợi ích khác của toán tử ống so với các chuỗi câu lệnh gán (dù có biến tạm thời có thể thay đổi hoặc không thể thay đổi) là chúng là biểu thức.
Biểu thức ống là biểu thức có thể được trả về trực tiếp, gán cho một biến hoặc được sử dụng trong ngữ cảnh như biểu thức JSX.
Ngược lại, việc sử dụng biến tạm thời yêu cầu các chuỗi câu lệnh.
Ví dụ
Đường ống | Biến tạm thời |
---|---|
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 => (
}>{envar}
));
return (
- {_}
);
Tại sao cần toán tử ống Hack
Có hai đề xuất cạnh tranh cho toán tử ống: Hack pipes và F# pipes. (Trước đó, có một third proposal for a “smart mix” of the first two proposals, nhưng đã bị rút lại, vì cú pháp của nó là một phần trên một trong những đề xuất.)
Hai đề xuất về toán tử ống chỉ khác nhau một chút về “ma thuật” là gì khi chúng ta viết mã khi sử dụng |>
.
Cả hai đề xuất sử dụng lại các khái niệm ngôn ngữ hiện có: Toán tử ống Hack dựa trên khái niệm của biểu thức , trong khi toán tử ống F# dựa trên khái niệm của hàm đơn ngữ.
Toán tử ống biểu thức và toán tử ống hàm đơn ngữ tương ứng có lợi ích nhỏ và gần như đối xứng.
Đề xuất này: Toán tử ống Hack
Trong cú pháp toán tử ống của ngôn ngữ Hack , phần bên phải của toán tử ống là một biểu thức chứa một chỗ giữ chỗ đặc biệt , được đánh giá với chỗ giữ chỗ gắn với kết quả của việc đánh giá biểu thức phía bên trái. Tức là, chúng ta viết value |> one(%) |> two(%) |> three(%)
để dùng toán tử ống value
qua ba hàm.
Thuận: Phần bên phải có thể là bất kỳ biểu thức nào , và chỗ giữ chỗ có thể được đặt bất kỳ nơi nào mà bất kỳ tên biến thông thường nào cũng có thể được đặt, vì vậy chúng ta có thể dùng toán tử ống cho bất kỳ mã chúng ta muốn mà không có bất kỳ quy tắc đặc biệt nào :
value |> foo(%)
cho cuộc gọi hàm đơn ngữ,value |> foo(1, %)
cho cuộc gọi hàm n-ary,value |> %.foo()
cho cuộc gọi phương thức,value |> % + 1
cho phép tính,value |> [%, 0]
cho mảng chữ số,value |> {foo: %}
cho đối tượng chữ số,value |>
${%}“ cho chuỗi mẫu,value |> new Foo(%)
cho tạo đối tượng,value |> await %
cho đợi promises,value |> (yield %)
cho phát sinh giá trị từ bộ sinh,value |> import(%)
cho cuộc gọi từ khóa giống hàm,- vv.
Ngược lại: Sử dụng toán tử ống qua hàm đơn ngữ nguyên thủy hơi dài hơn với toán tử ống Hack so với toán tử ống F#. Điều này bao gồm cả các hàm đơn ngữ được tạo ra bởi các thư viện như Ramda, cũng như unary arrow functions that perform complex destructuring on their arguments: Toán tử ống Hack sẽ hơi dài hơn một chút với hậu tố cuộc gọi hàm rõ ràng (%)
.
(Việc phân rã phức tạp của giá trị chủ đề sẽ dễ dàng hơn khi do expressions tiến triển, vì bạn sau đó sẽ có thể thực hiện gán/destructuring biến bên trong cơ thể ống.)
Đề xuất thay thế: Toán tử ống F
Trong F# language’s pipe syntax, phần bên phải của toán tử ống là một biểu thức phải được đánh giá thành một hàm đơn ngữ , sau đó sẽ được gọi ngầm với giá trị phía bên trái là đối số duy nhất của nó. Tức là, chúng ta viết value |> one |> two |> three
để dùng toán tử ống value
qua ba hàm. left |> right
trở thành right(left)
. Điều này được gọi là tacit programming or point-free style.
Ví dụ thực tế , tiếp tục
Ví dụ, sử dụng real-world example from React đã được chỉnh sửa trước đó của chúng ta:
Object.keys(envars)
.map(envar => `${envar}=${envars[envar]}`)
.join(' ')
|> `$ ${%}`
|> chalk.dim(%, 'node', args.join(' '))
|> console.log(%);
…phiên bản sử dụng toán tử ống F# thay vì toán tử ống Hack sẽ trông như sau:
Object.keys(envars)
.map(envar => `${envar}=${envars[envar]}`)
.join(' ')
|> x=> `$ ${x}`
|> x=> chalk.dim(x, 'node', args.join(' '))
|> console.log;
Thuận: Ràng buộc rằng phần bên phải phải giải quyết thành một hàm đơn ngữ cho phép chúng ta viết những ống ngắn gọn khi phép thao tác mà chúng ta muốn thực hiện là một cuộc gọi hàm đơn ngữ :
value |> foo
cho cuộc gọi hàm đơn ngữ.
Điều này bao gồm cả các hàm đơn ngữ được tạo ra bởi các thư viện như Ramda, cũng như unary arrow functions that perform complex destructuring on their arguments: Toán tử ống F# sẽ ít dài hơn một chút với cuộc gọi hàm ngầm định (không có (%)
).
Ngược lại: Ràng buộc này có nghĩa là bất kỳ phép thao tác nào được thực hiện bằng cú pháp khác phải trở nên dài hơn một chút bằng cách bao gói phép thao tác trong một hàm mũi tên đơn ngữ :
value |> x=> x.foo()
cho cuộc gọi phương thức,value |> x=> x + 1
cho phép tính,value |> x=> [x, 0]
cho mảng chữ số,value |> x=> ({foo: x})
cho đối tượng chữ số,value |> x=>
${x}“ cho chuỗi mẫu,value |> x=> new Foo(x)
để tạo đối tượng,value |> x=> import(x)
để gọi các từ khóa giống hàm,- vv.
Thậm chí cuộc gọi hàm có tên cũng đòi hỏi bao gói khi chúng ta cần truyền hơn một đối số :
value |> x=> foo(1, x)
cho cuộc gọi hàm n-ary.
Ngược lại: Các hoạt động **await**
và **yield**
được phạm vi hóa cho hàm chứa của chúng, và do đó không thể được xử lý bởi hàm đơn ngữ một mình. Nếu chúng ta muốn tích hợp chúng vào một biểu thức ống, await and yield must be handled as special syntax cases:
value |> await
để đợi promises, vàvalue |> yield
để phát sinh giá trị từ bộ sinh.
Toán tử ống Hack ưu tiên những biểu thức phổ biến hơn
Cả hai Toán tử ống Hack và Toán tử ống F# đều áp đặt một thuế cú pháp nhỏ trên các biểu thức khác nhau:
Toán tử ống Hack nhẹ nhàng thuế chỉ cuộc gọi hàm đơn ngữ , và
Toán tử ống F# nhẹ nhàng thuế tất cả các biểu thức ngoại trừ cuộc gọi hàm đơn ngữ.
Trong cả hai đề xuất, thuế cú pháp cho mỗi biểu thức bị thuế là nhỏ ( cả hai (%)
và x=>
chỉ là ba ký tự ). Tuy nhiên, thuế được nhân lên bởi sự phổ biến của các biểu thức bị thuế tương ứng. Vì vậy có thể có ý nghĩa để áp đặt thuế cho bất kỳ biểu thức nào ít phổ biến hơn và tối ưu hóa cho bất kỳ biểu thức nào phổ biến hơn.
Cuộc gọi hàm đơn ngữ nói chung ít phổ biến hơn tất cả các biểu thức ngoại trừ cuộc gọi hàm đơn ngữ. Cụ thể, gọi phương thức và gọi hàm n-ary sẽ luôn luôn là phổ biến ; về tần suất chung, cuộc gọi hàm đơn ngữ bằng hoặc vượt quá hai trường hợp này một mình – chưa kể đến các cú pháp phổ biến khác như mảng chữ số , đối tượng chữ số , và phép tính. Hướng dẫn này chứa một số ví dụ thực tế về sự khác biệt này trong tần suất xuất hiện.
Hơn nữa, một số cú pháp mới được đề xuất khác, chẳng hạn như extension calling, do expressions, và record/tuple literals, cũng có khả năng trở thành phổ biến trong tương lai. Tương tự, phép tính cũng sẽ trở thành phổ biến hơn nữa nếu TC39 tiêu chuẩn hóa operator overloading. Giải quyết các biểu thức của các cú pháp tương lai này sẽ trở nên dễ dàng hơn với toán tử ống Hack so với toán tử ống F#.
Toán tử ống Hack có thể đơn giản hơn khi sử dụng
Thuế cú pháp của toán tử ống Hack cho cuộc gọi hàm đơn ngữ (tức là (%)
để gọi hàm đơn ngữ bên phải) không phải là một trường hợp đặc biệt : nó đơn giản là việc viết mã thông thường một cách rõ ràng , theo cách chúng ta thường làm mà không có dấu ống.
Trong khi đó, toán tử ống F# yêu cầu chúng ta phân biệt giữa “mã mà trả về một hàm đơn ngữ” so với “bất kỳ biểu thức nào khác” – và nhớ thêm bao bọc hàm mũi tên xung quanh trường hợp sau.
Ví dụ, với toán tử ống Hack, value |> someFunction + 1
là cú pháp không hợp lệ và sẽ thất bại sớm. Không cần phải nhận biết rằng someFunction + 1
sẽ không trả về một hàm đơn ngữ. Nhưng với toán tử ống F#, value |> someFunction + 1
vẫn là cú pháp hợp lệ – nó chỉ sẽ thất bại muộn trong thời gian chạy, vì someFunction + 1
không thể gọi được.
TC39 đã từ chối toán tử ống F# nhiều lần
Nhóm nhân tố toán tử ống đã trình bày toán tử ống F# cho Stage 2 của TC39 hai lần. Cả hai lần đều không thành công trong việc tiến đến Stage 2. Cả toán tử ống F# (và partial function application (PFA)) đã gặp phải sự phản đối mạnh mẽ từ nhiều đại diện TC39 khác do những quan ngại khác nhau. Các quan ngại này bao gồm:
- Quan ngại về hiệu suất bộ nhớ (ví dụ, especially from browser-engine implementors),
- Quan ngại về cú pháp của
await
. - Quan ngại về khuyến khích phân nhánh/phân nhánh hệ sinh thái, v.v.
Sự phản đối này đã xuất phát từ bên ngoài nhóm nhân tố toán tử ống. Xem thêm HISTORY.md để biết thêm thông tin chi tiết.
Nhóm nhân tố toán tử ống tin rằng bất kỳ toán tử ống nào cũng tốt hơn không có, để dễ dàng tuyến tính hóa các biểu thức sâu lồng mà không cần phải sử dụng biến có tên. Nhiều thành viên trong nhóm nhân tố toán tử ống tin rằng toán tử ống Hack đôi chút tốt hơn toán tử ống F#, và một số thành viên trong nhóm nhân tố toán tử ống tin rằng toán tử ống F# đôi chút tốt hơn toán tử ống Hack. Nhưng tất cả thành viên trong nhóm nhân tố toán tử ống đều đồng tình rằng toán tử ống F# đã gặp quá nhiều sự phản đối để có thể thông qua TC39 trong tương lai có thể thấy được.
Để nhấn mạnh, rất có khả năng rằng việc cố gắng chuyển từ toán tử ống Hack sang toán tử ống F# sẽ khiến TC39 không bao giờ đồng ý với bất kỳ toán tử ống nào cả. PFA syntax cũng đang gặp khó khăn trong TC39 (xem HISTORY.md). Nhiều thành viên trong nhóm nhân tố toán tử ống nghĩ rằng điều này là không may, và họ sẵn sàng chiến đấu sau này cho sự kết hợp chia tách toán tử ống F# và PFA syntax. Nhưng có khá nhiều đại diện (bao gồm browser-engine implementers) ngoài nhóm nhân tố toán tử ống luôn phản đối việc khuyến khích tacit programming (và PFA syntax), bất kể về toán tử ống Hack.
Mô tả
(Một formal draft specification có sẵn.)
Tham chiếu chủ đề %
là một toán tử không tham số. Nó hoạt động như một nơi đặt chỗ cho một giá trị chủ đề , và nó có phạm vi từ vựng và bất biến.
%
không phải là lựa chọn cuối cùng
(Thông tin chính xác về token for the topic reference is not final. %
có thể được thay thế bằng ^
, hoặc nhiều biểu tượng khác. Chúng tôi dự định thực hiện bikeshed what actual token to use trước khi tiến vào Stage 3. Tuy nhiên, %
có vẻ là least syntactically problematic, và cũng tương tự các nơi đặt chỗ của printf format strings và Clojure’s #(%) function literals.)
Toán tử ống |>
là một toán tử trung tố tạo thành một biểu thức ống (còn gọi là dãy ống ). Nó đánh giá phần bên trái của nó (đầu ống hoặc đầu vào ống), bất biến ràng buộc giá trị kết quả (giá trị chủ đề) vào tham chiếu chủ đề , sau đó đánh giá phần bên phải của nó (thân ống) với việc ràng buộc đó. Giá trị kết quả của phần bên phải trở thành giá trị cuối cùng của biểu thức ống (đầu ra ống).
Sự ưu tiên của toán tử ống tương tự như:
- mũi tên hàm
=>
; - các toán tử gán
=
,+=
, vv; - các toán tử tạo ra giá trị cho generator
yield
vàyield *
;
Nó có độ ưu tiên cao hơn chỉ so với toán tử phẩy ,
.
Nó có độ ưu tiên thấp hơn so với tất cả các toán tử khác.
Ví dụ, v => v |> % == null |> foo(%, 0)
sẽ nhóm thành v => (v |> (% == null) |> foo(%, 0))
,
tức là tương đương với v => foo(v == null, 0)
.
Một thân ống phải sử dụng giá trị chủ đề ít nhất một lần. Ví dụ, value |> foo + 1
là cú pháp không hợp lệ , vì thân ống của nó không chứa tham chiếu chủ đề. Thiết kế này là vì bỏ sót tham chiếu chủ đề khỏi thân biểu thức ống hầu như chắc chắn là một lỗi ngẫu nhiên của người lập trình.
Tương tự, tham chiếu chủ đề phải nằm trong thân biểu thức ống. Việc sử dụng tham chiếu chủ đề bên ngoài thân biểu thức ống cũng là cú pháp không hợp lệ.
Để tránh nhầm lẫn trong việc nhóm, cú pháp không hợp lệ khi sử dụng các toán tử khác có độ ưu tiên tương tự (ví dụ, mũi tên =>
, toán tử điều kiện ba ngôi ?
:
, các toán tử gán và toán tử yield
) như là đầu hoặc thân của dấu chìa. Khi sử dụng |>
với các toán tử này, chúng ta phải sử dụng dấu ngoặc đơn để chỉ rõ nhóm hợp lệ. Ví dụ, a |> b ? % : c |> %.d
là cú pháp không hợp lệ; nó nên được sửa thành a |> (b ? % : c) |> %.d
hoặc a |> (b ? % : c |> %.d)
.
Cuối cùng, việc gắn kết chủ đề bên trong mã được biên dịch động (ví dụ, bằng cách sử dụng eval
hoặc new Function
) không thể được sử dụng bên ngoài của mã đó. Ví dụ, v |> eval('% + 1')
sẽ gây ra lỗi cú pháp khi biểu thức eval
được đánh giá tại thời gian chạy.
Không có quy tắc đặc biệt nào khác.
Một kết quả tự nhiên của những quy tắc này là, nếu chúng ta cần can thiệp một hiệu ứng phụ vào giữa chuỗi các biểu thức dấu chìa, mà không làm thay đổi dữ liệu đang được đưa qua, chúng ta có thể sử dụng một biểu thức phẩy , như trong trường hợp value |> (sideEffect(), %)
. Như thường lệ, biểu thức phẩy sẽ được đánh giá thành phần bên phải %
, tương đương việc truyền giá trị chủ đề mà không làm thay đổi nó. Điều này đặc biệt hữu ích cho việc gỡ lỗi nhanh: value |> (console.log(%), %)
.
Ví dụ trong thực tế
Chỉ có sự thay đổi về việc loại bỏ thụt đầu dòng và xóa bỏ bình luận trong các ví dụ gốc.
Từ 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];
Từ 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);
Từ underscore.js:
// Status quo
return filter(obj, negate(cb(predicate)), context);
// With pipes
return cb(predicate) |> _.negate(%) |> _.filter(obj, %, context);
Từ ramda.js.
// Status quo
return xf['@@transducer/result'](obj[methodName](bind(xf['@@transducer/step'], xf), acc));
// With pipes
return xf
|> bind(%['@@transducer/step'], %)
|> obj[methodName](%, acc)
|> xf['@@transducer/result'](%);
Từ ramda.js.
// Status quo
try {
return tryer.apply(this, arguments);
} catch (e) {
return catcher.apply(this, _concat([e], arguments));
}
// With pipes: Note the visual parallelism between the two clauses.
try {
return arguments
|> tryer.apply(this, %);
} catch (e) {
return arguments
|> _concat([e], %)
|> catcher.apply(this, %);
}
// Status quo
return this.set('Link', link + Object.keys(links).map(function(rel){
return '<' + links[rel] + '>; rel="' + rel + '"';
}).join(', '));
// With pipes
return links
|> Object.keys(%).map(function (rel) {
return '<' + links[rel] + '>; rel="' + rel + '"';
})
|> link + %.join(', ')
|> this.set('Link', %);
Từ react/scripts/jest/jest-cli.js.
// Status quo
console.log(
chalk.dim(
`$ ${Object.keys(envars)
.map(envar => `${envar}=${envars[envar]}`)
.join(' ')}`,
'node',
args.join(' ')
)
);
// With pipes
Object.keys(envars)
.map(envar => `${envar}=${envars[envar]}`)
.join(' ')
|> `$ ${%}`
|> chalk.dim(%, 'node', args.join(' '))
|> console.log(%);
Từ ramda.js.
// Status quo
return _reduce(xf(typeof fn === 'function' ? _xwrap(fn) : fn), acc, list);
// With pipes
return fn
|> (typeof % === 'function' ? _xwrap(%) : %)
|> xf(%)
|> _reduce(%, acc, list);
// Status quo
jQuery.merge( this, jQuery.parseHTML(
match[ 1 ],
context && context.nodeType ? context.ownerDocument || context : document,
true
) );
// With pipes
context
|> (% && %.nodeType ? %.ownerDocument || % : document)
|> jQuery.parseHTML(match[1], %, true)
|> jQuery.merge(%);
Mối quan hệ với các đề xuất khác
Các trợ giúp Function
Các ống Hack có thể và sẽ tồn tại cùng với Function helpers proposal, bao gồm các hàm pipe
và flow
. Những hàm tiện lợi đơn giản (và thường được tải xuống) này thao tác với các hàm đơn nguyên mà không cần cú pháp phụ trợ.
TC39 đã từ chối toán tử ống F# hai lần. Với thực tế này, TC39 có khả năng thông qua các hàm trợ giúp pipe
và flow
hơn là một toán tử cú pháp tương tự.
Các hàm tiện lợi chuẩn hóa pipe
và flow
cũng có thể làm giảm một phần nhu cầu về toán tử ống F#. (Chúng sẽ không ngăn cản việc chuẩn hóa một toán tử tương đương sau này. Ví dụ, TC39 đã chuẩn hóa toán tử nhị phân **
ngay cả khi Math.pow
đã tồn tại.)
Cú pháp áp dụng hàm một phần
Các ống Hack có thể tồn tại cùng với cú pháp cho áp dụng hàm một phần (PFA). Có hai cách tiếp cận mà chúng có thể tồn tại cùng nhau.
Cách tiếp cận đầu tiên là với một cú pháp PFA được đánh giá một cách sẵn sàng, có already been proposed in proposal-partial-application. Cú pháp PFA sẵn sàng này sẽ thêm một toán tử …~(…)
. Phía bên phải của toán tử sẽ là một danh sách đối số, mỗi đối số là một biểu thức bình thường hoặc một giữ chỗ ?
. Mỗi giữ chỗ ?
liên tiếp sẽ đại diện cho một tham số khác.
Biểu thức bình thường sẽ được đánh giá trước khi hàm được tạo ra. Ví dụ, f~(g(), ?, h(), ?)
sẽ đánh giá f
, sau đó g()
, sau đó h()
, và sau đó sẽ tạo ra một phiên bản của f
đã được áp dụng một phần với hai đối số.
Số tùy chọn sau giữ chỗ ?
sẽ ghi đè vị trí của tham số. Ví dụ, f~(?1, ?0)
sẽ có hai tham số nhưng sẽ hoán đổi chúng khi gọi f
.
Cách tiếp cận thứ hai là với một cú pháp được đánh giá một cách lười biếng. Điều này có thể được xử lý với một mở rộng cho các ống Hack , với cú pháp được thêm sáng tạo bởi Clojure’s #(^1 ^2) function literals. Điều này sẽ được thực hiện bằng cách kết hợp ống Hack |>
với toán tử hàm mũi tên =>
thành một toán tử hàm ống +>
, sẽ sử dụng cùng các quy tắc tổng quát như |>
.
+>
sẽ là một toán tử tiền tố tạo ra một hàm mới , mà lần lượt sẽ gắn các đối số của nó với các tham chiếu chủ đề. Các hàm không đơn nguyên sẽ được tạo ra bằng cách bao gồm các tham chiếu chủ đề với số (%0
, %1
, %2
, v.v.) hoặc ...
. %0
(tương đương với %
đơn giản) sẽ được gắn vào đối số thứ không, %1
sẽ được gắn vào đối số tiếp theo, và cứ thế. %...
sẽ được gắn vào một mảng của các đối số còn lại. Và giống như với |>
, +>
sẽ yêu cầu thân của nó phải chứa ít nhất một tham chiếu chủ đề để có tính hợp lệ cú pháp.
Áp dụng hàm một phần sẵn sàng | Hàm ống |
---|---|
a.map(f~(?, 0)) |
a.map(+> f(%, 0)) |
a.map(f~(?, ?, 0)) |
a.map(+> f(%0, %1, 0)) |
a.map(x=> x + 1) |
a.map(+> % + 1) |
a.map(x=> x + x) |
a.map(+> % + %) |
a.map(x=> f(x, x)) |
a.map(+> f(%, %)) |
So với eagerly evaluated PFA syntax, các hàm chủ đề sẽ đánh giá một cách lười biếng các đối số của nó, giống như cách một hàm mũi tên làm.
Ví dụ, +> f(g(), %0, h(), %1)
sẽ đánh giá f
, sau đó nó sẽ tạo ra một hàm mũi tên kết hợp g
và h
. Hàm được tạo ra không đánh giá g()
hoặc h()
cho đến khi mọi lần hàm được tạo ra đều được gọi.
Bất kể cách tiếp cận nào được chọn, các ống Hack có thể tồn tại cùng với áp dụng hàm một phần.
Gửi / đặt ống trong tương lai
Mặc dù có chung từ “pipe” trong tên, toán tử pipe và pipeline đối tượng từ xa của eventual-send proposal là vuông góc và độc lập. Chúng có thể tồn tại và thậm chí còn làm việc cùng nhau.
const fileP = E(
E(target).openDirectory(dirName)
).openFile(fileName);
const fileP = target
|> E(%).openDirectory(dirName)
|> E(%).openFile(fileName);
Những mở rộng có thể trong tương lai
Cú pháp Hack-pipe cho if
, catch
, và for
–of
Nhiều câu lệnh **if**
, **catch**
, và **for**
có thể trở nên ngắn gọn hơn nếu chúng có “cú pháp pipe” để liên kết đến tham chiếu chủ đề.
if () |>
sẽ liên kết giá trị điều kiện của nó với %
,
catch |>
sẽ liên kết lỗi nó bắt được với %
,
và for (of) |>
sẽ liên kết lần lượt mỗi giá trị của trình lặp của nó với %
.
Status quo | Cú pháp câu lệnh Hack-pipe |
---|---|
const c = f(); if (c) g(c); |
if (f()) |> g(%); |
catch (e) f(e); |
catch |> f(%); |
for (const v of f()) g(v); |
for (f()) |> g(%); |
Optional Hack pipes
Toán tử optional-pipe ngắn mạch |?>
cũng có thể hữu ích, tương tự như ?.
hữu ích cho các lời gọi phương thức tùy chọn.
Ví dụ, value |> (% == null ? % : await foo(%) |> (% == null ? % : % + 1))
sẽ tương đương với value |?> await foo(%) |?> % + 1
.
Cú pháp áp dụng hàm đơn vị ngầm
Cú pháp cho áp dụng hàm đơn vị ngầm – tức là, toán tử pipe F# – đã bị từ chối hai lần bởi TC39. Tuy nhiên, chúng vẫn có thể được thêm vào ngôn ngữ theo hai cách.
Đầu tiên, nó có thể được thêm vào dưới dạng một hàm tiện ích Function.pipe
. Đây là điều mà function-helpers proposal đề xuất. Function.pipe
có thể loại bỏ nhiều nhu cầu cho một toán tử pipe F#, trong khi vẫn không loại trừ khả năng có một toán tử pipe F#.
Cảm ơn!
Thứ hai, nó có thể được thêm vào như một toán tử pipe khác |>>
– tương tự như cách Clojure has multiple pipe macros ->
, ->>
, và as->
.
Ví dụ, value |> % + 1 |>> f |> g(%, 0)
sẽ có nghĩa là value |> % + 1 |> f(%) |> g(%, 0)
.
Có một đề xuất không chính thức cho sự kết hợp phân tách của hai toán tử pipe, đã bị đặt sang một bên để ưu tiên các đề xuất toán tử đơn. Sự kết hợp phân tách này có thể trở lại dưới dạng đề xuất sau Hack pipes.
- Giai đoạn: 2
- Nhà vô địch: J. S. Choi, James DiGioia, Ron Buckton, Tab Atkins-Bittner, [danh sách chưa hoàn thiện]
- Nhà vô địch trước: Daniel Ehrenberg
- Specification
- Contributing guidelines
- Proposal history
- Plugin Babel: Implemented in v7.15. Xem Babel documentation.
(Tài liệu này sử dụng %
như là ký hiệu trình giữ chỗ cho tham chiếu chủ đề. Điều này gần như chắc chắn không phải lựa chọn cuối cùng; xem the token bikeshedding discussion để biết chi tiết.)
Chi tiết Tải xuống:
Tác giả: tc39
Mã nguồn: https://github.com/tc39/proposal-pipeline-operator
Giấy phép: BSD-3-Clause license
Cảm ơn!