Чистый функционал против скажите, не спрашивайте?


14

«Идеальное число аргументов для функции - ноль» - совершенно неверно. Идеальное количество аргументов - это именно то количество, которое необходимо, чтобы ваша функция не имела побочных эффектов. Меньше этого, и вы без необходимости заставляете свои функции быть нечистыми, вынуждая вас уклоняться от пропасти успеха и подниматься на градиент боли. Иногда «дядя Боб» замечателен своим советом. Иногда он совершенно неправ. Его нулевой аргумент совет является примером последнего

( Источник: комментарий @David Arno под другим вопросом на этом сайте )

Комментарий набрал впечатляющее количество голосов - 133, поэтому я хотел бы обратить более пристальное внимание на его достоинства.

Насколько я знаю, в программировании есть два разных способа: чисто функциональное программирование (что обнадеживает этот комментарий) и говорить, не спрашивать (что время от времени рекомендуется и на этом сайте). AFAIK, эти два принципа в корне несовместимы, близки к тому, чтобы быть противоположностями друг другу: чистый функционал может быть обобщен как «только возвращаемые значения, не имеют побочных эффектов», в то время как скажите, не спрашивайте, можно суммировать как «ничего не возвращать, есть только побочные эффекты ". Кроме того, я немного озадачен, потому что я думал, что слово «не спрашивать» считалось ядром парадигмы ОО, тогда как чистые функции считались ядром функциональной парадигмы - теперь я вижу чистые функции, рекомендуемые в ОО!

Я полагаю, разработчики должны выбрать одну из этих парадигм и придерживаться ее? Ну, я должен признать, что никогда не мог заставить себя следовать. Часто мне кажется удобным вернуть значение, и я не могу понять, как мне добиться того, чего я хочу достичь, только с побочными эффектами. Часто мне кажется удобным иметь побочные эффекты, и я не могу понять, как мне добиться того, чего я хочу достичь, только возвращая значения. Кроме того, часто (я думаю, это ужасно) у меня есть методы, которые делают оба.

Тем не менее, из этих 133 возражений я рассуждаю о том, что в настоящее время чисто функциональное программирование "выигрывает", поскольку становится консенсусом, который лучше сказать, не спрашивайте. Это верно?

Поэтому на примере этой игры с антипаттернами я пытаюсь сделать так : если бы я хотел привести ее в соответствие с чисто функциональной парадигмой - КАК ?!

Мне кажется разумным иметь состояние битвы. Поскольку это пошаговая игра, я сохраняю боевые состояния в словаре (многопользовательский - многие игроки могут играть одновременно много сражений). Всякий раз, когда игрок делает свой ход, я вызываю соответствующий метод в состоянии битвы, который (а) соответствующим образом изменяет состояние и (б) возвращает обновления игрокам, которые сериализуются в JSON, и в основном просто рассказывают им, что только что произошло на доска. Это, я полагаю, является вопиющим нарушением ОБА принципов и в то же время.

ОК - я мог бы сделать метод ВОЗВРАТИТЬ состояние битвы вместо того, чтобы изменить его на месте, если бы я действительно хотел. Но! Придется ли мне тогда копировать все в состоянии битвы без необходимости, просто чтобы вернуть полностью новое состояние вместо того, чтобы изменить его на месте?

Теперь, возможно, если этот шаг является атакой, я мог бы просто вернуть персонажей, обновленных HP? Проблема в том, что все не так просто: правила игры, ход может и часто будет иметь гораздо больше эффектов, чем просто удаление части HP игрока. Например, это может увеличить расстояние между персонажами, применить спецэффекты и т. Д. И т. Д.

Мне кажется намного проще просто изменить состояние и вернуть обновления ...

Но как опытный инженер справится с этим?


9
Следование любой парадигме - верный путь к провалу. Политика никогда не должна превосходить интеллект. Решение проблемы должно зависеть от проблемы, а не от ваших религиозных убеждений относительно решения проблемы.
Джон Дума

1
У меня никогда не было вопроса о том, что я говорил раньше. Я удостоен чести :)
Дэвид Арно

Ответы:


14

