Tại sao các toán tử do người dùng định nghĩa phổ biến hơn?


94

Một tính năng tôi bỏ lỡ từ các ngôn ngữ chức năng là ý tưởng rằng các toán tử chỉ là các hàm, vì vậy việc thêm một toán tử tùy chỉnh thường đơn giản như thêm một hàm. Nhiều ngôn ngữ thủ tục cho phép quá tải toán tử, do đó, trong một số trường hợp, toán tử vẫn là các hàm (điều này rất đúng trong D khi toán tử được truyền dưới dạng một chuỗi trong một tham số mẫu).

Dường như khi quá tải toán tử được cho phép, việc thêm các toán tử tùy chỉnh bổ sung thường là chuyện nhỏ. Tôi tìm thấy bài đăng trên blog này , lập luận rằng các toán tử tùy chỉnh không hoạt động tốt với ký hiệu infix vì các quy tắc ưu tiên, nhưng tác giả đưa ra một số giải pháp cho vấn đề này.

Tôi nhìn xung quanh và không thể tìm thấy bất kỳ ngôn ngữ thủ tục nào hỗ trợ các nhà khai thác tùy chỉnh bằng ngôn ngữ này. Có các bản hack (như macro trong C ++), nhưng hầu như không giống như hỗ trợ ngôn ngữ.

Vì tính năng này khá nhỏ để thực hiện, tại sao nó không phổ biến hơn?

Tôi hiểu rằng điều đó có thể dẫn đến một số mã xấu, nhưng trước đây đã không ngăn các nhà thiết kế ngôn ngữ thêm các tính năng hữu ích có thể dễ dàng bị lạm dụng (macro, toán tử ternary, con trỏ không an toàn).

Các trường hợp sử dụng thực tế:

  • Triển khai các toán tử bị thiếu (ví dụ Lua không có toán tử bitwise)
  • Mimic D's ~(nối mảng)
  • DSL
  • Sử dụng |như đường cú pháp kiểu Unix (sử dụng coroutines / máy phát điện)

Tôi cũng quan tâm đến ngôn ngữ mà làm phép nhà khai thác tùy chỉnh, nhưng tôi quan tâm nhiều hơn trong lý do tại sao nó đã được loại trừ. Tôi đã nghĩ đến việc sử dụng một ngôn ngữ kịch bản để thêm các toán tử do người dùng định nghĩa, nhưng tôi đã dừng lại khi nhận ra rằng tôi không thấy nó ở bất cứ đâu, vì vậy có lẽ có một lý do chính đáng tại sao các nhà thiết kế ngôn ngữ thông minh hơn tôi không cho phép.


4
Có một cuộc thảo luận Reddit đang diễn ra về câu hỏi này.
Động

2
@dimatura: Tôi không biết nhiều về R, nhưng các toán tử tùy chỉnh vẫn có vẻ không linh hoạt (ví dụ: không có cách thích hợp nào để xác định tính cố định, phạm vi, quá tải, v.v.), điều này giải thích tại sao chúng không được sử dụng nhiều. Điều đó khác với các ngôn ngữ khác, chắc chắn là ở Haskell, nơi sử dụng các toán tử infix tùy chỉnh rất nhiều . Một ví dụ khác về ngôn ngữ thủ tục với sự hỗ trợ hợp lý tùy chỉnh hợp lý là Nimrod và dĩ nhiên Perl cũng cho phép chúng .
leftaroundabout

4
Có một lựa chọn khác, Lisp thực sự không có sự khác biệt giữa các toán tử và hàm.
BeardedO

4
Chưa đề cập đến Prolog, một ngôn ngữ khác trong đó các toán tử chỉ là đường cú pháp cho các hàm (có ngay cả toán học) và cho phép bạn xác định các toán tử tùy chỉnh với quyền ưu tiên tùy chỉnh.
David Cowden

2
@BeardedO, không có toán tử infix nào trong Lisp cả. Khi bạn giới thiệu họ, bạn sẽ phải giải quyết tất cả các vấn đề được ưu tiên và như vậy.
SK-logic

Câu trả lời:


134

Có hai trường phái tư tưởng trái ngược nhau trong thiết kế ngôn ngữ lập trình. Một là các lập trình viên viết mã tốt hơn với ít hạn chế hơn và hai là họ viết mã tốt hơn với nhiều hạn chế hơn. Theo tôi, thực tế là các lập trình viên giàu kinh nghiệm phát triển với ít hạn chế hơn, nhưng những hạn chế đó có thể có lợi cho chất lượng mã của người mới bắt đầu.

Các toán tử do người dùng định nghĩa có thể tạo mã rất thanh lịch trong tay có kinh nghiệm và mã hoàn toàn khủng khiếp bởi người mới bắt đầu. Vì vậy, liệu ngôn ngữ của bạn có bao gồm chúng hay không phụ thuộc vào trường phái tư tưởng của nhà thiết kế ngôn ngữ của bạn.


23
Trên hai trường phái tư tưởng, plus.google.com/110981030061712822816/posts/KaSKeg4vQtz cũng +1 vì có câu trả lời trực tiếp nhất (và có thể đúng nhất) cho câu hỏi tại sao các nhà thiết kế ngôn ngữ chọn cái này so với cái kia. Tôi cũng sẽ nói dựa trên luận điểm của bạn, bạn có thể ngoại suy rằng ít ngôn ngữ hơn cho phép vì một phần nhỏ các nhà phát triển có tay nghề cao hơn so với định nghĩa (phần trăm hàng đầu của mọi thứ sẽ luôn là thiểu số theo định nghĩa)
Jimmy Hoffa

12
Tôi nghĩ rằng câu trả lời này là mơ hồ và chỉ liên quan đến câu hỏi. (Tôi cũng tình cờ nghĩ rằng tính cách của bạn là sai vì nó mâu thuẫn với kinh nghiệm của tôi nhưng điều đó không quan trọng.)
Konrad Rudolph

