«Все это карта», я делаю это правильно?

69

Я смотрел выступление Стюарта Сьерры « Мышление в данных » и использовал одну из идей в качестве принципа дизайна в этой игре, которую я делаю. Разница в том, что он работает в Clojure, а я работаю в JavaScript. Я вижу некоторые основные различия между нашими языками в этом:

  • Clojure - идиоматически функциональное программирование
  • Большая часть государства неизменна

Я взял идею со слайда «Все это карта» (от 11 минут 6 секунд до> 29 минут). Некоторые вещи, которые он говорит:

  1. Всякий раз, когда вы видите функцию, которая принимает 2-3 аргумента, вы можете сделать так, чтобы превратить ее в карту и просто передать карту. У этого есть много преимуществ:
    1. Вам не нужно беспокоиться о порядке аргументов
    2. Вам не нужно беспокоиться о какой-либо дополнительной информации. Если есть дополнительные ключи, это не наша забота. Они просто текут, они не мешают.
    3. Вам не нужно определять схему
  2. В отличие от передачи в объекте нет скрытия данных. Но он утверждает, что сокрытие данных может вызвать проблемы и его переоценивают:
    1. Представление
    2. Простота реализации
    3. Как только вы обмениваетесь данными по сети или между процессами, вы все равно должны согласиться с представлением данных. Это дополнительная работа, которую можно пропустить, если вы просто работаете с данными.
  3. Наиболее актуален мой вопрос. Это 29 минут: «Сделайте ваши функции компонуемыми». Вот пример кода, который он использует для объяснения концепции:

    ;; Bad
    (defn complex-process []
      (let [a (get-component @global-state)
            b (subprocess-one a) 
            c (subprocess-two a b)
            d (subprocess-three a b c)]
        (reset! global-state d)))
    
    ;; Good
    (defn complex-process [state]
      (-> state
        subprocess-one
        subprocess-two
        subprocess-three))
    

    Я понимаю, что большинство программистов не знакомы с Clojure, поэтому я напишу это в императивном стиле:

    ;; Good
    def complex-process(State state)
      state = subprocess-one(state)
      state = subprocess-two(state)
      state = subprocess-three(state)
      return state
    

    Вот преимущества:

    1. Легко проверить
    2. Легко смотреть на эти функции в отдельности
    3. Легко закомментировать одну строку этого и посмотреть, каков будет результат, удалив один шаг
    4. Каждый подпроцесс может добавить больше информации о состоянии. Если для подпроцесса нужно что-то сообщить подпроцессу 3, это так же просто, как добавить ключ / значение.
    5. Нет шаблона для извлечения необходимых данных из состояния, чтобы вы могли сохранить их обратно. Просто передайте все состояние и позвольте подпроцессу назначить то, что ему нужно.

Теперь вернемся к моей ситуации: я взял этот урок и применил его к своей игре. То есть почти все мои высокоуровневые функции принимают и возвращают gameStateобъект. Этот объект содержит все данные игры. Например: список badGuys, список меню, добыча на земле и т. Д. Вот пример моей функции обновления:

update(gameState)
  ...
  gameState = handleUnitCollision(gameState)
  ...
  gameState = handleLoot(gameState)
  ...

Здесь я хочу спросить, создал ли я какую-то мерзость, извращающую идею, которая практична только для функционального языка программирования? JavaScript не является идиоматически функциональным (хотя он может быть написан таким образом), и действительно сложно написать неизменяемые структуры данных. Меня беспокоит то, что он предполагает, что каждый из этих подпроцессов чист. Почему необходимо сделать это предположение? Редко когда какие-либо из моих функций чистые (я имею в виду, что они часто модифицируют gameState. У меня нет никаких других сложных побочных эффектов, кроме этого). Разве эти идеи разваливаются, если у вас нет неизменных данных?

Я волнуюсь, что однажды я проснусь и пойму, что весь этот дизайн - обман, и я действительно только что внедрил антишаблон Big Ball Of Mud .


Честно говоря, я работал над этим кодом несколько месяцев, и это было здорово. Я чувствую, что получаю все преимущества, на которые он претендовал. Мой код очень прост для меня, чтобы рассуждать о. Но я команда из одного человека, поэтому у меня есть проклятие знаний.

Обновить

Я кодировал 6+ месяцев с этим шаблоном. Обычно к этому времени я забываю, что я сделал, и именно здесь "я написал это чисто?" вступает в игру. Если бы не я, я бы действительно боролся. Пока я не борюсь вообще.

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