Как и большинство программных афоризмов, «говори, не спрашивай» жертвует ясностью ради краткости. Он вовсе не предназначен для того, чтобы рекомендовать не запрашивать результаты вычислений, он рекомендует не запрашивать входные данные для вычислений. «Не получай, потом вычисляй, потом устанавливай, но это нормально, чтобы вернуть значение из расчета», не так уж и сложно.

Раньше люди часто вызывали геттер, делали какие-то вычисления, а затем вызывали сеттер с результатом. Это явный признак того, что ваши расчеты действительно принадлежат к классу, в котором вы назвали метод получения. «Скажите, не спрашивайте» было придумано, чтобы напомнить людям, что нужно искать этот анти-паттерн, и это сработало настолько хорошо, что теперь некоторые люди думают, что эта часть очевидна, и они ищут другие виды «просит», чтобы Устранить. Однако афоризм полезен только для этой ситуации.

Чистые функциональные программы никогда не страдали от этого точного анти-паттерна, по той простой причине, что в этом стиле нет сеттеров. Однако более общая (и более трудная для понимания) проблема не смешивания различных уровней семантической абстракции в одной и той же функции применима к каждой парадигме.


Спасибо за правильное объяснение "Скажи, не спрашивай".
user949300

13

У дяди Боба и Дэвида Арно (автора вашей цитаты) есть важные уроки, которые мы можем извлечь из того, что они написали. Я думаю, что стоит выучить урок и затем экстраполировать, что это действительно означает для вас и вашего проекта.

Первый: урок дяди Боба

Дядя Боб подчеркивает, что чем больше аргументов в вашей функции / методе, тем больше разработчиков, которые ее используют, должны понимать. Эта когнитивная нагрузка приходит не бесплатно, и если вы не согласны с порядком аргументов и т. Д., Когнитивная нагрузка только увеличивается.

Это факт существования человека. Я думаю, что ключевой ошибкой в ​​книге «Чистый код» дяди Боба является утверждение «Идеальное число аргументов для функции равно нулю» . Минимализм великолепен, пока его нет. Точно так же, как вы никогда не достигнете своих пределов в Calculus, вы никогда не достигнете «идеального» кода - как и вы.

Как сказал Альберт Эйнштейн: «Все должно быть как можно проще, но не проще».

Второе: урок Дэвида Арно

Описанный способ разработки Дэвида Арно - более функциональный стиль разработки, чем объектно-ориентированный . Однако функциональный код масштабируется намного лучше, чем традиционное объектно-ориентированное программирование. Почему? Из-за блокировки. В любое время, когда состояние объекта изменчиво, вы рискуете получить состояние гонки или заблокировать конфликт.

Написав высококонкурентные системы, используемые в симуляциях и других серверных приложениях, функциональная модель творит чудеса. Я могу засвидетельствовать улучшения, сделанные в подходе. Тем не менее, это совершенно другой стиль разработки, с другими требованиями и идиомами.

Развитие - это серия компромиссов

Вы знаете свое заявление лучше, чем любой из нас. Вам может не понадобиться масштабируемость, которая идет с функциональным стилем программирования. Между двумя идеалами, перечисленными выше, существует мир. Те из нас, кто имеет дело с системами, требующими высокой пропускной способности и смешного параллелизма, будут стремиться к идеалу функционального программирования.

Тем не менее, вы можете использовать объекты данных для хранения набора информации, которую необходимо передать в метод. Это помогает справиться с проблемой когнитивной нагрузки, к которой обращался дядя Боб, и в то же время поддерживать функциональный идеал, к которому обращался Дэвид Арно.

Я работал как с настольными системами с ограниченным параллелизмом, так и с программным обеспечением для моделирования с высокой пропускной способностью. У них очень разные потребности. Я могу оценить хорошо написанный объектно-ориентированный код, который разработан на основе концепции скрытия данных, с которыми вы знакомы. Это работает для нескольких приложений. Тем не менее, это не работает для всех из них.

Кто прав? Ну, в этом случае Дэвид более прав, чем дядя Боб. Тем не менее, основной момент, который я хочу подчеркнуть, заключается в том, что метод должен иметь столько аргументов, сколько имеет смысл.