1
Như @KonradRudolph đã nói, điều này không thực sự trả lời câu hỏi. Sử dụng một ngôn ngữ như Prolog cho phép bạn làm bất cứ điều gì bạn muốn với các toán tử bao gồm xác định mức độ ưu tiên của chúng khi đặt infix hoặc tiền tố. Tôi không nghĩ rằng việc bạn có thể viết các toán tử tùy chỉnh có liên quan gì đến mức độ kỹ năng của đối tượng mục tiêu, mà là vì mục tiêu của Prolog là đọc càng logic càng tốt. Việc bao gồm các toán tử tùy chỉnh cho phép bạn viết các chương trình đọc rất logic (sau tất cả một chương trình prolog chỉ là một loạt các câu lệnh logic).
David Cowden

Làm thế nào tôi đã bỏ lỡ lời nói sáo rỗng đó? Ôi trời, đánh dấu trang
George Mauer

Tôi sẽ rơi vào triết lý nào nếu tôi nghĩ rằng các lập trình viên có khả năng viết mã tốt nhất nếu các ngôn ngữ cung cấp cho họ các công cụ để nói khi trình biên dịch nên đưa ra các suy luận khác nhau (ví dụ như các đối số tự động theo Formatphương thức) và khi nào nên từ chối ( ví dụ: đối số tự động đấm bốc đến ReferenceEquals). Ngôn ngữ càng có nhiều khả năng giúp các lập trình viên nói khi nào những suy luận nhất định sẽ không phù hợp, thì nó càng có thể đưa ra những suy luận thuận tiện khi thích hợp.
supercat

83

Đưa ra lựa chọn giữa các mảng ghép với ~ hoặc với "myArray.Concat (secondArray)", tôi có thể thích cái sau hơn. Tại sao? Bởi vì ~ là một ký tự hoàn toàn vô nghĩa chỉ có ý nghĩa của nó - đó là nối chuỗi - được đưa ra trong dự án cụ thể nơi nó được viết.

Về cơ bản, như bạn đã nói, các toán tử không khác với các phương thức. Nhưng trong khi các phương thức có thể được cung cấp các tên dễ hiểu, dễ hiểu để tăng thêm sự hiểu biết về dòng mã, thì các toán tử lại mờ đục và tình huống.

Đây là lý do tại sao tôi cũng không thích .toán tử của PHP (nối chuỗi) hoặc hầu hết các toán tử trong Haskell hoặc OCaml, mặc dù trong trường hợp này, một số tiêu chuẩn được chấp nhận phổ biến đang xuất hiện cho các ngôn ngữ chức năng.


27
Điều tương tự có thể được nói của tất cả các nhà khai thác. Điều làm cho chúng tốt và cũng có thể làm cho các toán tử do người dùng xác định tốt (nếu chúng được xác định một cách thận trọng), là: Mặc dù ký tự được sử dụng cho chúng chỉ là một ký tự, sử dụng rộng rãi và có thể là các thuộc tính ghi nhớ có nghĩa là mã nguồn ngay lập tức rõ ràng cho những người quen thuộc với nó. Vì vậy, câu trả lời của bạn không phải là IMHO thuyết phục.

33
Như tôi đã đề cập ở phần cuối, một số toán tử có tính nhận biết phổ quát (chẳng hạn như các ký hiệu số học tiêu chuẩn, dịch chuyển bitwise, AND / OR, v.v.), và do đó độ căng của chúng vượt trội hơn độ mờ đục của chúng. Nhưng việc có thể định nghĩa các nhà khai thác tùy ý dường như giữ được điều tồi tệ nhất của cả hai thế giới.
Avner Shahar-Kashtan

11
Vì vậy, bạn cũng ủng hộ việc cấm, ví dụ, tên một chữ cái ở cấp độ ngôn ngữ? Nói chung, không cho phép bất cứ điều gì trong ngôn ngữ dường như gây hại nhiều hơn là tốt? Đó là một cách tiếp cận hợp lệ để thiết kế ngôn ngữ, nhưng không phải là cách duy nhất, vì vậy tôi không nghĩ rằng bạn không thể đoán trước được.

9
Tôi không đồng ý rằng các nhà khai thác tùy chỉnh đang "thêm" một tính năng, nó sẽ loại bỏ một hạn chế. Các toán tử nói chung chỉ là các hàm, vì vậy thay vì một bảng ký hiệu tĩnh cho các toán tử, một toán tử động, phụ thuộc vào ngữ cảnh có thể được sử dụng thay thế. Tôi tưởng tượng đây là cách xử lý quá tải toán tử, bởi vì +<<chắc chắn không được xác định trên Object(Tôi nhận được "không khớp với toán tử + trong ..." khi thực hiện điều đó trên một lớp trần trong C ++).
beatgammit

10
Điều cần có một thời gian dài để nhận ra rằng mã hóa thường không phải là để máy tính làm điều gì đó cho bạn, mà là tạo ra một tài liệu mà cả người và máy tính đều có thể đọc được, với những người quan trọng hơn máy tính một chút. Câu trả lời này hoàn toàn chính xác, Nếu người tiếp theo (hoặc bạn trong 2 năm) phải mất thậm chí 10 giây để tìm hiểu ý nghĩa của ~, thì bạn cũng có thể đã dành 10 giây để gõ một cuộc gọi phương thức thay thế.
Bill K

71

Vì tính năng này khá nhỏ để thực hiện, tại sao nó không phổ biến hơn?

Tiền đề của bạn là sai. Nó không phải là tầm thường để thực hiện trực tiếp. Trong thực tế, nó mang lại một túi các vấn đề.

Chúng ta hãy xem các giải pháp được đề xuất trên thế giới trong bài viết:

  • Không có quyền ưu tiên . Bản thân tác giả cho biết, không sử dụng quy tắc ưu tiên đơn giản không phải là một lựa chọn.
  • Phân tích nhận thức ngữ nghĩa . Giống như bài báo nói, điều này sẽ đòi hỏi trình biên dịch phải có nhiều kiến ​​thức ngữ nghĩa. Bài viết không thực sự cung cấp một giải pháp cho điều này và để tôi nói với bạn, điều này đơn giản là không tầm thường. Trình biên dịch được thiết kế như một sự đánh đổi giữa sức mạnh và sự phức tạp. Cụ thể, tác giả đề cập đến một bước phân tích trước để thu thập thông tin liên quan, nhưng phân tích trước là không hiệu quả và trình biên dịch cố gắng rất nhiều để giảm thiểu việc phân tích cú pháp.
  • Không có toán tử infix tùy chỉnh . Chà, đó không phải là một giải pháp.
  • Giải pháp lai . Giải pháp này mang nhiều (nhưng không phải tất cả) những nhược điểm của phân tích nhận thức ngữ nghĩa. Cụ thể, do trình biên dịch phải coi các mã thông báo không xác định là có khả năng đại diện cho các toán tử tùy chỉnh, nên nó thường không thể tạo ra các thông báo lỗi có ý nghĩa. Nó cũng có thể yêu cầu định nghĩa của toán tử đã nói để tiến hành phân tích cú pháp (để thu thập thông tin loại, v.v.), một lần nữa yêu cầu một phân tích cú pháp bổ sung.

