В функциональных языках мне не хватает функции, заключающейся в том, что операторы - это просто функции, поэтому добавить пользовательский оператор часто так же просто, как добавить функцию. Многие процедурные языки допускают перегрузки операторов, поэтому в некотором смысле операторы все еще являются функциями (это очень верно в D, где оператор передается в виде строки в параметре шаблона).
Кажется, что там, где допустима перегрузка операторов, часто бывает просто добавить дополнительные пользовательские операторы. Я нашел этот пост в блоге , в котором утверждается, что пользовательские операторы плохо работают с инфиксной нотацией из-за правил приоритета, но автор дает несколько решений этой проблемы.
Я огляделся и не смог найти процедурных языков, которые бы поддерживали пользовательские операторы на этом языке. Есть хаки (такие как макросы в C ++), но это не то же самое, что поддержка языка.
Поскольку эту функцию довольно просто реализовать, почему она не является более распространенной?
Я понимаю, что это может привести к некоторому уродливому коду, но это не мешало разработчикам языков в прошлом добавлять полезные функции, которыми можно легко злоупотреблять (макросы, троичные операторы, небезопасные указатели).
Фактические варианты использования:
- Реализовать пропущенные операторы (например, Lua не имеет побитовых операторов)
- Mimic D's
~
(конкатенация массивов) - DSL,
- Использовать
|
как синтаксический сахар в стиле конвейера Unix (используя сопрограммы / генераторы)
Я также заинтересован в языках , которые делают позволяют пользовательские операторы, но я больше заинтересован в почему она была исключена. Я думал о разветвлении языка сценариев для добавления пользовательских операторов, но остановился, когда понял, что нигде его не видел, так что, вероятно, есть веская причина, почему разработчики языка умнее меня не допустили этого.
источник
Ответы:
Существуют две диаметрально противоположные точки зрения на дизайн языков программирования. Во-первых, программисты пишут лучший код с меньшими ограничениями, а во-вторых, они пишут лучший код с большими ограничениями. На мой взгляд, реальность такова, что хорошие опытные программисты процветают с меньшим количеством ограничений, но эти ограничения могут улучшить качество кода для начинающих.
Пользовательские операторы могут создать очень элегантный код в опытных руках и совершенно ужасный код для новичка. Так что, включает ли ваш язык их или нет, зависит от школы мысли вашего языкового дизайнера.
источник
Format
метода) и когда он должен отказаться ( например, аргументы автобокса дляReferenceEquals
). Чем больше языка способностей дает программистам возможность сказать, когда определенные выводы будут неуместны, тем безопаснее он может предложить удобные выводы в случае необходимости.Если бы у меня был выбор между конкатенацией массивов с помощью ~ или «myArray.Concat (secondArray)», я бы предпочел последнее. Почему? Потому что ~ - это абсолютно бессмысленный символ, который имеет только значение - конкатенацию массива - заданный в конкретном проекте, в котором он был написан.
В принципе, как вы сказали, операторы ничем не отличаются от методов. Но в то время как методам могут быть даны понятные, понятные имена, которые улучшают понимание потока кода, операторы непрозрачны и ситуативны.
Вот почему мне также не нравится
.
оператор PHP (конкатенация строк) или большинство операторов в Haskell или OCaml, хотя в этом случае появляются некоторые общепринятые стандарты для функциональных языков.источник
+
и ,<<
конечно , не определены наObject
(я не получаю «не подходят для оператора + в ...» , когда делает что на голом классе в C ++).Ваша предпосылка неверна. Это не «довольно тривиально для реализации». На самом деле это приносит массу проблем.
Давайте посмотрим на предлагаемые «решения» в посте:
В целом, это дорогостоящая функция для реализации, как с точки зрения сложности синтаксического анализатора, так и с точки зрения производительности, и неясно, принесет ли она много преимуществ. Конечно, у определения новых операторов есть некоторые преимущества, но даже они спорные (просто посмотрите на другие ответы, утверждающие, что наличие новых операторов не очень хорошая вещь).
источник
Давайте пока проигнорируем весь аргумент «операторы будут злоупотреблять, чтобы нанести вред читабельности», и сосредоточимся на последствиях языкового дизайна.
Инфиксные операторы имеют больше проблем, чем простые правила приоритетов (хотя, если говорить прямо, ссылка, на которую вы ссылаетесь, упрощает воздействие этого проектного решения). Одним из них является разрешение конфликтов: что происходит, когда вы определяете
a.operator+(b)
иb.operator+(a)
? Предпочтение одного над другим приводит к нарушению ожидаемого коммутативного свойства этого оператора. Сгенерирование ошибки может привести к тому, что модули, которые в противном случае будут работать, будут сломаны вместе Что происходит, когда вы начинаете бросать производные типы в микс?В том-то и дело, что операторы - это не просто функции. Функции либо автономны, либо принадлежат их классу, что дает четкое предпочтение тому, какой параметр (если есть) владеет полиморфной диспетчеризацией.
И это игнорирует различные проблемы с упаковкой и разрешением, которые возникают у операторов. В языках причины дизайнеров (по большому счету) ограничить определение оператора инфикса потому, что он создает кучу проблем для языка, обеспечивая при этом спорное преимущество.
И, честно говоря, потому что они не тривиальны для реализации.
источник
+
- это зло. Но действительно ли это аргумент против пользовательских операторов? Это похоже на аргумент против перегрузки операторов в целом.boost::spirit
. Как только вы разрешите пользовательские операторы, ситуация ухудшится, так как нет хорошего способа даже четко определить приоритет для математики. Я сам немного об этом написал в контексте языка, специально предназначенного для решения проблем с произвольно заданными операторами.Я думаю , вы будете удивлены , как часто оператор перегрузки будут реализованы в той или иной форме. Но они обычно не используются во многих сообществах.
Зачем использовать ~ для объединения в массив? Почему бы не использовать << как в Ruby ? Потому что программисты, с которыми вы работаете, вероятно, не являются программистами Ruby. Или D программисты. Так что же они делают, когда сталкиваются с вашим кодом? Они должны пойти и посмотреть, что означает этот символ.
Раньше я работал с очень хорошим разработчиком C #, который также имел вкус к функциональным языкам. Внезапно он начал вводить монады в C # с помощью методов расширения и используя стандартную терминологию монад. Никто не может оспорить, что часть его кода стала более краткой и даже более читаемой, когда вы узнали, что это значит, но это означало, что каждый должен был изучить терминологию монады, прежде чем код обрел смысл .
Как вы думаете, достаточно справедливо? Это была только небольшая команда. Лично я не согласен. Каждому новому разработчику суждено было запутаться в этой терминологии. У нас не хватает проблем с изучением нового домена?
С другой стороны, я с удовольствием использовать в
??
оператор в C # , потому что я ожидаю других разработчиков C # знать , что это такое, но я бы не перегружать его на языке , который не поддерживает его по умолчанию.источник
??
примера.Я могу придумать несколько причин:
O(1)
. Но с перегрузкой оператораsomeobject[i]
может легко бытьO(n)
операция в зависимости от реализации оператора индексации.В действительности, очень мало случаев, когда перегрузка оператора имеет оправданное применение по сравнению с обычными функциями. Законным примером может быть разработка класса комплексных чисел для использования математиками, которые понимают понятные способы определения математических операторов для комплексных чисел. Но это действительно не очень распространенный случай.
Некоторые интересные случаи для рассмотрения:
+
это просто обычная функция. Вы можете определять функции по своему усмотрению (обычно есть способ определения их в отдельных пространствах имен, чтобы избежать конфликта со встроенными+
), включая операторы. Но существует культурная тенденция использовать значимые имена функций, поэтому этим не злоупотребляют. Кроме того, в префиксной нотации Lisp имеет тенденцию использоваться исключительно, поэтому в «синтаксическом сахаре», который обеспечивают перегрузки операторов, меньше значения.cout << "Hello World!"
кто-нибудь?), Но этот подход имеет смысл, учитывая позиционирование C ++ как сложного языка, который позволяет программировать на высоком уровне, в то же время позволяя вам очень близко подходить к производительности, поэтому вы можете, например, написать класс сложных чисел, который ведет себя именно так, как вы хотите, без ущерба для производительности. Понятно, что это ваша собственная ответственность, если вы стреляете себе в ногу.источник
Это не тривиально для реализации (если не тривиально реализовано). Это также не очень-то вас устраивает, даже если реализовано идеально: выигрыш в читаемости от краткости компенсируется потерями читаемости из-за незнакомости и непрозрачности. Короче говоря, это необычно, потому что это обычно не стоит времени разработчиков или пользователей.
Тем не менее, я могу думать о трех языках, которые делают это, и они делают это по-разному:
источник
Одна из главных причин, по которой пользовательским операторам не рекомендуется, заключается в том, что тогда любой оператор может означать / может делать что угодно
Например
cstream
, очень критикуется перегрузка влево.Когда язык допускает перегрузки операторов, обычно рекомендуется сохранять поведение оператора аналогичным базовому поведению, чтобы избежать путаницы.
Кроме того, определяемые пользователем операторы значительно затрудняют синтаксический анализ, особенно когда существуют также пользовательские правила предпочтений.
источник
+
добавить две вещи,-
вычитает их,*
умножает их. Я чувствую, что никто не заставляет программиста заставлять функцию / методadd
фактически добавлять что-либо иdoNothing
может запускать ядерное оружие. Иa.plus(b.minus(c.times(d)).times(e)
гораздо менее читабельноa + (b - c * d) * e
(добавленный бонус - в первом укусе ошибка в транскрипции). Я не вижу, как первый более значимым ...Мы не используем пользовательские операторы по той же причине, по которой мы не используем пользовательские слова. Никто бы не назвал их функцию "sworp". Единственный способ донести свою мысль до другого человека - это использовать общий язык. А это значит, что и общество, для которого вы пишете свой код, должны знать как слова, так и знаки (операторы).
Поэтому операторы, которые вы видите в использовании в языках программирования, - это те, кого мы учили в школе (арифметика), или те, которые были созданы в сообществе программистов, например, булевы операторы.
источник
elem
является отличной идеей, и, безусловно, оператор должен понимать каждый, но другие, похоже, не согласны.Что касается языков, которые поддерживают такую перегрузку: Scala делает, на самом деле гораздо чище и лучше, чем C ++. Большинство символов можно использовать в именах функций, поэтому вы можете определять операторы, как! + * = ++, если хотите. Есть встроенная поддержка инфикса (для всех функций, принимающих один аргумент). Я думаю, что вы также можете определить ассоциативность таких функций. Вы не можете, однако, определить приоритет (только с уродливыми уловками, см. Здесь ).
источник
Одна вещь, которая еще не упоминалась, - это случай Smalltalk, где все (включая операторов) является отправкой сообщения. «Операторы» , как
+
,|
и так далее, на самом деле одинарные методы.Все методы могут быть переопределены, поэтому
a + b
означает целочисленное сложение, еслиa
иb
оба являются целыми числами, и означает векторное сложение, если они обаOrderedCollection
s.Нет никаких правил приоритета, так как это всего лишь вызовы методов. Это имеет важное значение для стандартных математических обозначений:
3 + 4 * 5
значит(3 + 4) * 5
, нет3 + (4 * 5)
.(Это основной камень преткновения для новичков Smalltalk. Нарушение правил математики исключает особый случай, так что вся оценка кода происходит равномерно слева направо, что делает язык намного проще.)
источник
Вы боретесь против двух вещей здесь:
В большинстве языков операторы на самом деле не реализованы как простые функции. Они могут иметь некоторые функциональные леса, но компилятор / среда выполнения явно знают об их семантическом значении и о том, как эффективно их преобразовать в машинный код. Это гораздо более верно даже по сравнению со встроенными функциями (именно поэтому большинство реализаций также не включают в себя все накладные расходы вызова функций). Большинство операторов являются абстракциями более высокого уровня примитивных инструкций, найденных в процессорах (отчасти поэтому большинство операторов являются арифметическими, логическими или побитовыми). Вы могли бы смоделировать их как «специальные» функции (назовите их «примитивами» или «встроенными» или «нативными» или как угодно), но для этого, как правило, требуется очень надежный набор семантики для определения таких специальных функций. Альтернатива состоит в том, чтобы иметь встроенные операторы, которые семантически выглядят как пользовательские операторы, но в противном случае вызывают специальные пути в компиляторе. Это идет вразрез с ответом на второй вопрос ...
Помимо проблемы машинного перевода, о которой я упоминал выше, на синтаксическом уровне операторы на самом деле не отличаются от функций. Их отличительными характеристиками, как правило, является то, что они кратки и символичны, что намекает на существенную дополнительную характеристику, которая должна быть им полезна: они должны иметь широкое понимание значения / семантики для разработчиков. Короткие символы не имеют большого значения, если только они не являются сокращением для набора семантики, которая уже понятна. Это делает определяемые пользователем операторы по своей сути бесполезными, так как по самой своей природе они не так широко поняты. Они имеют столько же смысла, сколько одно- или двухбуквенные имена функций.
Перегрузки операторов в C ++ обеспечивают благоприятную почву для изучения этого. «Злоупотребление» перегрузкой операторов происходит в форме перегрузок, которые нарушают некоторые из семантических контрактов, которые широко понимаются (классический пример - перегрузка оператора + такая, что a + b! = B + a, или где + изменяет любой из его операнды).
Если вы посмотрите на Smalltalk, который допускает перегрузку операторов и определяемые пользователем операторы, вы можете увидеть, как язык может это делать и насколько он будет полезен. В Smalltalk операторы - это просто методы с различными синтаксическими свойствами (а именно, они закодированы как бинарный инфикс). Язык использует «примитивные методы» для специальных ускоренных операторов и методов. Вы обнаружите, что немногие, если создаются какие-либо определяемые пользователем операторы, и когда они есть, они, как правило, используют не столько, сколько автор, вероятно, намеревался их использовать. Даже эквивалент перегрузки оператора встречается редко, потому что определение новой функции как оператора, а не метода - это в основном чистый убыток, поскольку последний допускает выражение семантики функции.
источник
Я всегда считал, что перегрузки операторов в C ++ удобны для команды разработчиков, но в долгосрочной перспективе это приводит к всевозможным путаницам просто потому, что вызовы методов «скрыты» таким образом, что это нелегко чтобы такие инструменты, как doxygen, можно было разобрать, и люди должны понимать идиомы, чтобы правильно их использовать.
Иногда даже понять гораздо сложнее, чем вы ожидаете. Давным-давно, в большом кроссплатформенном проекте C ++ я решил, что было бы неплохо нормализовать способ построения путей, создав
FilePath
объект (аналогичныйFile
объекту Java ), в котором был бы оператор / использовался для объединения другого часть пути к нему (так что вы можете сделать что-то подобное,File::getHomeDir()/"foo"/"bar"
и это будет делать правильно на всех наших поддерживаемых платформах). Каждый, кто видел это, по сути говорил: «Какого черта? Струнное деление? ... О, это мило, но я не верю, что это правильно».Точно так же в графическом программировании или в других областях много случаев, когда математика векторов / матриц часто встречается, когда соблазнительно делать такие вещи, как матрица * матрица, вектор * вектор (точка), вектор% вектор (крест), матрица * вектор ( матричное преобразование), Matrix ^ Vector (матричное преобразование в особом случае, игнорирующее однородную координату - полезно для нормалей поверхности) и т. д., но, хотя это экономит немного времени на разбор для человека, который написал библиотеку векторной математики, он только заканчивается путать вопрос больше для других. Это просто не стоит того.
источник
Перегрузки операторов - плохая идея по той же причине, что перегрузки методов - плохая идея: один и тот же символ на экране будет иметь разные значения в зависимости от того, что вокруг него. Это усложняет случайное чтение.
Поскольку удобочитаемость является критическим аспектом ремонтопригодности, вам всегда следует избегать перегрузки (за исключением некоторых очень особых случаев). Гораздо лучше для каждого символа (будь то оператор или буквенно-цифровой идентификатор) иметь уникальное значение, которое стоит само по себе.
Чтобы проиллюстрировать: при чтении незнакомого кода, если вы встречаете новый идентификатор алфавита, который вы не знаете, по крайней мере, у вас есть преимущество, которое вы знаете, что вы его не знаете, Вы можете пойти посмотреть его. Однако, если вы видите общий идентификатор или оператор, значение которого вам известно, вы с гораздо меньшей вероятностью заметите, что он на самом деле был перегружен и имеет совершенно другое значение. Чтобы узнать, какие операторы были перегружены (в кодовой базе, которая широко использовала перегрузку), вам понадобятся практические знания о полном коде, даже если вы хотите прочитать только небольшую его часть. Это затруднило бы привлечение новых разработчиков к работе над этим кодом, и было бы невозможно привлечь людей для небольшой работы. Это может быть полезно для обеспечения безопасности работы программиста, но если вы несете ответственность за успех базы кода, вам следует избегать этой практики любой ценой.
Поскольку операторы имеют небольшой размер, операторы с перегрузкой допускают более плотный код, но создание плотного кода не является реальным преимуществом. Строка с двойной логикой занимает вдвое больше времени для чтения. Компилятору все равно. Единственная проблема - удобочитаемость. Поскольку компактность кода не повышает удобочитаемость, компактность не дает никаких преимуществ. Идите вперед и займите место, и дайте уникальным операциям уникальный идентификатор, и ваш код будет более успешным в долгосрочной перспективе.
источник
Технические трудности, связанные с обработкой приоритетов и сложным синтаксическим анализом, остались в стороне, я думаю, что есть несколько аспектов того, что представляет собой язык программирования, который необходимо учитывать.
Операторы, как правило, представляют собой короткие логические конструкции, которые хорошо определены и задокументированы на базовом языке (сравните, назначьте ..). Они также , как правило , трудно понять без документации (сравните
a^b
сxor(a,b)
, например). Существует довольно ограниченное количество операторов, которые могут иметь смысл в обычном программировании (>, <, =, + и т. Д.).Моя идея состоит в том, что лучше придерживаться набора четко определенных операторов в языке - затем разрешить перегрузку операторов этих операторов (учитывая мягкую рекомендацию, что операторы должны делать то же самое, но с пользовательским типом данных).
Ваши случаи использования
~
и|
были бы возможны при простой перегрузке операторов (C #, C ++ и т. Д.). DSL является допустимой областью использования, но, вероятно, одной из единственных допустимых областей (с моей точки зрения). Я, однако, думаю, что есть лучшие инструменты для создания новых языков внутри. Выполнение настоящего языка DSL на другом языке не так уж сложно, используя любой из этих инструментов компилятор-компилятор. То же самое относится и к «расширению аргумента LUA». Скорее всего, язык определен в первую очередь для решения проблем особым образом, а не как основание для подъязыков (существуют исключения).источник
Еще одним фактором является то, что не всегда просто определить операцию с доступными операторами. Я имею в виду, да, для любого вида чисел оператор '*' может иметь смысл и обычно реализуется на языке или в существующих модулях. Но в случае типичных сложных классов, которые вам нужно определить (такие как ShipingAddress, WindowManager, ObjectDimensions, PlayerCharacter и т. Д.), Такое поведение неясно ... Что означает добавить или вычесть число в адрес? Умножить два адреса?
Конечно, вы можете определить, что добавление строки в класс ShippingAddress означает пользовательскую операцию, такую как «заменить строку 1 в адресе» (вместо функции «setLine1») и добавление числа «заменить почтовый индекс» (вместо «setZipCode») , но тогда код не очень читабелен и запутан. Обычно мы думаем, что операторы используются в базовых типах / классах, поскольку их поведение интуитивно понятно, понятно и непротиворечиво (по крайней мере, если вы знакомы с языком). Думайте в таких типах, как Integer, String, ComplexNumbers и т. Д.
Таким образом, даже если определение операторов может быть очень полезным в некоторых конкретных случаях, их реальная реализация весьма ограничена, поскольку 99% случаев, когда это будет явным выигрышем, уже реализованы в базовом языковом пакете.
источник