Существует паралелизм. Различные битвы могут обрабатываться параллельно. Однако, да: отдельная битва, пока она обрабатывается, должна быть заблокирована.
Гаазкам

Да, я имел в виду, что читатели (жнецы по вашей аналогии) почерпнут из своих (сеятельских) сочинений. Тем не менее, я вернулся, чтобы посмотреть на некоторые вещи, которые я написал в прошлом, и либо переучил что-то, либо не согласился с моим прежним я. Мы все учимся и развиваемся, и это причина номер один, почему вы всегда должны рассуждать о том, как и если вы применяете то, чему научились.
Берин Лорич

8

ОК - я мог бы сделать метод ВОЗВРАТИТЬ состояние битвы вместо того, чтобы изменить его на месте, если бы я действительно хотел.

Да, это идея.

Придется ли мне тогда копировать все в состоянии битвы, чтобы вернуть полностью новое состояние вместо того, чтобы изменить его на месте?

Нет. Ваше «состояние битвы» можно смоделировать как неизменную структуру данных, которая содержит другие неизменные структуры данных в качестве строительных блоков, возможно, вложенных в некоторые иерархии неизменных структур данных.

Таким образом, могут быть части состояния битвы, которые не должны быть изменены в течение одного хода, и другие, которые должны быть изменены. Части, которые не меняются, копировать не нужно, так как они являются неизменяемыми, нужно всего лишь скопировать ссылку на эти части без риска возникновения побочных эффектов. Это лучше всего работает в языковых средах со сборщиком мусора.

Google для «эффективных структур неизменяемых данных», и вы обязательно найдете некоторые ссылки, как это работает в целом.

Мне кажется намного проще просто изменить состояние и вернуть обновления.

Для определенных проблем это может быть действительно проще. Игры и раундовые симуляции могут подпадать под эту категорию, учитывая, что большая часть игрового состояния изменяется от одного раунда к другому. Однако восприятие того, что на самом деле «проще», в некоторой степени субъективно, и оно также во многом зависит от того, к чему привыкли люди.


8

Как автор комментария, я думаю, мне следует пояснить это здесь, поскольку, конечно же, в этом есть нечто большее, чем упрощенная версия, которую предлагает мой комментарий.

AFAIK, эти два принципа в корне несовместимы, близки к тому, чтобы быть противоположностями друг другу: чистый функционал может быть обобщен как «только возвращаемые значения, не имеют побочных эффектов», в то время как сказать, не спрашивать можно суммировать как «ничего не возвращать, есть только побочные эффекты ".

Честно говоря, я нахожу это действительно странным использованием термина «говори, не спрашивай». Поэтому я прочитал, что сказал по этому поводу Мартин Фаулер несколько лет назад, и это было поучительно . Причина, по которой я нахожу это странным, заключается в том, что «скажи, не спрашивай» является синонимом внедрения зависимости в мою голову, а самая чистая форма внедрения зависимости передает все, что требуется функции через ее параметры.

Но похоже, что значение, которое я применяю к «говорите, не спрашивайте», происходит от того, что вы взяли ОО-ориентированное определение Фаулера и сделали его более независимым от парадигмы. В процессе, я полагаю, это приводит концепцию к логическим выводам.

Вернемся к простому начинанию. У нас есть «кусочки логики» (процедуры) и у нас есть глобальные данные. Процедуры считывают эти данные напрямую, чтобы получить к ним доступ. У нас есть простой «спросить» сценарий.

Ветер вперед немного. Теперь у нас есть объекты и методы. Эти данные больше не должны быть глобальными, их можно передавать через конструктор и содержать в объекте. И тогда у нас есть методы, которые воздействуют на эти данные. Так что теперь у нас есть «говори, не спрашивай», как описывает это Фаулер. Объекту сообщают свои данные. Этим методам больше не нужно запрашивать глобальную область своих данных. Но вот в чем суть: на мой взгляд, это все еще не так: «говори, не спрашивай», поскольку эти методы все еще должны запрашивать область объекта. Это скорее сценарий «скажи, потом спроси», который я чувствую.