Nói chung, đây là một tính năng đắt tiền để thực hiện, cả về độ phức tạp của trình phân tích cú pháp và về hiệu suất, và không rõ ràng rằng nó sẽ mang lại rất nhiều lợi ích. Chắc chắn, có một số lợi ích đối với khả năng xác định toán tử mới nhưng ngay cả những toán tử gây tranh cãi (chỉ cần nhìn vào các câu trả lời khác cho rằng có toán tử mới không phải là một điều tốt).


2
Cảm ơn tiếng nói của sự tỉnh táo. Kinh nghiệm của tôi cho thấy rằng những người coi những thứ tầm thường là chuyên gia hoặc không biết gì về hạnh phúc, và thường xuyên hơn trong loại sau: x
Matthieu M.

14
mỗi một trong những vấn đề này đã được giải quyết bằng các ngôn ngữ đã tồn tại được 20 năm ...
Philip JF

11
Tuy nhiên, nó là đặc biệt tầm thường để thực hiện. Hãy quên đi tất cả các thuật toán phân tích cuốn sách rồng, đó là thế kỷ 21 và đã đến lúc tiến lên. Ngay cả việc mở rộng một cú pháp ngôn ngữ cũng dễ dàng và việc thêm các toán tử của một ưu tiên nhất định là không đáng kể. Hãy xem, nói, trình phân tích cú pháp Haskell. Nó đơn giản hơn nhiều so với các trình phân tích cú pháp cho các ngôn ngữ cồng kềnh "chính thống".
SK-logic

3
@ SK-logic Khẳng định chăn của bạn không thuyết phục tôi. Vâng, phân tích đã di chuyển trên. Không, việc thực hiện các ưu tiên toán tử tùy ý tùy thuộc vào loại không phải là tầm thường. Sản xuất các thông báo lỗi tốt với bối cảnh hạn chế không phải là tầm thường. Sản xuất các trình phân tích cú pháp hiệu quả với các ngôn ngữ yêu cầu nhiều lượt để chuyển đổi là không khả thi.
Konrad Rudolph

6
Trong Haskell, phân tích cú pháp là "dễ dàng" (so với hầu hết các ngôn ngữ). Ưu tiên là bối cảnh độc lập. Phần cung cấp cho bạn các thông báo lỗi cứng có liên quan đến kiểu chữ và các tính năng hệ thống loại nâng cao, không liên quan đến các toán tử do người dùng xác định. Đó là quá tải, không phải định nghĩa người dùng, đó là vấn đề khó khăn.
Philip JF

25

Chúng ta hãy bỏ qua toàn bộ đối số "các nhà khai thác bị lạm dụng để gây hại cho khả năng đọc" và tập trung vào ý nghĩa thiết kế ngôn ngữ.

Các toán tử Infix có nhiều vấn đề hơn các quy tắc ưu tiên đơn giản (mặc dù nói thẳng ra, liên kết mà bạn tham chiếu tầm thường hóa tác động của quyết định thiết kế đó). Một là giải quyết xung đột: điều gì xảy ra khi bạn xác định a.operator+(b)b.operator+(a)? Ưu tiên cái này hơn cái kia dẫn đến phá vỡ tài sản giao hoán dự kiến ​​của nhà điều hành đó. Ném một lỗi có thể dẫn đến các mô-đun sẽ làm việc bị hỏng một lần cùng nhau. Điều gì xảy ra khi bạn bắt đầu ném các loại dẫn xuất vào hỗn hợp?

Thực tế của vấn đề là các nhà khai thác không chỉ là chức năng. Các hàm độc lập hoặc được sở hữu bởi lớp của chúng, cung cấp một ưu tiên rõ ràng về tham số nào (nếu có) sở hữu công văn đa hình.

Và điều đó bỏ qua các vấn đề bao bì và độ phân giải khác nhau phát sinh từ các nhà khai thác. Lý do các nhà thiết kế ngôn ngữ (bằng và lớn) giới hạn định nghĩa toán tử trung gian là bởi vì nó tạo ra một đống vấn đề cho ngôn ngữ trong khi cung cấp lợi ích gây tranh cãi.

Và thẳng thắn, bởi vì chúng không tầm thường để thực hiện.


1
Tôi tin rằng đây là lý do Java không bao gồm chúng, nhưng các ngôn ngữ có chúng có các quy tắc rõ ràng về quyền ưu tiên. Tôi nghĩ nó tương tự như nhân ma trận. Nó không giao hoán, vì vậy bạn phải lưu ý khi bạn sử dụng ma trận. Tôi nghĩ điều tương tự cũng áp dụng khi giao dịch với các lớp học, nhưng vâng, tôi đồng ý rằng việc không giao hoán +là xấu xa. Nhưng đây có thực sự là một đối số chống lại các toán tử do người dùng định nghĩa? Có vẻ như một cuộc tranh cãi chống lại quá tải toán tử nói chung.
beatgammit

1
@tjameson - Có, ngôn ngữ có quy tắc rõ ràng về quyền ưu tiên, đối với toán học . Các quy tắc ưu tiên cho các hoạt động toán học có thể sẽ không thực sự được áp dụng nếu các toán tử đang bị quá tải cho những thứ như boost::spirit. Ngay khi bạn cho phép các toán tử do người dùng định nghĩa trở nên tồi tệ hơn vì không có cách nào tốt để thậm chí xác định trước ưu tiên cho toán học. Tôi đã viết một chút về bản thân mình trong bối cảnh của một ngôn ngữ đặc biệt tìm cách giải quyết các vấn đề với các toán tử được xác định tùy ý.
Telastyn