Я хочу прямо ответить на те, которые уже имеют плохой личный опыт с этим способом кодирования. Тогда я этого не знал, но думаю, что мы действительно говорим о двух разных способах написания кода. То, как я это сделал, выглядит более структурированным, чем то, что пережили другие. Когда кто-то имеет плохой личный опыт работы с «Все это карта», они говорят о том, как трудно поддерживать, потому что:

  1. Вы никогда не знаете структуру карты, которая требуется для функции
  2. Любая функция может изменить входные данные способами, которые вы никогда не ожидаете. Вы должны просмотреть всю кодовую базу, чтобы узнать, как тот или иной ключ попал в карту или почему он исчез.

Для тех, у кого такой опыт, возможно, кодовая база гласила: «Все требует 1 из N типов карт». Моя, «Все занимает 1 из 1 типа карты». Если вы знаете структуру этого 1 типа, вы знаете структуру всего. Конечно, эта структура обычно растет со временем. Вот почему...

Есть одно место для поиска эталонной реализации (т.е. схема). Эта эталонная реализация является кодом, используемым в игре, поэтому она не может устареть.

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

Если эта архитектура рухнет под собственным весом, я добавлю второе обновление. В противном случае предположим, что все идет хорошо :)

Даниэль Каплан
источник
2
Классный вопрос (+1)! Я нахожу это очень полезным упражнением, чтобы попытаться реализовать функциональные идиомы на нефункциональном (или не очень сильно функциональном) языке.
Джорджио
15
Любой, кто скажет вам, что сокрытие информации в ОО-стиле (со свойствами и функциями доступа) является плохой вещью из-за (как правило, незначительного) падения производительности, а затем скажет вам превратить все ваши параметры в карту, что дает вам (гораздо большие) издержки поиска хеша каждый раз, когда вы пытаетесь получить значение, могут быть безопасно проигнорированы.
Мейсон Уилер
4
@MasonWheeler позволяет сказать, что вы правы в этом. Собираетесь ли вы аннулировать все остальные замечания, которые он делает из-за того, что это неправильно?
Даниэль Каплан
9
В Python (и я полагаю, что большинство динамических языков, включая Javascript), объект в любом случае является просто сахаром синтаксиса для dict / map.
Ложь Райан
6
@EvanPlaice: обозначение Big-O может быть обманчивым. Простой факт заключается в том, что все происходит медленно по сравнению с прямым доступом с двумя или тремя отдельными инструкциями машинного кода, и в случае чего-то, что происходит так же часто, как вызов функции, эти издержки будут складываться очень быстро.
Мейсон Уилер

Ответы:

42

Я поддерживал приложение, где «все это карта» раньше. Это ужасная идея. ПОЖАЛУЙСТА, не делай этого!

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

С другой стороны, если вы передадите все как карту, программист, поддерживающий ваше приложение, должен будет полностью понимать вызываемую функцию, чтобы знать, какие значения должна содержать карта. Хуже того, очень заманчиво повторно использовать карту, переданную текущей функции, чтобы передать данные следующим функциям. Это означает, что программист, поддерживающий ваше приложение, должен знать все функции, вызываемые текущей функцией, чтобы понять, что делает текущая функция. Это прямо противоположно цели написания функций - абстрагирование проблем, чтобы вам не приходилось думать о них! Теперь представьте 5 глубоких и 5 телефонных звонков каждый. Это чертовски много, чтобы иметь в виду, и чертовски много ошибок, чтобы сделать.

«все является картой» также, по-видимому, приводит к использованию карты в качестве возвращаемого значения. Я видел это. И, опять же, это боль. Вызываемые функции никогда не должны перезаписывать возвращаемое значение друг друга - если вы не знаете функциональности всего и не знаете, что значение X входной карты необходимо заменить для следующего вызова функции. И текущей функции необходимо изменить карту, чтобы она возвращала свое значение, которое иногда должно перезаписывать предыдущее значение, а иногда нет.

редактировать - пример

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

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

Следующие функции будут получать данные из базы данных. Или, скорее, они передадут карту в слой доступа к данным. DAL проверит, содержит ли карта определенные значения, чтобы контролировать выполнение запроса. Если бы 'justcount' был ключом, то запрос был бы 'count select foo from bar'. Любая из ранее вызванных функций могла иметь ту, которая добавила 'justcount' на карту. Результаты запроса будут добавлены на ту же карту.

Результаты выдаются вызывающей стороне (бизнес-логике), которая проверяет карту, что делать. Отчасти это может быть связано с тем, что было добавлено на карту с помощью исходной бизнес-логики. Некоторые приходят из данных из базы данных. Единственный способ узнать, откуда он взялся, - найти код, который его добавил. И другое место, которое также может добавить его.

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