Итак, вернемся к современности, избавимся от подхода «все кончено» и позаимствовали некоторые принципы из функционального программирования. Теперь, когда метод вызывается, все данные передаются ему через его параметры. Можно (и уже) утверждать: «Какой смысл, это просто усложняет код?» И да, передача через параметры данных, которые доступны через область объекта, усложняет код. Но хранение этих данных в объекте, а не его глобальная доступность также усложняет задачу. И все же мало кто будет утверждать, что глобальные переменные всегда лучше, потому что они проще. Дело в том, что преимущества, которые «говорят, а не спрашивают», перевешивают сложность сокращения масштабов. Это в большей степени относится к передаче через параметры, чем к ограничению области видимости объекта.private staticи передавайте все, что ему нужно, через параметры, и теперь этому методу можно доверять, чтобы он не крадучий получил доступ к вещам, к которым он не должен. Кроме того, это поощряет держать метод маленьким, иначе список параметров выходит из-под контроля. И это поощряет методы написания, которые соответствуют критериям «чистой функции».

Поэтому я не вижу «чистый функционал» и «говори, не спрашивай» как противоположность друг другу. Первое, насколько я понимаю, является единственной полной реализацией последнего. Подход Фаулера не является полным «скажи, не спрашивай».

Но важно помнить, что эта «полная реализация« говорите, не спрашивайте »действительно является идеалом, то есть прагматизм должен проявиться меньше, если мы станем идеалистами и, таким образом, ошибочно относимся к нему как к единственно возможному правильному подходу. Очень немногие приложения могут быть даже близки к тому, чтобы быть на 100% свободными от побочных эффектов по той простой причине, что они не сделали бы ничего полезного, если бы они были действительно свободны от побочных эффектов. Нам нужно изменить состояние, нам нужны IO и т. Д., Чтобы приложение было полезным. И в таких случаях методы должны вызывать побочные эффекты и поэтому не могут быть чистыми. Но эмпирическое правило здесь сводит эти «нечистые» методы к минимуму; только у них есть побочные эффекты, потому что они должны, а не как норма.

Мне кажется разумным иметь состояние битвы. Поскольку это пошаговая игра, я сохраняю боевые состояния в словаре (многопользовательский - многие игроки могут играть одновременно много сражений). Всякий раз, когда игрок делает свой ход, я вызываю соответствующий метод в состоянии битвы, который (а) соответствующим образом изменяет состояние и (б) возвращает обновления игрокам, которые сериализуются в JSON, и в основном просто рассказывают им, что только что произошло на доска.

Мне кажется более чем разумным иметь состояние битвы для меня; это кажется необходимым. Цель такого кода - обрабатывать запросы на изменение состояния, управлять этими изменениями состояния и сообщать о них. Вы могли бы обрабатывать это состояние глобально, вы могли бы держать его внутри отдельных объектов игрока, или вы могли бы передавать его через набор чистых функций. Какой из них вы выберете, зависит от того, что лучше всего подходит для вашего конкретного сценария. Глобальное состояние упрощает разработку кода и является быстрым, что является ключевым требованием большинства игр. Но это усложняет поддержку, тестирование и отладку кода. Набор чистых функций сделает код более сложным для реализации и рискует стать слишком медленным из-за чрезмерного копирования данных. Но это будет проще всего проверить и поддерживать. «ОО подход» находится на полпути между ними.

Ключ: нет единого идеального решения, которое работает постоянно. Цель чистых функций - помочь вам «попасть в пропасть успеха». Но если эта яма настолько мелкая, из-за сложности, которую она может принести в код, что вы не столько впадаете в нее, сколько перепрыгиваете через нее, тогда это не правильный подход для вас. Стремитесь к идеалу, но будьте прагматичны и остановитесь, когда этот идеал не подходит для этого времени.

И в заключение, еще раз повторю: чистые функции и «говори, не спрашивай» вовсе не являются противоположностями.


5

Для всего, что когда-либо говорилось, существует контекст, в который вы можете поместить это утверждение, которое сделает его абсурдом.

введите описание изображения здесь

Дядя Боб совершенно неправ, если вы берете совет с нулевым аргументом как требование. Он совершенно прав, если принять это к сведению, что каждый дополнительный аргумент затрудняет чтение кода. Это приходит за плату. Вы не добавляете аргументы к функциям, потому что это облегчает их чтение. Вы добавляете аргументы к функциям, потому что не можете придумать хорошее имя, которое делает зависимость от этого аргумента очевидной.