22
Tôi gọi là nhảm nhí, thưa ngài. Có cuộc sống bên ngoài OO-ghetto và có các chức năng không nhất thiết thuộc về bất kỳ đối tượng nào , do đó, việc tìm đúng chức năng hoàn toàn giống như tìm đúng toán tử.
Matthieu M.

1
@matthieuM. - Chắc chắn rồi. Quy tắc sở hữu và điều phối được thống nhất hơn trong các ngôn ngữ không hỗ trợ các chức năng thành viên. Tuy nhiên, tại thời điểm đó, câu hỏi ban đầu trở thành 'tại sao các ngôn ngữ không phải là OO phổ biến hơn?' đó là một trò chơi bóng khác
Telastyn

11
Bạn đã xem Haskell thực hiện các toán tử tùy chỉnh như thế nào chưa? Chúng hoạt động chính xác như các chức năng bình thường, ngoại trừ chúng cũng có một ưu tiên liên quan. (Trên thực tế, các hàm bình thường cũng vậy, do đó, ngay cả ở đó chúng không thực sự khác biệt.) Về cơ bản, các toán tử được mặc định theo mặc định và tên là tiền tố, nhưng đó là sự khác biệt duy nhất.
Tikhon Jelvis

19

Tôi nghĩ bạn sẽ ngạc nhiên về mức độ thường xuyên quá tải của nhà điều hành được thực hiện trong một số hình thức. Nhưng chúng không được sử dụng phổ biến trong nhiều cộng đồng.

Tại sao sử dụng ~ để nối với một mảng? Tại sao không sử dụng << như Ruby không ? Bởi vì các lập trình viên bạn làm việc cùng có lẽ không phải là lập trình viên Ruby. Hoặc lập trình viên D. Vậy họ làm gì khi gặp mã của bạn? Họ phải đi và tìm kiếm những gì biểu tượng có nghĩa.

Tôi đã từng làm việc với một nhà phát triển C # rất giỏi, người cũng có sở thích về ngôn ngữ chức năng. Ra khỏi màu xanh, ông bắt đầu giới thiệu monads C # bằng cách phương pháp khuyến nông và sử dụng thuật ngữ đơn nguyên tiêu chuẩn. Không ai có thể tranh cãi rằng một số mã của anh ta đã bị thay đổi và thậm chí còn dễ đọc hơn một khi bạn biết ý nghĩa của nó, nhưng điều đó có nghĩa là mọi người phải học thuật ngữ đơn âm trước khi mã có ý nghĩa .

Đủ công bằng, bạn nghĩ sao? Đó chỉ là một đội nhỏ. Cá nhân, tôi không đồng ý. Mỗi nhà phát triển mới đã được định sẵn để bị nhầm lẫn bởi thuật ngữ này. Chúng ta không có đủ vấn đề khi học một tên miền mới?

Mặt khác, tôi sẽ vui vẻ sử dụng các ??toán tử trong C # vì tôi mong đợi các nhà phát triển C # khác để biết nó là gì, nhưng tôi sẽ không quá tải nó thành một ngôn ngữ mà không hỗ trợ nó theo mặc định.


Tôi không hiểu ví dụ Double.NaN của bạn, hành vi này là một phần của thông số dấu phẩy động và được hỗ trợ trong mọi ngôn ngữ tôi đã sử dụng. Bạn có nói rằng các nhà khai thác tùy chỉnh không được hỗ trợ bởi vì nó sẽ gây nhầm lẫn cho các nhà phát triển không biết về tính năng này? Nghe có vẻ giống như lập luận chống lại việc sử dụng toán tử ternary hoặc ??ví dụ của bạn .
beatgammit

@tjameson: Thật ra, bạn nói đúng, đó là một chút tiếp tuyến đến điểm mà tôi đang cố gắng thực hiện, và không được viết đặc biệt. Tôi nghĩ về ?? ví dụ như tôi đã viết nó và thích ví dụ đó. Loại bỏ đoạn đôi.NaN.
pdr

6
Tôi nghĩ rằng "mọi nhà phát triển mới của bạn đã bị nhầm lẫn bởi thuật ngữ này" là một chút mờ nhạt, lo lắng về đường cong học tập của các nhà phát triển mới đối với cơ sở mã của bạn cũng giống như lo lắng về đường cong học tập của các nhà phát triển mới đối với phiên bản mới của .NET. Tôi nghĩ quan trọng hơn là khi các nhà phát triển học nó (.NET mới hoặc cơ sở mã của bạn), nó có ý nghĩa và hiển thị giá trị không? Nếu vậy, đường cong học tập là xứng đáng, bởi vì mỗi nhà phát triển chỉ cần học nó một lần, mặc dù mã cần được xử lý vô số lần, vì vậy nếu nó cải thiện mã thì đó là một chi phí nhỏ.
Jimmy Hoffa

7
@JimmyHoffa Đó là một chi phí định kỳ. Trả trước cho mọi nhà phát triển mới và cũng ảnh hưởng đến các nhà phát triển hiện tại vì họ phải hỗ trợ nó. Có một chi phí tài liệu cũng như một yếu tố rủi ro - nó cảm thấy đủ an toàn cho đến ngày hôm nay, nhưng sau 30 năm, mọi người sẽ tiếp tục, ngôn ngữ và ứng dụng hiện đang là "di sản", tài liệu này là một đống hấp lớn và một số kẻ hút người nghèo sẽ bị xé tóc trước sự sáng chói của các lập trình viên quá lười biếng để gõ ".concat ()". Là giá trị dự kiến ​​là đủ để bù đắp chi phí?
Sylverdrag

3
Ngoài ra, lập luận "Mọi nhà phát triển mới đã bị định mệnh nhầm lẫn bởi thuật ngữ này. Chúng ta không có đủ vấn đề khi học một tên miền mới phải không?" có thể đã được áp dụng cho OOP từ những năm 80, lập trình có cấu trúc trước đó hoặc IoC 10 năm trước. Đã đến lúc ngừng sử dụng lập luận ngụy biện này.
Mauricio Scheffer

11

