Я читаю и слышу, что люди (также на этом сайте) регулярно хвалят парадигму функционального программирования, подчеркивая, как хорошо иметь все неизменное. Примечательно, что люди предлагают этот подход даже в традиционно обязательных ОО-языках, таких как C #, Java или C ++, а не только в чисто функциональных языках, таких как Haskell, которые навязывают это программисту.
Мне трудно это понять, потому что я нахожу изменчивость и побочные эффекты ... удобными. Тем не менее, учитывая то, как люди в настоящее время осуждают побочные эффекты и считают хорошей практикой избавляться от них везде, где это возможно, я считаю, что если я хочу быть компетентным программистом, я должен начать свой путь к лучшему пониманию парадигмы ... Отсюда мой вопрос.
Одно место, где я нахожу проблемы с функциональной парадигмой, - это когда на объект естественным образом ссылаются из нескольких мест. Позвольте мне описать это на двух примерах.
Первым примером будет моя игра на C #, которую я пытаюсь сделать в свободное время . Это пошаговая веб-игра, в которой оба игрока имеют команды по 4 монстра и могут отправить монстра из своей команды на поле битвы, где он столкнется с монстром, посланным противником. Игроки могут также вызывать монстров с поля битвы и заменять их другим монстром из своей команды (аналогично покемонам).
В этой настройке на одного монстра можно естественным образом ссылаться как минимум из двух мест: команды игрока и поля битвы, которое ссылается на двух «активных» монстров.
Теперь давайте рассмотрим ситуацию, когда один монстр получает удар и теряет 20 очков здоровья. В рамках императивной парадигмы я health
изменяю поле этого монстра, чтобы отразить это изменение - и это то, что я делаю сейчас. Однако это делает Monster
класс изменчивым и связанные с ним функции (методы) нечистыми, что, я думаю, считается плохой практикой на данный момент.
Несмотря на то, что я дал себе разрешение перевести код этой игры в состояние, меньшее идеального, чтобы иметь какие-либо надежды на его окончательное завершение в будущем, я бы хотел знать и понимать, каким он должен быть. написано правильно. Поэтому: если это недостаток дизайна, как это исправить?
В функциональном стиле, насколько я понимаю, я бы вместо этого сделал копию этого Monster
объекта, оставив его идентичным старому, за исключением одного этого поля; и метод suffer_hit
будет возвращать этот новый объект вместо того, чтобы модифицировать старый на месте. Затем я также скопировал бы Battlefield
объект, оставив все его поля одинаковыми, за исключением этого монстра.
Это связано как минимум с 2 трудностями:
- Иерархия может быть намного глубже, чем этот упрощенный пример просто
Battlefield
->Monster
. Мне пришлось бы делать такое копирование всех полей, кроме одного, и возвращать новый объект по всей этой иерархии. Это был бы стандартный код, который я нахожу раздражающим, особенно потому, что функциональное программирование должно уменьшать шаблон. - Однако гораздо более серьезная проблема заключается в том, что это приведет к несинхронизации данных . Активное чудовище поля увидит, что его здоровье уменьшено; однако, этот же монстр, на который ссылается его контролирующий игрок
Team
, не будет. Если бы я вместо этого принял императивный стиль, каждая модификация данных была бы немедленно видна из всех других мест кода, и в таких случаях, как этот, я нахожу это действительно удобным - но способ, которым я получаю вещи, это именно то, что говорят люди неправильно с императивным стилем!- Теперь можно было бы решить эту проблему, отправляясь в путь
Team
после каждой атаки. Это дополнительная работа. Однако что, если на монстра можно будет внезапно ссылаться из еще большего количества мест? Что если я приду со способностью, которая, например, позволяет монстру сфокусироваться на другом монстре, который не обязательно находится на поле (на самом деле я рассматриваю такую способность)? Не забуду ли я обязательно совершать путешествие к сфокусированным монстрам сразу после каждой атаки? Кажется, это бомба замедленного действия, которая взорвется, когда код станет более сложным, поэтому я думаю, что это не решение проблемы.
- Теперь можно было бы решить эту проблему, отправляясь в путь
Идея лучшего решения возникла из моего второго примера, когда я столкнулся с той же проблемой. В академии нам сказали написать переводчик языка нашего собственного дизайна на Хаскеле. (Это также, как я был вынужден начать понимать, что такое FP). Проблема обнаружилась, когда я реализовывал замыкания. Еще раз одна и та же область теперь может быть использована из нескольких мест: через переменную, которая содержит эту область и как родительскую область для любых вложенных областей! Очевидно, что если в эту область внесено изменение с помощью любой из ссылок, указывающих на нее, это изменение также должно быть видно через все остальные ссылки.
Решение, с которым я пришел, состояло в том, чтобы назначить каждой области идентификатор и держать центральный словарь всех областей в State
монаде. Теперь переменные будут содержать только идентификатор области, к которой они были привязаны, а не саму область действия, а вложенные области также будут содержать идентификатор своей родительской области.
Я предполагаю, что такой же подход мог бы быть применен в моей битве с монстрами ... Поля и команды не ссылаются на монстров; вместо этого они содержат идентификаторы монстров, которые сохраняются в центральном словаре монстров.
Тем не менее, я снова вижу проблему с этим подходом, которая мешает мне принять его без колебаний в качестве решения проблемы:
Это еще раз является источником стандартного кода. Это делает однострочники обязательно 3-строчными: то, что раньше было однострочной модификацией отдельного поля на месте, теперь требует (а) извлечения объекта из центрального словаря (б) внесения изменений (в) сохранения нового объекта в центральный словарь. Кроме того, хранение идентификаторов объектов и центральных словарей вместо ссылок увеличивает сложность. Так как FP рекламируется для уменьшения сложности и стандартного кода, это указывает на то, что я делаю это неправильно.
Я также собирался написать о второй проблеме, которая кажется гораздо более серьезной: этот подход вызывает утечки памяти . Недоступные объекты обычно будут собираться мусором. Однако объекты, хранящиеся в центральном словаре, не могут быть собраны сборщиком мусора, даже если ни один достижимый объект не ссылается на этот конкретный идентификатор. И хотя теоретически тщательное программирование может избежать утечек памяти (мы могли бы позаботиться о том, чтобы вручную удалить каждый объект из центрального словаря, когда он больше не нужен), это может привести к ошибкам, и FP объявляется для повышения правильности программ, поэтому еще раз это может не будет правильным способом.
Однако со временем я обнаружил, что это, скорее, решенная проблема. Java предоставляет, WeakHashMap
что может быть использовано для решения этой проблемы. C # предоставляет аналогичное средство - ConditionalWeakTable
хотя, согласно документам, оно предназначено для использования компиляторами. А в Хаскеле у нас есть System.Mem.Weak .
Хранение таких словарей - правильное функциональное решение этой проблемы или есть более простой, который я не вижу? Я предполагаю, что количество таких словарей может легко и плохо расти; так что если предполагается, что эти словари также являются неизменяемыми, это может означать много передачи параметров или, в языках, которые поддерживают это, монадические вычисления, поскольку словари будут храниться в монадах (но еще раз я читаю это в чисто функциональном языки как можно меньшего количества кода должны быть монадическими, в то время как это словарное решение поместит почти весь код в State
монаду, что еще раз заставляет меня сомневаться в правильности этого решения.)
Подумав немного, я бы добавил еще один вопрос: что мы получаем, создавая такие словари? То, что не так с императивным программированием, по мнению многих экспертов, заключается в том, что изменения в некоторых объектах распространяются на другие части кода. Для решения этой проблемы объекты должны быть неизменными - именно по этой причине, если я правильно понимаю, что внесенные в них изменения не должны быть видны в другом месте. Но теперь я обеспокоен другими частями кода, работающими с устаревшими данными, поэтому я придумываю центральные словари, чтобы ... снова изменения в некоторых частях кода распространялись на другие части кода! Разве мы не возвращаемся к императивному стилю со всеми его предполагаемыми недостатками, но с добавленной сложностью?
источник
Team
) могут получить исход битвы и, таким образом, состояния монстров с помощью кортежа (номер битвы, идентификатор объекта монстра).Ответы:
Как функциональное программирование обрабатывает объект, на который ссылаются из нескольких мест? Он приглашает вас вернуться к вашей модели!
Чтобы объяснить ... давайте посмотрим, как иногда пишутся сетевые игры - с центральной «золотой исходной» копией состояния игры и набором входящих клиентских событий, которые обновляют это состояние и затем передают широковещательную рассылку другим клиентам. ,
Вы можете прочитать о том, что команда Factorio получила удовольствие от того, что в некоторых ситуациях она ведет себя хорошо; вот краткий обзор их модели:
Ключевым моментом является то, что состояние каждого объекта является неизменным в определенный момент времени . Все в глобальном многопользовательском состоянии должно в конечном итоге сходиться к детерминированной реальности.
И - это может быть ключом к вашему вопросу. Состояние каждой сущности является неизменным для данного тика, и вы отслеживаете события перехода, которые со временем создают новые экземпляры.
Если подумать, очередь входящих событий с сервера должна иметь доступ к центральному каталогу сущностей, чтобы он мог применять свои события.
В конце концов, ваши простые однострочные методы-мутаторы, которые вы не хотите усложнять, просты только потому, что вы не совсем точно моделируете время. В конце концов, если здоровье может измениться в середине цикла обработки, тогда более ранние объекты в этом тике будут видеть старое значение, а более поздние видят измененное значение. Тщательно управлять этим означает по меньшей мере различающие текущее (неизменяемое) и следующее (находящееся в стадии разработки) состояния, которые на самом деле представляют собой всего два тика на большой временной шкале тиков!
Итак, в качестве общего руководства рассмотрите возможность разбить состояние монстра на множество мелких объектов, которые относятся, скажем, к локации / скорости / физике, здоровью / урону, активам. Создайте событие, чтобы описать каждую возможную мутацию, и запустите ваш основной цикл следующим образом:
Или что-то типа того. Я нахожу мысль: «Как бы я сделал это распределенным?» Это довольно хорошее умственное упражнение, как правило, для уточнения моего понимания, когда я не понимаю, где живут вещи и как они должны развиваться.
Благодаря заметке @ AaronM.Eshbach, в которой подчеркивается, что эта область проблем аналогична проблемному источнику событий и шаблону CQRS , где вы моделируете изменения состояния в распределенной системе как последовательность неизменяемых событий с течением времени . В этом случае мы, скорее всего, пытаемся очистить сложное приложение базы данных, отделив (как следует из названия!) Обработку команды мутатора от системы запросов / представлений. Более сложный, конечно, но более гибкий.
источник
Вы все еще наполовину в императивном лагере. Вместо того, чтобы думать об одном объекте за раз, подумайте о своей игре с точки зрения истории игр или событий.
так далее
Вы можете вычислить состояние игры в любой заданной точке, объединяя действия в цепочку, чтобы создать неизменный объект состояния. Каждая игра - это функция, которая принимает объект состояния и возвращает новый объект состояния.
источник