атк
источник
2
Ваш второй абзац имеет смысл для меня, и это действительно звучит как отстой. Из твоего третьего абзаца я понимаю, что на самом деле мы говорим не о том же дизайне. «повторное использование» это точка. Было бы неправильно избегать этого. И я действительно не могу относиться к вашему последнему абзацу. У меня есть каждая функция, gameStateне зная ничего о том, что произошло до или после нее. Он просто реагирует на данные, которые ему дают. Как вы попали в ситуацию, когда функции будут наступать друг на друга? Можете привести пример?
Даниэль Каплан
2
Я добавил пример, чтобы сделать его немного понятнее. Надеюсь, это поможет. Кроме того, существует разница между передачей четко определенного объекта состояния и передачей большого двоичного объекта, который по многим причинам устанавливает измененные объекты, смешиванием логики пользовательского интерфейса, бизнес-логики и логики доступа к базе данных
atk
28

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

Например, попробуйте ответить на следующие вопросы о каждой функции подпроцесса:

  • Какие поля stateэто требует?
  • Какие поля он изменяет?
  • Какие поля не изменены?
  • Можете ли вы безопасно изменить порядок функций?

С этим шаблоном вы не сможете ответить на эти вопросы, не прочитав всю функцию.

В объектно-ориентированном языке шаблон имеет еще меньше смысла, потому что отслеживание состояния - это то, что делают объекты.

Карл Билефельдт
источник
2
«Преимущества неизменяемости уменьшаются, чем больше становятся ваши неизменные объекты» Почему? Это комментарий к производительности или ремонтопригодности? Пожалуйста, уточните это предложение.
Даниэль Каплан
8
@tieTYT Immutables хорошо работают, когда есть что-то маленькое (например, числовой тип). Вы можете копировать их, создавать их, отбрасывать их, весить их с довольно низкой стоимостью. Когда вы начинаете работать со всеми игровыми состояниями, состоящими из глубоких и больших карт, деревьев, списков и десятков, если не сотен переменных, стоимость их копирования или удаления возрастает (и весы становятся непрактичными).
3
Понимаю. Это проблема «неизменяемых данных на непостоянных языках» или проблема «неизменяемых данных»? IE: Может быть, это не проблема в коде Clojure. Но я вижу, как это в JS. Также сложно написать весь стандартный код для этого.
Даниэль Каплан
3
@MichaelT and Karl: чтобы быть справедливым, вы должны действительно упомянуть другую сторону истории неизменности / эффективности. Да, наивное использование может быть ужасно неэффективным, поэтому люди придумали лучшие подходы. Смотрите работу Криса Окасаки для получения дополнительной информации.
3
@MattFenwick Мне лично очень нравятся неизменные. Имея дело с многопоточностью, я знаю вещи об неизменяемости и могу работать и копировать их безопасно. Я помещаю его в вызов параметра и передаю другому, не беспокоясь о том, что кто-то изменит его, когда он вернется ко мне. Если говорить о сложном игровом состоянии (вопрос использовался в качестве примера - я был бы испуган, думая о чем-то столь же «простом», как игровое состояние nethack как неизменное), вероятно, неизменность - неправильный подход.
12

То, что вы, похоже, делаете, это, по сути, ручная государственная монада; то, что я хотел бы сделать, это создать (упрощенный) комбинатор связывания и повторно выразить связи между вашими логическими шагами, используя это:

function stateBind() {
    var computation = function (state) { return state; };
    for ( var i = 0 ; i < arguments.length ; i++ ) {
        var oldComp = computation;
        var newComp = arguments[i];
        computation = function (state) { return newComp(oldComp(state)); };
    }
    return computation;
}

...

stateBind(
  subprocessOne,
  subprocessTwo,
  subprocessThree,
);

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

Для объяснения полной, не упрощенной государственной монады и отличного введения в монады в целом в JavaScript, см. Этот пост в блоге .