Tôi có thể nghĩ ra một vài lý do:

  • Chúng không tầm thường để thực hiện - cho phép các toán tử tùy chỉnh tùy ý có thể làm cho trình biên dịch của bạn phức tạp hơn nhiều, đặc biệt nếu bạn cho phép các quy tắc ưu tiên, sửa lỗi và quy tắc do người dùng xác định. Nếu sự đơn giản là một ưu điểm, thì quá tải toán tử sẽ đưa bạn ra khỏi thiết kế ngôn ngữ tốt.
  • Họ bị lạm dụng - chủ yếu bởi các lập trình viên nghĩ rằng thật tuyệt vời khi xác định lại các toán tử và bắt đầu xác định lại chúng cho tất cả các loại lớp tùy chỉnh. Chẳng bao lâu, mã của bạn chứa đầy các ký hiệu tùy chỉnh mà không ai khác có thể đọc hoặc hiểu được vì các toán tử không tuân theo các quy tắc được hiểu rõ thông thường. Tôi không mua đối số "DSL", trừ khi DSL của bạn là tập con của toán học :-)
  • Chúng làm tổn thương khả năng đọc và khả năng bảo trì - nếu các nhà khai thác thường xuyên bị ghi đè, có thể khó phát hiện khi cơ sở này đang được sử dụng và các lập trình viên buộc phải liên tục tự hỏi người vận hành đang làm gì. Nó là tốt hơn nhiều để cung cấp tên chức năng có ý nghĩa. Gõ thêm một vài ký tự là rẻ, vấn đề bảo trì dài hạn là tốn kém.
  • Họ có thể phá vỡ kỳ vọng hiệu suất ngầm . Ví dụ, tôi thường mong đợi việc tra cứu một phần tử trong một mảng O(1). Nhưng với quá tải toán tử, someobject[i]có thể dễ dàng là một O(n)hoạt động tùy thuộc vào việc thực hiện toán tử lập chỉ mục.

Trong thực tế, có rất ít trường hợp quá tải toán tử có công dụng chính đáng so với việc chỉ sử dụng các chức năng thông thường. Một ví dụ hợp pháp có thể là thiết kế một lớp số phức được sử dụng bởi các nhà toán học, những người hiểu các cách hiểu rõ mà các toán tử toán học được định nghĩa cho các số phức. Nhưng đây thực sự không phải là một trường hợp rất phổ biến.

Một số trường hợp thú vị để xem xét:

  • Lisps : nói chung không phân biệt tất cả giữa các toán tử và hàm - +chỉ là một hàm thông thường. Bạn có thể xác định các chức năng theo cách bạn thích (thông thường có một cách xác định chúng trong các không gian tên riêng biệt để tránh xung đột với tích hợp +), bao gồm các toán tử. Nhưng có một xu hướng văn hóa là sử dụng các tên hàm có ý nghĩa, vì vậy điều này không bị lạm dụng nhiều. Ngoài ra, trong ký hiệu tiền tố Lisp có xu hướng được sử dụng riêng, do đó, có ít giá trị hơn trong "đường cú pháp" mà quá tải toán tử cung cấp.
  • Java - không cho phép quá tải toán tử. Điều này đôi khi gây khó chịu (đối với những thứ như trường hợp số phức) nhưng trung bình có lẽ đó là quyết định thiết kế đúng cho Java, được dùng như một ngôn ngữ OOP đơn giản, có mục đích chung. Mã Java thực sự khá dễ dàng đối với các nhà phát triển có tay nghề thấp / trung bình để duy trì do sự đơn giản này.
  • C ++ có quá tải toán tử rất tinh vi. Đôi khi điều này bị lạm dụng ( cout << "Hello World!"bất cứ ai?) Nhưng cách tiếp cận có nghĩa là định vị của C ++ là một ngôn ngữ phức tạp cho phép lập trình cấp cao trong khi vẫn cho phép bạn tiến gần đến kim loại để thực hiện, vì vậy bạn có thể viết một lớp Số phức hoạt động chính xác như bạn muốn mà không ảnh hưởng đến hiệu suất. Điều này được hiểu rằng đó là trách nhiệm của riêng bạn nếu bạn tự bắn vào chân mình.

8

Vì tính năng này khá nhỏ để thực hiện, tại sao nó không phổ biến hơn?

không tầm thường để thực hiện (trừ khi thực hiện tầm thường). Nó cũng không giúp bạn có được nhiều thứ, ngay cả khi được thực hiện một cách lý tưởng: mức tăng khả năng đọc được từ sự căng thẳng được bù đắp bằng những mất mát có thể đọc được từ sự không quen thuộc và độ mờ đục. Nói tóm lại, nó không phổ biến vì nó thường không xứng đáng với thời gian của nhà phát triển hoặc người dùng.

Điều đó nói rằng, tôi có thể nghĩ về ba ngôn ngữ làm điều đó và họ làm điều đó theo những cách khác nhau:

  • Vợt, một lược đồ, khi nó không phải là tất cả biểu thức S cho phép và mong bạn viết một số tiền cho trình phân tích cú pháp cho bất kỳ cú pháp nào bạn muốn mở rộng (và cung cấp các móc hữu ích để thực hiện điều này có thể thực hiện được).
  • Haskell, một ngôn ngữ lập trình chức năng thuần túy, cho phép xác định bất kỳ toán tử nào chỉ bao gồm dấu câu và cho phép bạn cung cấp một mức độ cố định (có sẵn 10) và tính kết hợp. Toán tử ternary vv có thể được tạo ra từ các toán tử nhị phân và các hàm bậc cao hơn.
  • Agda, một ngôn ngữ lập trình được gõ phụ thuộc, cực kỳ linh hoạt với các toán tử (giấy ở đây ) cho phép cả định nghĩa if-then và if-then-other được định nghĩa là các toán tử trong cùng một chương trình, nhưng trình phân tích cú pháp, trình phân tích cú pháp và trình đánh giá của nó đều được kết hợp mạnh mẽ kết quả là

4
Bạn đã nói nó "không tầm thường", và ngay lập tức liệt kê ba ngôn ngữ có một số triển khai tầm thường. Vì vậy, nó khá tầm thường, phải không?
SK-logic

7

