При программировании в функциональном стиле, есть ли у вас единое состояние приложения, которое вы вплетаете в логику приложения?

12

Как мне построить систему, которая имеет все следующее :

  1. Использование чистых функций с неизменяемыми объектами.
  2. Передайте в функцию только те данные, которые ей нужны, не более (то есть, нет большого объекта состояния приложения)
  3. Избегайте слишком большого количества аргументов для функций.
  4. Избегайте необходимости создавать новые объекты только с целью упаковки и распаковки параметров в функции, просто чтобы избежать слишком большого количества параметров, передаваемых в функции. Если я собираюсь упаковать несколько элементов в функцию как один объект, я хочу, чтобы этот объект был владельцем этих данных, а не что-то созданное временно

Мне кажется, что государственная монада нарушает правило № 2, хотя это не очевидно, потому что она пронизана монадой.

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

Фон

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

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

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

Исходя из этого, я просмотрел свое приложение и: 1. Сначала поместил все состояние своего приложения в один глобальный объект (GameState) 2. Во-вторых, я сделал GameState неизменным. Вы не можете это изменить. Если вам нужно изменить, вы должны построить новое. Я сделал это, добавив конструктор копирования, который необязательно принимает одно или несколько измененных полей. 3. Каждому приложению я передаю GameState в качестве параметра. Внутри функции, после того, как она делает то, что собирается, она создает новый GameState и возвращает его.

У меня чисто функциональное ядро ​​и внешний цикл, который передает GameState в основной рабочий цикл приложения.

Мой вопрос:

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

Что-то в этом кажется неправильным. Функция не нуждается во всей GameState. Ему просто нужен объект Score. Поэтому я обновил его, чтобы передать в Счет и возвращать только Счет.

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

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

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

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

Дайша Линн
источник
1
Я не специалист по дизайну и особенно функциональным, но, поскольку ваша игра по своей природе находится в состоянии, которое развивается, вы уверены, что функциональное программирование - это парадигма, которая вписывается во все уровни вашего приложения?
Уолфрат
Уолфрат, я думаю, что если вы поговорите с экспертами по функциональному программированию, вы, вероятно, обнаружите, что они скажут, что парадигма функционального программирования имеет решения для управления развивающимся состоянием.
Дайша Линн
Ваш вопрос показался мне шире, что только гласит. Если речь идет только об управлении состояниями, вот начало: посмотрите ответ и ссылку в stackoverflow.com/questions/1020653/…
Walfrat
2
@DaishaLynn Я не думаю, что вы должны удалить вопрос. За него проголосовали, и никто не пытается его закрыть, поэтому я не думаю, что это выходит за рамки этого сайта. Отсутствие ответа до сих пор может быть только потому, что это требует некоторого относительно нишевого опыта. Но это не значит, что в конечном итоге его не найдут и не ответят.
Бен Ааронсон
2
Управление изменяемым состоянием в сложной чисто функциональной программе без существенной языковой помощи - огромная боль. В Haskell это возможно благодаря монадам, краткому синтаксису, очень хорошему выводу типов, но все равно может быть очень раздражающим. Я думаю, что в C # у вас будет гораздо больше проблем.
Восстановить Монику

Ответы:

2

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

  • Разделите GameStateиерархически, так что вы получите 3-5 меньших частей вместо 15.
  • Пусть он реализует интерфейсы, поэтому ваши методы видят только нужные части. Никогда не отбрасывайте их обратно, так как вы бы лгали себе о настоящем типе.
  • Позвольте также частям реализовать интерфейсы, так что вы получите прекрасный контроль над тем, что вы передаете.
  • Используйте объекты параметров, но делайте это экономно и старайтесь превратить их в реальные объекты со своим поведением.
  • Иногда передача немного больше, чем нужно, лучше длинного списка параметров.

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

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

Сравните ваш дизайн с изменчивым. Есть ли вещи, которые ухудшились после переписывания? Если так, разве вы не можете сделать их лучше так же, как делали изначально?

maaartinus
источник
Кто-то сказал мне изменить мой дизайн так, чтобы когда-либо функция принимала только один параметр, чтобы я мог использовать каррирование. Я попробовал эту единственную функцию, поэтому вместо вызова DeleteEntity (a, b, c) теперь я вызываю DeleteEntity (a) (b) (c). Так что это мило, и это должно сделать вещи более составными, но я просто пока не понимаю.
Дайша Линн
@DaishaLynn Я использую Java, и нет никакого сладкого синтаксического сахара для карри, так что (для меня) это не стоит пробовать. Я довольно скептически отношусь к возможному использованию функций более высокого порядка в нашем случае, но дайте мне знать, сработало ли это для вас.
Маартин
2

Я не могу говорить с C #, но в Haskell, вы в конечном итоге обойдете весь штат. Вы можете сделать это либо явно, либо с помощью государственной монады. Единственное, что вы можете сделать, чтобы решить вопрос о функциях, получающих больше информации, чем им нужно, - это использовать классы типов Has. (Если вы не знакомы, классы типов Haskell немного похожи на интерфейсы C #.) Для каждого элемента E состояния вы можете определить класс типов HasE, для которого требуется функция getE, которая возвращает значение E. Тогда монаду State можно сделал экземпляр всех этих классов типов. Затем в ваших реальных функциях вместо того, чтобы явно требовать монаду State, вам нужна любая монада, принадлежащая классам типов Has для элементов, которые вам нужны; это ограничивает то, что функция может делать с монадой, которую она использует. Для получения дополнительной информации об этом подходе, см. Майкл Снойманпост на шаблон дизайна ReaderT .