Пламя Птариена
источник
1
Хорошо, я посмотрю на это (и прокомментирую это позже). Но что вы думаете об идее использования шаблона?
Даниэль Каплан
1
@tieTYT Я думаю, что сама модель - очень хорошая идея; монада состояний в целом является полезным инструментом структурирования кода для псевдо-изменяемых алгоритмов (алгоритмов, которые являются неизменяемыми, но эмулируют изменчивость).
Пламя Птариена
2
+1 за то, что вы заметили, что эта модель по сути является монадой. Однако я не согласен с тем, что это хорошая идея в языке, который на самом деле имеет изменчивость. Monad - это способ предоставить возможность глобальных / изменяемых состояний в языке, который не допускает мутации. ИМО, на языке, который не обеспечивает неизменности, модель Монады - это просто умственная мастурбация.
Ли Райан
6
@LieRyan Монады вообще не имеют ничего общего с изменчивостью или глобальностью; только государственная монада делает это конкретно (потому что это именно то, для чего она предназначена). Я также не согласен с тем, что государственная монада бесполезна на языке с изменчивостью, хотя реализация, основанная на изменчивости внизу, может быть более эффективной, чем неизменная, которую я дал (хотя я совсем не уверен в этом). Монадический интерфейс может обеспечить высокоуровневые возможности, которые в противном случае не легко доступны, и приведенный stateBindмною комбинатор является очень простым примером этого.
Пламя Птариена
1
@LieRyan Я второй комментарий Птариена - большинство монад не о состоянии или изменчивости, и даже не о глобальном состоянии. Монады на самом деле работают довольно хорошо в ОО / императивных / изменчивых языках.
11

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

Fogus: Итак, как только побочные сложности были уменьшены, как Clojure может помочь решить проблему? Например, идеализированная объектно-ориентированная парадигма предназначена для стимулирования повторного использования, но Clojure не является классически объектно-ориентированным - как мы можем структурировать наш код для повторного использования?

Хикки: Я бы поспорил об ОО и повторном использовании, но, безусловно, возможность многократно использовать вещи упрощает проблему, так как вы не изобретаете колеса вместо того, чтобы строить автомобили. А Clojure, находящийся в JVM, делает доступным множество колес - библиотек. Что делает библиотеку многоразовой? Он должен хорошо выполнять одну или несколько вещей, быть относительно самодостаточным и предъявлять мало требований к клиентскому коду. Ничего из этого не выпадает из ОО, и не все библиотеки Java соответствуют этим критериям, но многие из них.

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

Эта ассоциативная модель - всего лишь одна из нескольких абстракций, поставляемых с Clojure, и они являются истинной основой ее подхода к повторному использованию: функции на абстракциях. Наличие открытого и большого набора функций работает с открытым и небольшим набором расширяемых абстракций является ключом к алгоритмическому повторному использованию и совместимости библиотек. Подавляющее большинство функций Clojure определены в терминах этих абстракций, и авторы библиотек также разрабатывают свои форматы ввода и вывода, исходя из них, осознавая огромную совместимость между независимо разработанными библиотеками. Это резко контрастирует с DOM и другими подобными вещами, которые вы видите в ОО. Конечно, вы можете сделать подобную абстракцию в OO с интерфейсами, например, с коллекциями java.util, но вы можете сделать это так же легко, как и в java.io.

Фогус повторяет эти моменты в своей книге « Функциональный Javascript» :

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

Если единственными операциями, которые мы можем выполнить над объектом Book или экземпляром типа Employee, являются setTitle или getSSN, то мы заблокировали наши данные на микроязыках по частям (Hickey 2011). Более гибкий подход к моделированию данных - это метод ассоциативных данных. Объекты JavaScript, даже без прототипа, являются идеальным средством для ассоциативного моделирования данных, где именованные значения могут быть структурированы для формирования моделей данных более высокого уровня, к которым обращаются единообразным образом.

Хотя инструменты для манипулирования объектами JavaScript и доступа к ним в качестве карт данных немногочисленны внутри самого JavaScript, к счастью, Underscore предоставляет множество полезных операций. Среди простейших функций: _.keys, _.values ​​и _.pluck. И _.keys, и _.values ​​именуются в соответствии с их функциональностью, то есть для получения объекта и возврата массива его ключей или значений ...

pooya72
источник
2
Я читал это интервью Фогуса / Хикки раньше, но я не был способен понять, о чем он говорил до сих пор. Спасибо за Ваш ответ. Все еще не уверен, что Хикки / Фогус дадут моему дизайну их благословение. Я обеспокоен тем, что довел дух их совета до крайности.
Даниэль Каплан
9

Адвокат дьявола

Я думаю, что этот вопрос заслуживает защиты дьявола (но, конечно, я пристрастен). Я думаю, что @KarlBielefeldt делает очень хорошие замечания, и я хотел бы обратиться к ним. Сначала я хочу сказать, что его очки великолепны.

Поскольку он упомянул, что это не очень хорошая модель даже в функциональном программировании, я буду рассматривать JavaScript и / или Clojure в своих ответах. Одним из чрезвычайно важных сходств между этими двумя языками является то, что они динамически типизированы. Я был бы более согласен с его идеями, если бы реализовывал это на языке статической типизации, таком как Java или Haskell. Но я собираюсь рассмотреть альтернативу шаблону «Все это карта» как традиционный дизайн ООП в JavaScript, а не в статически типизированном языке (надеюсь, я не буду настраивать аргумент бессмысленного действия, выполняя это, пожалуйста, дайте мне знать).