Một trong những lý do chính khiến các nhà khai thác tùy chỉnh không được khuyến khích là vì sau đó bất kỳ nhà khai thác nào cũng có thể có nghĩa / có thể làm bất cứ điều gì.

Ví dụ cstream, nhiều chỉ trích quá tải trái ca.

Khi một ngôn ngữ cho phép người vận hành quá tải, thường có một sự khuyến khích để giữ cho hành vi của người vận hành tương tự như hành vi cơ sở để tránh nhầm lẫn.

Ngoài ra các toán tử do người dùng định nghĩa làm cho việc phân tích cú pháp khó khăn hơn nhiều, đặc biệt là khi cũng có các quy tắc tùy chọn tùy chỉnh.


6
Tôi không thấy các phần về quá tải toán tử áp dụng để xác định các toán tử hoàn toàn mới.

Tôi đã thấy sử dụng rất tốt quá tải toán tử (thư viện đại số tuyến tính) và rất tệ như bạn đã đề cập. Tôi không nghĩ rằng đây là một lập luận tốt chống lại các nhà khai thác tùy chỉnh. Việc phân tích cú pháp có thể là một vấn đề, nhưng nó khá rõ ràng khi một nhà điều hành được mong đợi. Sau khi phân tích cú pháp, tùy thuộc vào trình tạo mã để tìm ý nghĩa của toán tử.
beatgammit

3
Và điều này khác với quá tải phương pháp như thế nào?
Jörg W Mittag

@ JörgWMittag với quá tải phương thức có (hầu hết thời gian) một tên có ý nghĩa kèm theo. với một biểu tượng khó có thể giải thích ngắn gọn về những gì sẽ xảy ra
ratchet freak