Возможно, вы могли бы воспроизвести что-то подобное в C #, в зависимости от того, как вы определяете состояние, которое передается. Если у вас есть что-то вроде

public class MyState
{
    public int MyInt {get; set; }
    public string MyString {get; set; }
}

Вы можете определить интерфейсы IHasMyIntи IHasMyStringс методами GetMyIntи GetMyStringсоответственно. Класс состояния выглядит следующим образом:

public class MyState : IHasMyInt, IHasMyString
{
    public int MyInt {get; set; }
    public string MyString {get; set; }
    public double MyDouble {get; set; }

    public int GetMyInt () 
    {
        return MyInt;
    }

    public string GetMyString ()
    {
        return MyString;
    }

    public double GetMyDouble ()
    {
        return MyDouble;
    }
}

тогда ваши методы могут потребовать IHasMyInt, IHasMyString или весь MyState в зависимости от ситуации.

Затем вы можете использовать ограничение where для определения функции, чтобы вы могли передать объект состояния, но он может получить только значение string и int, а не double.

public static T DoSomething<T>(T state) where T : IHasMyString, IHasMyInt
{
    var s = state.GetMyString();
    var i = state.GetMyInt();
    return state;
}
DylanSp
источник
Это интересно. Так что в настоящее время, когда я передаю вызов функции и передаю 10 параметров по значению, я передаю "gameSt'ate" 10 раз, но 10 различным типам параметров, таким как "IHasGameScore", "IHasGameBoard" и т. Д. был способ передать один параметр, который может указывать функция, должен реализовывать все интерфейсы в этом одном типе. Интересно, смогу ли я сделать это с «общим ограничением» .. Позвольте мне попробовать это.
Дайша Линн
1
Это сработало. Вот это работает: dotnetfiddle.net/cfmDbs .
Дайша Линн
1

Я думаю, что вам лучше узнать о Redux или Elm и о том, как они справляются с этим вопросом.

По сути, у вас есть одна чистая функция, которая принимает все состояние и действие, выполненное пользователем, и возвращает новое состояние.

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

Чтобы узнать больше, Google Elm Architecture или Redux.js.org.

Даниэль Т.
источник
Я не знаю, Элм, но я верю, что это похоже на Redux. В Redux не все редукторы вызываются для каждого изменения состояния? Звучит крайне неэффективно.
Дайша Линн
Когда дело доходит до оптимизации низкого уровня, не предполагайте, измеряйте. На практике это достаточно быстро.
Даниэль Т.
Спасибо Дэниел, но у меня это не сработает. Я сделал достаточно разработки, чтобы знать, что он не уведомляет каждый компонент пользовательского интерфейса о любых изменениях данных, независимо от того, заботится ли элемент управления о контроле.
Дайша Линн
-2

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

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

C # не готов (пока) к использованию так, как вам хотелось бы.

t3chb0t
источник
3
Не в восторге ни от этого ответа, ни от тона. Я не злоупотребляю ничем. Я раздвигаю границы C #, чтобы использовать его как более функциональный язык. Это не редкость. Вы, кажется, философски противитесь этому, что нормально, но в этом случае не смотрите на этот вопрос. Ваш комментарий никому не нужен. Двигаться дальше.
Дайша Линн
@DaishaLynn вы не правы, я ни в коем случае не против этого, и на самом деле я часто этим пользуюсь ... но там, где это естественно и возможно, и не пытаюсь превратить ОО-язык в функциональный, просто потому, что он модный сделать это. Вы не должны соглашаться с моим ответом, но это не меняет того факта, что вы неправильно используете свои инструменты.
t3chb0t
Я не делаю этого, потому что это модно. Сам C # движется в сторону использования функционального стиля. Сам Андерс Хейлсберг указал как таковой. Я понимаю, что вы заинтересованы только в основном использовании языка, и я понимаю, почему и когда это уместно. Я просто не знаю, почему кто-то вроде тебя даже в этой теме .. Как ты на самом деле помогаешь?
Дайша Линн
@DaishaLynn, если вы не можете справиться с ответами, критикующими ваш вопрос или подход, вам, вероятно, не следует задавать здесь вопросы, или в следующий раз вы должны просто добавить заявление об отказе от ответственности, в котором говорится, что вас интересуют только те ответы, которые поддерживают вашу идею на 100%, потому что вы этого не делаете хочу услышать правду, а скорее получить поддержку мнения.
t3chb0t
Пожалуйста, будьте более сердечны друг другу. Можно дать критику, не оскорбляя язык. Попытка программирования на C # в функциональном стиле, безусловно, не является «злоупотреблением» или крайним случаем. Это обычная техника, используемая многими разработчиками C # для изучения других языков.
zumalifeguard