Например, попробуйте ответить на следующие вопросы о каждой функции подпроцесса:

  • Какие поля состояния это требует?

  • Какие поля он изменяет?

  • Какие поля не изменены?

В динамически типизированном языке, как бы вы обычно отвечали на эти вопросы? Первый параметр функции может быть назван foo, но что это? Массив? Объект? Объект из массивов объектов? Как ты узнал? Единственный способ, которым я знаю, это

  1. читать документацию
  2. посмотрите на тело функции
  3. посмотрите на тесты
  4. угадать и запустить программу, чтобы увидеть, работает ли она.

Я не думаю, что паттерн «Все это карта» имеет здесь какое-то значение. Это все еще единственный способ ответить на эти вопросы.

Также имейте в виду, что в JavaScript и большинстве императивных языков программирования любой functionможет требовать, изменять и игнорировать любое состояние, к которому он имеет доступ, и подпись не имеет значения: функция / метод может что-то делать с глобальным состоянием или с единичным. Подписи часто врут.

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

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

  • Можете ли вы безопасно изменить порядок функций?

В моем коде да. Смотрите мой второй комментарий к ответу @ Evicatos. Возможно, это только потому, что я делаю игру, но не могу сказать. В игре, которая обновляется 60 раз в секунду, не имеет значения, dead guys drop lootтогда good guys pick up lootили наоборот. Каждая функция по-прежнему выполняет именно то, что должна, независимо от порядка их запуска. Одни и те же данные просто вводятся в них при разных updateвызовах, если вы меняете порядок. Если у вас есть good guys pick up lootто dead guys drop loot, хорошие парни будут собирать добычу в следующем, updateи это не имеет большого значения. Человек не сможет заметить разницу.

По крайней мере, это был мой общий опыт. Я чувствую себя действительно уязвимым, признавая это публично. Может быть, считать, что это хорошо, очень и очень плохо. Дайте мне знать, если я совершил здесь ужасную ошибку. Но, если у меня есть, это очень легко переставить функции так , заказ dead guys drop lootзатем good guys pick up lootснова. Это займет меньше времени, чем время, необходимое для написания этого абзаца: P

Может быть, вы думаете, что «мертвые парни должны сначала бросить добычу. Было бы лучше, если бы ваш код применял этот порядок». Но почему враги должны сбрасывать добычу, прежде чем вы сможете забрать добычу? Для меня это не имеет смысла. Может быть, добыча была отброшена 100 лет updatesназад. Нет необходимости проверять, должен ли произвольный плохой парень собирать добычу, которая уже находится на земле. Поэтому я считаю, что порядок этих операций совершенно произвольный.

С помощью этого шаблона естественно написать разделенные шаги, но трудно заметить ваши связанные шаги в традиционном ООП. Если бы я писал традиционный ООП, естественный, наивный способ мышления - сделать dead guys drop lootвозвращение Lootобъектом, который я должен передать в good guys pick up loot. Я не смог бы переупорядочить эти операции, так как первая возвращает ввод второй.

В объектно-ориентированном языке шаблон имеет еще меньше смысла, потому что отслеживание состояния - это то, что делают объекты.

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

Кроме того, преимущества неизменяемости снижаются с увеличением размера неизменяемых объектов.

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

Даниэль Каплан
источник
4
«Первый параметр функции может быть назван foo, но что это?» Вот почему вы не называете свои параметры «foo», а «повторы», «родитель» и другие имена, которые делают очевидным, что ожидается в сочетании с именем функции.
Себастьян Редл
1
Я должен согласиться с вами по всем пунктам. Единственная проблема, которую Javascript на самом деле представляет с этим шаблоном, заключается в том, что вы работаете с изменяемыми данными, и, как таковая, вы, скорее всего, будете изменять состояние. Тем не менее, есть библиотека, которая дает вам доступ к clojure структурам данных в простом javascript, хотя я забываю, как это называется. Передача аргументов как объекта не является неслыханной, jquery делает это в нескольких местах, но документирует, какие части объекта они используют. Лично я бы разделил UI-поля и GameLogic-поля, но все, что вам
подходит
@SebastianRedl За что я должен пройти parent? Есть repetitionsи массив чисел или строк или это не имеет значения? Или, может быть, повторения - это просто число, представляющее количество представлений, которые я хочу? Есть много apis там, которые просто берут объект параметров . Мир лучше, если вы правильно называете вещи, но это не гарантирует, что вы будете знать, как использовать API, без вопросов.
Даниэль Каплан
8