1
@ratchetfreak: Vâng. +thêm hai thứ, -trừ chúng, *nhân chúng. Cảm giác của tôi là không ai bắt buộc lập trình viên thực hiện chức năng / phương thức addthực sự thêm bất cứ thứ gì và doNothingcó thể khởi chạy các hạt nhân. Và a.plus(b.minus(c.times(d)).times(e)sau đó ít được đọc hơn a + (b - c * d) * e(phần thưởng được thêm vào - trong đó sting đầu tiên là lỗi trong phiên âm). Tôi không thấy cách đầu tiên có ý nghĩa hơn ...
Maciej Piechotka

4

Chúng tôi không sử dụng các toán tử do người dùng định nghĩa cho cùng một lý do chúng tôi không sử dụng các từ do người dùng xác định. Không ai gọi chức năng của họ là "sworp". Cách duy nhất để truyền đạt suy nghĩ của bạn cho người khác là sử dụng ngôn ngữ chung. Và điều đó có nghĩa là cả hai từ và dấu hiệu (toán tử) phải được biết đến với xã hội mà bạn đang viết mã của mình.

Do đó, các toán tử mà bạn thấy được sử dụng trong các ngôn ngữ lập trình là những toán tử chúng ta đã được dạy ở trường (số học) hoặc các toán tử đã được thiết lập trong cộng đồng lập trình, như các toán tử boolean nói.


1
Thêm vào đó là khả năng khám phá ý nghĩa đằng sau một chức năng. Trong trường hợp "sworp", bạn có thể dễ dàng dán nó vào hộp tìm kiếm và tìm tài liệu của nó. Dán một người điều khiển vào một công cụ tìm kiếm là một công viên bóng hoàn toàn khác. Các công cụ tìm kiếm hiện tại của chúng tôi chỉ đơn giản là tìm kiếm các nhà khai thác và tôi đã lãng phí rất nhiều thời gian để cố gắng tìm ra ý nghĩa trong một số trường hợp.
Đánh dấu H

1
Bạn đếm được bao nhiêu "trường học"? Ví dụ, tôi nghĩ ∈ cho elemlà một ý tưởng tuyệt vời và chắc chắn một nhà điều hành mọi người nên hiểu, nhưng những người khác dường như không đồng ý.
Tikhon Jelvis

1
Đó không phải là vấn đề với biểu tượng. Đó là vấn đề với bàn phím. Tôi ví dụ sử dụng emacs haskell-mode với chế độ làm đẹp unicode được bật. Và nó tự động chuyển đổi các toán tử ascii thành các ký hiệu unicode. Nhưng tôi sẽ không thể gõ nó nếu không có ascii tương đương.
Vagif Verdi

2
Ngôn ngữ tự nhiên được xây dựng chỉ từ "từ do người dùng định nghĩa" và không có gì khác. Và một số ngôn ngữ thực sự khuyến khích hình thành từ tùy chỉnh.
SK-logic

4

Đối với các ngôn ngữ hỗ trợ quá tải như vậy: Scala thực sự, theo cách tốt hơn và tốt hơn có thể C ++. Hầu hết các ký tự có thể được sử dụng trong tên hàm, vì vậy bạn có thể xác định các toán tử như! + * = ++, nếu bạn muốn. Có hỗ trợ tích hợp cho infix (cho tất cả các hàm lấy một đối số). Tôi nghĩ bạn cũng có thể định nghĩa tính kết hợp của các chức năng đó. Tuy nhiên, bạn không thể xác định quyền ưu tiên (chỉ với các thủ thuật xấu, xem tại đây ).


4

Một điều chưa được đề cập đến là trường hợp của Smalltalk, nơi mọi thứ (bao gồm cả các nhà khai thác) là một tin nhắn gửi. "Toán tử" thích +, |v.v. thực sự là các phương thức đơn nguyên.

Tất cả các phương thức có thể được ghi đè, a + bcó nghĩa là cộng số nguyên nếu abđều là số nguyên và có nghĩa là phép cộng nếu cả hai đều OrderedCollectionlà s.

Không có quy tắc ưu tiên, vì đây chỉ là các cuộc gọi phương thức. Điều này có một ý nghĩa quan trọng đối với ký hiệu toán học tiêu chuẩn: 3 + 4 * 5phương tiện (3 + 4) * 5, không 3 + (4 * 5).

.


3

Bạn đang chiến đấu chống lại hai điều ở đây:

  1. Tại sao các nhà khai thác tồn tại trong các ngôn ngữ ở nơi đầu tiên?
  2. Đức tính của các nhà khai thác trên các chức năng / phương pháp là gì?

Trong hầu hết các ngôn ngữ, các toán tử không thực sự được thực hiện như các hàm đơn giản. Chúng có thể có một số giàn giáo chức năng, nhưng trình biên dịch / thời gian chạy nhận thức rõ ràng về ý nghĩa ngữ nghĩa của chúng và cách dịch chúng hiệu quả sang mã máy. Điều này đúng hơn nhiều so với các hàm dựng sẵn (đó là lý do tại sao hầu hết các triển khai cũng không bao gồm tất cả các hàm gọi qua chức năng trong quá trình thực hiện của chúng). Hầu hết các toán tử là trừu tượng mức cao hơn trên các hướng dẫn nguyên thủy được tìm thấy trong CPU (đó là một phần lý do tại sao hầu hết các toán tử là số học, boolean hoặc bitwise). Bạn có thể mô hình hóa chúng thành các hàm "đặc biệt" (gọi chúng là "nguyên thủy" hoặc "nội dung" hoặc "nguyên gốc" hoặc bất cứ thứ gì), nhưng để làm điều đó một cách khái quát đòi hỏi một bộ ngữ nghĩa rất mạnh mẽ để xác định các hàm đặc biệt đó. Cách khác là có các toán tử dựng sẵn trông giống như các toán tử do người dùng định nghĩa, nhưng nếu không thì gọi các đường dẫn đặc biệt trong trình biên dịch. Đó là câu trả lời cho câu hỏi thứ hai ...

Ngoài vấn đề dịch máy tôi đã đề cập ở trên, ở cấp độ cú pháp, các toán tử không thực sự khác biệt với các chức năng. Các đặc điểm phân biệt của chúng có xu hướng là chúng ngắn gọn và tượng trưng, ​​gợi ý về một đặc điểm bổ sung quan trọng mà chúng phải có ích: chúng phải có ý nghĩa / ngữ nghĩa được hiểu rộng rãi cho các nhà phát triển. Các biểu tượng ngắn không truyền tải nhiều ý nghĩa trừ khi đó là bàn tay ngắn cho một tập hợp các ngữ nghĩa đã được hiểu. Điều đó làm cho các toán tử do người dùng định nghĩa vốn không có ích, vì bản chất của chúng, chúng không được hiểu rộng rãi như vậy. Chúng có ý nghĩa nhiều như một hoặc hai tên hàm chữ cái.

Quá tải toán tử của C ++ cung cấp mảnh đất màu mỡ để kiểm tra điều này. Hầu hết quá trình "lạm dụng" của nhà điều hành xuất hiện dưới dạng quá tải phá vỡ một số hợp đồng ngữ nghĩa được hiểu rộng rãi (một ví dụ cổ điển là sự quá tải của toán tử + sao cho a + b! = B + a hoặc trong đó + sửa đổi một trong hai Toán hạng).

Nếu bạn nhìn vào Smalltalk, công cụ cho phép nạp chồng toán tử toán tử do người dùng xác định, bạn có thể thấy ngôn ngữ có thể thực hiện như thế nào và mức độ hữu dụng của nó. Trong Smalltalk, các toán tử chỉ là các phương thức có các thuộc tính cú pháp khác nhau (cụ thể, chúng được mã hóa dưới dạng nhị phân infix). Ngôn ngữ sử dụng "phương thức nguyên thủy" cho các phương thức và toán tử tăng tốc đặc biệt. Bạn thấy rằng rất ít nếu có bất kỳ toán tử do người dùng xác định nào được tạo và khi có, chúng có xu hướng không được sử dụng nhiều như tác giả có thể dự định sử dụng chúng. Ngay cả tương đương với quá tải toán tử là hiếm, bởi vì phần lớn là mất mạng để định nghĩa hàm mới là toán tử thay vì phương thức, vì cái sau cho phép biểu thị ngữ nghĩa của hàm.


1

Tôi luôn thấy quá tải toán tử trong C ++ là lối tắt thuận tiện cho nhóm nhà phát triển đơn lẻ, nhưng điều này gây ra tất cả các loại nhầm lẫn trong thời gian dài chỉ vì các cuộc gọi phương thức bị "ẩn" theo cách không dễ dàng để các công cụ như doxygen chọn ra và mọi người cần hiểu các thành ngữ để sử dụng chúng đúng cách.

Đôi khi, rất khó để có ý nghĩa hơn bạn mong đợi, thậm chí. Ngày xưa, trong một dự án C ++ đa nền tảng lớn, tôi đã quyết định nên bình thường hóa cách các đường dẫn được xây dựng bằng cách tạo một FilePathđối tượng (tương tự như Fileđối tượng Java ), có toán tử / được sử dụng để ghép nối một đối tượng khác phần đường dẫn trên đó (vì vậy bạn có thể làm một cái gì đó như thế File::getHomeDir()/"foo"/"bar"và nó sẽ làm điều đúng trên tất cả các nền tảng được hỗ trợ của chúng tôi). Tất cả những người nhìn thấy nó về cơ bản sẽ nói, "Cái quái gì vậy? Phân chia chuỗi? ... Ồ, thật dễ thương, nhưng tôi không tin nó sẽ làm điều đúng đắn."

Tương tự, có rất nhiều trường hợp trong lập trình đồ họa hoặc các lĩnh vực khác trong đó toán học vectơ / ma trận xảy ra rất nhiều trong đó thật hấp dẫn để làm những việc như Ma trận * Ma trận, Vector * Vector (chấm), Vector% Vector (chéo), Ma trận * Vector ( biến đổi ma trận), Ma trận ^ Vector (biến đổi ma trận trường hợp đặc biệt bỏ qua tọa độ đồng nhất - hữu ích cho các quy tắc bề mặt), v.v., nhưng trong khi nó tiết kiệm một chút thời gian phân tích cú pháp cho người viết thư viện toán học vectơ, nó chỉ kết thúc gây nhầm lẫn vấn đề nhiều hơn cho người khác. Nó không đáng


0

Quá tải toán tử là một ý tưởng tồi vì cùng một lý do rằng quá tải phương thức là một ý tưởng tồi: cùng một biểu tượng trên màn hình sẽ có ý nghĩa khác nhau tùy thuộc vào những gì xung quanh nó. Điều này làm cho nó khó khăn hơn cho việc đọc thông thường.

Vì khả năng đọc là một khía cạnh quan trọng của khả năng bảo trì, bạn nên luôn luôn tránh quá tải (trừ một số trường hợp rất đặc biệt). Sẽ tốt hơn nhiều cho mỗi ký hiệu (cho dù là toán tử hoặc mã định danh chữ số) có một ý nghĩa duy nhất đứng riêng.

Để minh họa: khi đọc mã không quen thuộc, nếu bạn gặp một mã định danh chữ và số mới mà bạn không biết, ít nhất bạn có lợi thế là bạn biết bạn không biết nó. Sau đó bạn có thể đi tìm nó. Tuy nhiên, nếu bạn thấy một mã định danh hoặc toán tử phổ biến mà bạn biết ý nghĩa của nó, thì bạn sẽ ít nhận thấy rằng nó thực sự đã bị quá tải để có một ý nghĩa hoàn toàn khác. Để biết những toán tử nào đã bị quá tải (trong một cơ sở mã đã sử dụng quá tải), bạn sẽ cần có kiến ​​thức làm việc về mã hoàn chỉnh, ngay cả khi bạn chỉ muốn đọc một phần nhỏ của nó. Điều này sẽ gây khó khăn cho việc đưa các nhà phát triển mới tăng tốc mã đó và không thể đưa mọi người vào một công việc nhỏ. Điều này có thể tốt cho bảo mật công việc lập trình viên, nhưng nếu bạn chịu trách nhiệm cho sự thành công của cơ sở mã, bạn nên tránh thực hành này bằng mọi giá.

Bởi vì các toán tử có kích thước nhỏ, các toán tử quá tải sẽ cho phép mã dày đặc hơn, nhưng làm cho mã dày đặc không phải là một lợi ích thực sự. Một dòng có hai lần logic mất gấp đôi thời gian để đọc. Trình biên dịch không quan tâm. Vấn đề duy nhất là khả năng đọc của con người. Vì làm cho mã nhỏ gọn không tăng cường khả năng đọc, không có lợi ích thực sự cho sự gọn nhẹ. Hãy tiếp tục và chiếm dung lượng và cung cấp cho các hoạt động duy nhất một mã định danh duy nhất và mã của bạn sẽ thành công hơn về lâu dài.


"cùng một biểu tượng trên màn hình sẽ có ý nghĩa khác nhau tùy thuộc vào những gì xung quanh nó" - đó là trường hợp đã có đối với nhiều nhà khai thác ở hầu hết các ngôn ngữ.
rkj

-1

Những khó khăn kỹ thuật để xử lý quyền ưu tiên và phân tích cú pháp phức tạp được đặt sang một bên, tôi nghĩ rằng có một vài khía cạnh của ngôn ngữ lập trình là gì phải được xem xét.

Các toán tử thường là các cấu trúc logic ngắn được xác định rõ và được ghi lại bằng ngôn ngữ cốt lõi (so sánh, gán ..). Họ cũng thường khó hiểu mà không cần tài liệu (so sánh a^bvới xor(a,b)ví dụ). Có một số lượng toán tử khá hạn chế thực sự có thể có ý nghĩa trong lập trình bình thường (>, <, =, + vv ..).

Ý tưởng của tôi là tốt hơn là nên bám vào một tập hợp các toán tử được xác định rõ bằng ngôn ngữ - sau đó cho phép toán tử nạp chồng các toán tử đó (đưa ra một khuyến nghị mềm rằng các toán tử nên làm điều tương tự, nhưng với kiểu dữ liệu tùy chỉnh).

Các trường hợp sử dụng của bạn ~|thực sự sẽ có thể với quá tải toán tử đơn giản (C #, C ++, v.v.). DSL là một khu vực sử dụng hợp lệ, nhưng có lẽ là một trong những khu vực hợp lệ duy nhất (theo quan điểm của tôi). Tuy nhiên, tôi nghĩ rằng có những công cụ tốt hơn để tạo ra các ngôn ngữ mới bên trong. Thực hiện một ngôn ngữ DSL thực sự trong một ngôn ngữ khác không phải là khó khăn bằng cách sử dụng bất kỳ công cụ biên dịch trình biên dịch nào. Tương tự với "đối số LUA mở rộng". Một ngôn ngữ rất có thể được xác định chủ yếu để giải quyết vấn đề theo một cách cụ thể, không phải là nền tảng cho các ngôn ngữ phụ (ngoại lệ tồn tại).


-1

Một yếu tố khác cho nó là không phải lúc nào cũng dễ dàng xác định một hoạt động với các toán tử có sẵn. Ý tôi là, vâng, đối với bất kỳ loại số nào, toán tử '*' có thể có ý nghĩa và thường được triển khai bằng ngôn ngữ hoặc trong các mô-đun hiện có. Nhưng trong trường hợp các lớp phức tạp điển hình mà bạn cần xác định (những thứ như ShipingAddress, WindowManager, ObjectDimensions, PlayerCharacter, v.v.) thì hành vi đó không rõ ràng ... Việc thêm hoặc bớt một số vào Địa chỉ nghĩa là gì? Nhân hai địa chỉ?

Chắc chắn, bạn có thể định nghĩa rằng việc thêm một chuỗi vào lớp ShippingAddress có nghĩa là một hoạt động tùy chỉnh như "thay thế dòng 1 trong địa chỉ" (thay vì chức năng "setLine1") và thêm một số là "thay thế mã zip" (thay vì "setZipCode") , nhưng sau đó mã không dễ đọc và khó hiểu. Chúng tôi thường nghĩ rằng toán tử được sử dụng trong các loại / lớp cơ bản, vì hành vi của chúng là trực quan, rõ ràng và nhất quán (ít nhất là khi bạn đã quen với ngôn ngữ này). Hãy suy nghĩ trong các loại như Integer, String, ComplexNumbers, v.v.

Vì vậy, ngay cả khi việc xác định toán tử có thể rất hữu ích trong một số trường hợp cụ thể, thì việc triển khai trong thế giới thực của chúng khá hạn chế, vì 99% các trường hợp đó sẽ là một chiến thắng rõ ràng đã được thực hiện trong gói ngôn ngữ cơ bản.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.