Например, pi()это очень хорошая функция, как она есть. Почему? Потому что мне все равно, как, или даже если, это было рассчитано. Или, если он использовал e или sin (), чтобы получить число, которое он возвращает. Я в порядке с этим, потому что имя говорит мне все, что мне нужно знать.

Однако не каждое имя говорит мне все, что мне нужно знать. Некоторые имена не раскрывают важную для понимания информацию, которая управляет поведением функции, а также открытые аргументы. Это именно то, что облегчает рассуждение о функциональном стиле программирования.

Я могу оставить вещи неизменными и побочные эффекты свободными в совершенно ООП стиле. Return - это просто механика, используемая для того, чтобы оставить значения в стеке для следующей процедуры. Вы можете оставаться неизменными, используя выходные порты для передачи значений другим неизменным вещам, пока не дойдете до последнего выходного порта, который, наконец, должен что-то изменить, если вы хотите, чтобы люди могли его прочитать. Это верно для каждого языка, функционального или нет.

Поэтому, пожалуйста, не утверждайте, что функциональное программирование и объектно-ориентированное программирование "принципиально несовместимы". Я могу использовать объекты в своих функциональных программах, и я могу использовать чистые функции в моих OO-программах.

Однако их смешивание сопряжено с определенными издержками: ожидания. Вы можете добросовестно следовать механике обеих парадигм и все же вызывать путаницу. Одним из преимуществ использования функционального языка является то, что побочные эффекты, хотя они и должны существовать для получения какого-либо результата, находятся в предсказуемом месте. Если, конечно, к изменчивому объекту обращаются недисциплинированным образом. Тогда то, что вы восприняли как данность на этом языке, распадается.

Точно так же вы можете поддерживать объекты с чистыми функциями, вы можете создавать неизменяемые объекты. Проблема в том, что если вы не сообщаете о том, что функции чистые или объекты неизменяемы, то люди не получают никаких разумных выгод от этих функций, пока не потратят много времени на чтение кода.

Это не новая проблема. В течение многих лет люди процедурно кодировали на «ОО-языках», думая, что они делают ОО, потому что они используют «ОО-язык». Немногие языки настолько хороши, чтобы удержать вас от выстрела в ногу. Чтобы эти идеи работали, они должны жить в вас.

Оба обеспечивают хорошие функции. Вы можете сделать оба. Если вы достаточно смелы, чтобы смешать их, пожалуйста, четко обозначьте их.


0

Я изо всех сил пытаюсь понять все правила различных парадигм. Время от времени они расходятся друг с другом, как они в этой ситуации.

ООП - это обязательная парадигма бегства с ножницами в мире, где происходят опасные вещи.

FP - это функциональная парадигма, в которой человек находит абсолютную безопасность в чистых вычислениях. Здесь ничего не происходит.

Тем не менее, все программы должны быть связаны с императивным миром, чтобы быть полезными. Итак, функциональное ядро, императивная оболочка .

Все становится запутанным, когда вы начинаете определять неизменяемые объекты (те, чьи команды возвращают измененную копию, а не видоизменяют). Вы говорите себе: «Это ООП» и «Я определяю поведение объекта». Вы вспоминаете проверенный принцип «говори, не спрашивай». Проблема в том, что вы применяете это в неправильном мире.

Области совершенно разные и подчиняются другим правилам. Функциональная сфера развивается до такой степени, что она хочет выпустить побочные эффекты в мир. Чтобы эти эффекты были освобождены, все данные, которые были бы инкапсулированы в императивном объекте (если бы это было написано таким образом!), Должны быть в распоряжении императивной оболочки. Без доступа к этим данным, которые в ином мире были бы спрятаны через инкапсуляцию, он не может сделать эту работу. Это вычислительно невозможно.

Таким образом, когда вы пишете неизменные объекты (то, что Clojure называет постоянными структурами данных), помните, что вы находитесь в функциональной области. Бросай Скажи, не проси в окно, и впусти его обратно в дом, только когда ты снова войдешь в императивное царство.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.