Я обнаружил, что мой код имеет такую ​​структуру:

  • Функции, которые принимают карты, как правило, больше и имеют побочные эффекты.
  • Функции, которые принимают аргументы, как правило, меньше и являются чистыми.

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

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

В javascript одна вещь, которая очень помогает, - это использовать что-то вроде библиотеки совпадений Meteor для проверки параметров. Это делает очень ясным, что ожидает функция и может обрабатывать карты довольно чисто.

Например,

function foo (post) {
  check(post, {
    text: String,
    timestamp: Date,
    // Optional, but if present must be an array of strings
    tags: Match.Optional([String])
    });

  // do stuff
}

Смотрите http://docs.meteor.com/#match для получения дополнительной информации.

:: ОБНОВИТЬ ::

Видеозапись Стюарта Сьерры из Clojure / West "Clojure in the Large" также затрагивает эту тему. Как и OP, он контролирует побочные эффекты как часть карты, поэтому тестирование становится намного проще. У него также есть пост в блоге с изложением его текущего рабочего процесса Clojure, который кажется актуальным.

alanning
источник
1
Я думаю, что мои комментарии к @Evicatos уточнят мою позицию здесь. Да, я мутирую, а функции не чистые. Но мои функции действительно легко тестировать, особенно если вспомнить регрессионные дефекты, которые я не планировал тестировать. Половина заслуг принадлежит JS: очень просто построить «карту» / объект, используя только те данные, которые мне нужны для моего теста. Тогда это так же просто, как передать его и проверить мутации. Побочные эффекты всегда представлены в карте, так что они легко проверить.
Даниэль Каплан
1
Я считаю, что прагматичное использование обоих методов - это «правильный» путь. Если тестирование является легким для вас, и вы можете решить проблему с передачей обязательных полей другим разработчикам, это звучит как победа. Спасибо Вам за Ваш вопрос; Мне понравилось читать интересную дискуссию, которую вы начали.
13
5

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

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

Чем больше я думаю об этом, тем больше ваш объект gameState пахнет как глобальный. Если это то, как это используется, зачем раздавать это?

Майк Партридж
источник
1
Да, так как я обычно мутирую, это глобально. Зачем проходить мимо? Я не знаю, это правильный вопрос. Но моя интуиция подсказывает мне, что если я перестану ее передавать, моя программа сразу станет труднее рассуждать. Каждая функция может делать все или ничего для глобального состояния. Теперь, вы видите этот потенциал в сигнатуре функции. Если вы не можете сказать, я не уверен в этом :)
Даниэль Каплан
1
КСТАТИ: re: главный аргумент против этого: это кажется правдой, будь то в clojure или javascript. Но это ценное замечание. Возможно, перечисленные преимущества намного перевешивают негативные последствия этого.
Даниэль Каплан
2
Теперь я знаю, почему я пытаюсь передать его, хотя это глобальная переменная: она позволяет мне писать чистые функции. Если я поменяю gameState = f(gameState)на f(), это будет намного сложнее проверить f. f()может возвращать разные вещи каждый раз, когда я это называю. Но легко заставить f(gameState)возвращать одно и то же каждый раз, когда ему дают одинаковые входные данные.
Даниэль Каплан
3

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

update(gameState)
  ...
  gameState = handleUnitCollision(gameState)
  ...
  gameState = handleLoot(gameState)
  ...

а также

{
  ...
  handleUnitCollision: function() {
    ...
  },
  ...
  handleLoot: function() {
    ...
  },
  ...
  update: function() {
    ...
    this.handleUnitCollision()
    ...
    this.handleLoot()
    ...
  },
  ...
};

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

user7610
источник
Связанный, более общий вопрос [ programmers.stackexchange.com/questions/260309/… Моделирование данных по сравнению с традиционными классами)
user7610
«В объектно-ориентированном программировании объект бога - это объект, который знает слишком много или делает слишком много. Объект бога является примером анти-паттерна». Итак, объект бога не очень хорошая вещь, но, похоже, ваше послание говорит об обратном. Это немного смущает меня.
Даниэль Каплан
@tieTYT вы не занимаетесь объектно-ориентированным программированием, так что все в порядке
user7610
Как вы пришли к такому выводу («все в порядке»)?
Даниэль Каплан
Проблема с объектом Бога в ОО состоит в том, что «Объект становится настолько осведомленным обо всем, или все объекты становятся настолько зависимыми от объекта Бога, что когда есть изменение или исправление ошибки, это становится настоящим кошмаром для реализации». источник В вашем коде есть это другой объект рядом с объектом Бога, так что вторая часть не является проблемой. Что касается первой части, ваш объект Бога - это программа, и ваша программа должна знать обо всем. Так что это тоже нормально.
user7610
2

Я только что столкнулся с этой темой ранее сегодня, когда играл с новым проектом. Я работаю в Clojure, чтобы сделать игру в покер. Я представлял номиналы и масти как ключевые слова и решил представить карту как карту

{ :face :queen :suit :hearts }

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

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

(defn face [card] (card :face))
(defn suit [card] (card :suit))

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

В моей программе карта, вероятно, будет только двухзначной картой. В этом вопросе все игровое состояние передается как карта. Состояние игры будет намного сложнее, чем у одной карты, но я не думаю, что есть какая-то ошибка в использовании карты. В объектно-императивном языке я также мог бы иметь один большой объект GameState и вызывать его методы, и у меня была бы та же проблема:

class State
  def complex-process()
    state = clone(this) ; or just use 'this' below if mutation is fine
    state.subprocess-one()
    state.subprocess-two()
    state.subprocess-three()
    return state

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

Xen
источник
2

Из того, что (мало) я видел, использование карт или других вложенных структур для создания единого глобального неизменяемого объекта состояния, подобного этому, довольно распространено в функциональных языках, по крайней мере, в чистых, особенно при использовании State Monad в качестве @ Ptharien'sFlame mentioend .

Два препятствия для эффективного использования этого, о которых я видел / читал (и другие ответы здесь упоминались):

  • Мутирование (глубоко) вложенного значения в (неизменяемом) состоянии
  • Сокрытие большей части состояния от функций, которые не нуждаются в этом, и просто предоставление им того, что им нужно для работы с / mutate

Есть несколько различных методов / общих шаблонов, которые могут помочь облегчить эти проблемы:

Первый - это застежки-молнии : они позволяют проходить и изменять состояние глубоко внутри неизменной вложенной иерархии.

Другой - линзы : они позволяют вам сфокусироваться на структуре в определенном месте и прочитать / изменить значение там. Вы можете комбинировать разные линзы вместе, чтобы сосредоточиться на разных вещах, что-то вроде цепочки настраиваемых свойств в ООП (где вы можете заменить переменные для фактических имен свойств!)

Prismatic недавно сделал сообщение в блоге об использовании такого рода техники, среди прочего, в JavaScript / ClojureScript, которую вы должны проверить. Они используют курсоры (которые они сравнивают с застежками-молниями) для состояния окна для функций:

Ом восстанавливает инкапсуляцию и модульность с помощью курсоров. Курсоры предоставляют обновляемые окна в определенные части состояния приложения (во многом как молнии), позволяя компонентам брать ссылки только на соответствующие части глобального состояния и обновлять их без контекста.

IIRC, они также касаются неизменности в JavaScript в этом посте.

Павел
источник
Упомянутый OP обсуждений также обсуждает использование функции update-in для ограничения области действия, которую функция может обновлять до поддерева карты состояний. Я думаю, что никто еще не поднял это.
user7610
@ user7610 Хороший улов, я не могу поверить, что я забыл упомянуть об этом - мне нравится эта функция (и assoc-inдр.). Думаю, у меня только что был Хаскелл в мозгу. Интересно, кто-нибудь сделал это с портом JavaScript? Люди, вероятно, не поднимали это, потому что (как я) они не смотрели разговор :)
Пол
@paul в некотором смысле они имеют, потому что это доступно в ClojureScript, но я не уверен, если это «учитывается» в вашем уме. Он может существовать в PureScript, и я считаю, что есть по крайней мере одна библиотека, которая обеспечивает неизменные структуры данных в JavaScript. Я надеюсь, что по крайней мере у одного из них они есть, иначе их было бы неудобно использовать.
Даниэль Каплан
@tieTYT Я думал о нативной реализации JS, когда делал этот комментарий, но вы хорошо понимаете, что такое ClojureScript / PureScript. Я должен посмотреть на неизменный JS и посмотреть, что там, я не работал с этим раньше.
Пол
1

Хорошая ли это идея или нет, будет зависеть от того, что вы делаете с состоянием внутри этих подпроцессов. Если я правильно понимаю пример Clojure, возвращаемые словари состояний не являются теми же словарями состояний, которые передаются. Это копии, возможно, с дополнениями и модификациями, которые (я предполагаю) Clojure способен эффективно создавать, потому что от этого зависит функциональная природа языка. Исходные словари состояния для каждой функции никак не изменяются.

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

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

Evicatos
источник
1
В конце концов, это действительно "очень, очень разные"? В примере с Clojure он перезаписывает свое старое состояние новым. Да, настоящей мутации не происходит, идентичность просто меняется. Но в своем «хорошем» примере, как написано, у него нет способа получить копию, которая была передана в подпроцесс-два. Идентификатор этого значения был перезаписан. Поэтому я думаю, что то, что «очень, очень отличается», - это на самом деле просто деталь реализации языка. По крайней мере, в контексте того, что вы воспитываете.
Даниэль Каплан
2
В примере Clojure происходит две вещи: 1) первый пример зависит от функций, вызываемых в определенном порядке, и 2) функции являются чистыми, поэтому они не имеют побочных эффектов. Поскольку функции во втором примере являются чистыми и имеют одну и ту же сигнатуру, вы можете изменить их порядок, не беспокоясь о каких-либо скрытых зависимостях в порядке их вызова. Если вы изменяете состояние в своих функциях, у вас нет той же гарантии. Мутация состояния означает, что ваша версия не так легко компоноваться, что и было первоначальной причиной словаря.
Evicatos
1
Тебе придется показать мне пример, потому что в моем опыте с этим я могу перемещать вещи по желанию, и это имеет очень мало эффекта. Просто чтобы доказать это себе, я переместил два случайных вызова подпроцесса в середине моей update()функции. Я переместил один наверх и один на дно. Все мои тесты все еще прошли, и когда я играл в свою игру, я не заметил никаких побочных эффектов. Я чувствую, что мои функции так же сложны, как и пример Clojure. Мы оба выбрасываем наши старые данные после каждого шага.
Даниэль Каплан
1
Если вы прошли тесты и не заметили каких-либо побочных эффектов, это означает, что вы в настоящее время не изменяете ни одно состояние, которое имеет неожиданные побочные эффекты в других местах. Поскольку ваши функции не чисты, у вас нет гарантии, что так будет всегда. Я думаю, что я должен в корне неправильно понять что-то о вашей реализации, если вы говорите, что ваши функции не чисты, но вы выбрасываете свои старые данные после каждого шага.
Evicatos
1
@Evicatos - чистота и одинаковая подпись не означают, что порядок функций не имеет значения. Представьте себе расчет цены с фиксированными и процентными скидками. (-> 10 (- 5) (/ 2))возвращает 2.5. (-> 10 (/ 2) (- 5))возвращает 0.
Зак
1

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

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

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

Если вы передавали «молнию», «линзу» или «курсор», как описано в посте @paul, это одно. Вы будете содержать доступ и позволять молнии и т. Д. Контролировать чтение и запись данных.

Нарушение единой ответственности Утверждать, что каждое из «subprocess-one», «subprocess-two» и «subprocess-three» несет только одну ответственность, а именно создавать новый объект глобального состояния с правильными значениями в нем, является вопиющим редукционизмом. Это все биты в конце концов, не так ли?

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

Влияние систем

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

Добавление другого программиста, или двух, или восьми, приведет к тому, что ваша система развалится почти сразу.

  1. Кривая обучения для божьих объектов плоская (то есть требуется очень много времени, чтобы стать компетентным в них). Каждый дополнительный программист должен будет выучить все, что вы знаете, и держать это в голове. Вы сможете нанять только тех программистов, которые лучше вас, при условии, что вы сможете заплатить им достаточно, чтобы страдать от поддержки огромного объекта бога.
  2. Подготовка теста, в вашем описании, только белый ящик. Вам нужно знать каждую деталь объекта бога, а также тестируемого модуля, чтобы настроить тест, запустить его и определить, что а) он поступил правильно, и б) он не сделал ничего 10000 неправильных вещей. Шансы на тебя сильно сложены.
  3. Добавление новой функции требует, чтобы вы a) прошли каждый подпроцесс и определили, влияет ли функция на какой-либо код в нем, и наоборот, b) просматривают ваше глобальное состояние и спроектируют дополнения, и c) проходят каждый модульный тест и модифицируют его. чтобы убедиться, что ни одно из тестируемых устройств не повлияло на новую функцию .

в заключение

  1. Изменчивые объекты бога были проклятием моего существования в программировании, некоторые из моих собственных дел, а некоторые были в ловушке.
  2. Государственная монада не масштабируется. Состояние растет в геометрической прогрессии со всеми вытекающими отсюда последствиями для тестирования и операций. То, как мы контролируем состояние в современных системах, - это делегирование (разделение обязанностей) и определение области действия (ограничение доступа только к подмножеству состояния). Подход «все это карта» является полной противоположностью контроля государства.
BobDalgleish
источник