Как мне построить систему, которая имеет все следующее :
- Использование чистых функций с неизменяемыми объектами.
- Передайте в функцию только те данные, которые ей нужны, не более (то есть, нет большого объекта состояния приложения)
- Избегайте слишком большого количества аргументов для функций.
- Избегайте необходимости создавать новые объекты только с целью упаковки и распаковки параметров в функции, просто чтобы избежать слишком большого количества параметров, передаваемых в функции. Если я собираюсь упаковать несколько элементов в функцию как один объект, я хочу, чтобы этот объект был владельцем этих данных, а не что-то созданное временно
Мне кажется, что государственная монада нарушает правило № 2, хотя это не очевидно, потому что она пронизана монадой.
У меня есть ощущение, что мне нужно как-то использовать линзы, но очень мало написано об этом для нефункциональных языков.
Фон
В качестве упражнения я преобразовываю одно из моих существующих приложений из объектно-ориентированного стиля в функциональный стиль. Первое, что я пытаюсь сделать, - это сделать как можно больше внутреннего ядра приложения.
Одна вещь, которую я слышал, состоит в том, что как управлять «состоянием» на чисто функциональном языке, и это то, что, как я полагаю, выполняется монадами состояний, - это то, что логически вы называете чистую функцию, «передавая состояние «Мир как есть», затем, когда функция возвращается, она возвращает вам состояние мира, как оно изменилось.
Чтобы проиллюстрировать, как вы можете сделать «привет мир» чисто функциональным способом, вы можете передать в свою программу это состояние экрана и получить обратно состояние экрана с напечатанным «привет миром». Технически, вы делаете вызов чистой функции, и никаких побочных эффектов нет.
Исходя из этого, я просмотрел свое приложение и: 1. Сначала поместил все состояние своего приложения в один глобальный объект (GameState) 2. Во-вторых, я сделал GameState неизменным. Вы не можете это изменить. Если вам нужно изменить, вы должны построить новое. Я сделал это, добавив конструктор копирования, который необязательно принимает одно или несколько измененных полей. 3. Каждому приложению я передаю GameState в качестве параметра. Внутри функции, после того, как она делает то, что собирается, она создает новый GameState и возвращает его.
У меня чисто функциональное ядро и внешний цикл, который передает GameState в основной рабочий цикл приложения.
Мой вопрос:
Теперь моя проблема в том, что GameState содержит около 15 различных неизменяемых объектов. Многие из функций на самом низком уровне работают только с некоторыми из этих объектов, например, ведение счета. Итак, допустим, у меня есть функция, которая вычисляет счет. Сегодня GameState передается этой функции, которая изменяет счет путем создания нового GameState с новым счетом.
Что-то в этом кажется неправильным. Функция не нуждается во всей GameState. Ему просто нужен объект Score. Поэтому я обновил его, чтобы передать в Счет и возвращать только Счет.
Это, казалось, имело смысл, поэтому я пошел дальше с другими функциями. Некоторые функции требуют, чтобы я передавал 2, 3 или 4 параметра из GameState, но, поскольку я использовал шаблон во всем внешнем ядре приложения, я передаю все больше и больше состояния приложения. Например, в верхней части цикла рабочего процесса я бы вызвал метод, который вызвал бы метод, который вызвал бы метод и т. Д., Вплоть до того места, где вычисляется оценка. Это означает, что текущий счет проходит через все эти слои только потому, что функция в самом низу собирается вычислить счет.
Теперь у меня есть функции с десятками параметров. Я мог бы поместить эти параметры в объект, чтобы уменьшить количество параметров, но тогда я хотел бы, чтобы этот класс был главным местоположением состояния приложения состояния, а не объектом, который просто создается во время вызова просто для того, чтобы избежать передачи в несколько параметров, а затем распаковать их.
Так что теперь мне интересно, проблема в том, что мои функции вложены слишком глубоко. Это результат желания иметь маленькие функции, поэтому я делаю рефакторинг, когда функция становится большой, и делю ее на несколько меньших функций. Но при этом создается более глубокая иерархия, и все, что передается во внутренние функции, необходимо передавать во внешнюю функцию, даже если внешняя функция не работает с этими объектами напрямую.
Казалось, что просто прохождение GameState по пути избежало этой проблемы. Но я вернулся к первоначальной проблеме передачи большего количества информации в функцию, чем требуется функции.
источник
Ответы:
Я не уверен, что есть хорошее решение. Это может быть или не быть ответом, но это слишком долго для комментария. Я делал подобное, и помогли следующие уловки:
GameState
иерархически, так что вы получите 3-5 меньших частей вместо 15.Я так не думаю. Рефакторинг на маленькие функции - это правильно, но, возможно, вы могли бы лучше их перегруппировать. Иногда это невозможно, иногда просто нужно второй (или третий) взгляд на проблему.
Сравните ваш дизайн с изменчивым. Есть ли вещи, которые ухудшились после переписывания? Если так, разве вы не можете сделать их лучше так же, как делали изначально?
источник
Я не могу говорить с C #, но в Haskell, вы в конечном итоге обойдете весь штат. Вы можете сделать это либо явно, либо с помощью государственной монады. Единственное, что вы можете сделать, чтобы решить вопрос о функциях, получающих больше информации, чем им нужно, - это использовать классы типов Has. (Если вы не знакомы, классы типов Haskell немного похожи на интерфейсы C #.) Для каждого элемента E состояния вы можете определить класс типов HasE, для которого требуется функция getE, которая возвращает значение E. Тогда монаду State можно сделал экземпляр всех этих классов типов. Затем в ваших реальных функциях вместо того, чтобы явно требовать монаду State, вам нужна любая монада, принадлежащая классам типов Has для элементов, которые вам нужны; это ограничивает то, что функция может делать с монадой, которую она использует. Для получения дополнительной информации об этом подходе, см. Майкл Снойманпост на шаблон дизайна ReaderT .
Возможно, вы могли бы воспроизвести что-то подобное в C #, в зависимости от того, как вы определяете состояние, которое передается. Если у вас есть что-то вроде
Вы можете определить интерфейсы
IHasMyInt
иIHasMyString
с методамиGetMyInt
иGetMyString
соответственно. Класс состояния выглядит следующим образом:тогда ваши методы могут потребовать IHasMyInt, IHasMyString или весь MyState в зависимости от ситуации.
Затем вы можете использовать ограничение where для определения функции, чтобы вы могли передать объект состояния, но он может получить только значение string и int, а не double.
источник
Я думаю, что вам лучше узнать о Redux или Elm и о том, как они справляются с этим вопросом.
По сути, у вас есть одна чистая функция, которая принимает все состояние и действие, выполненное пользователем, и возвращает новое состояние.
Затем эта функция вызывает другие чистые функции, каждая из которых обрабатывает определенный фрагмент состояния. В зависимости от действия многие из этих функций могут ничего не делать, кроме как возвращать исходное состояние без изменений.
Чтобы узнать больше, Google Elm Architecture или Redux.js.org.
источник
Я думаю, что вы пытаетесь использовать объектно-ориентированный язык так, как его не следует использовать, как если бы он был чисто функциональным. Не то чтобы ОО языки были злыми. У обоих подходов есть свои преимущества, поэтому мы можем теперь смешивать ОО-стиль с функциональным стилем и иметь возможность сделать некоторые части кода функциональными, тогда как другие остаются объектно-ориентированными, чтобы мы могли воспользоваться всеми возможностями повторного использования, наследования или полимофизма. К счастью, мы больше не связаны ни одним из подходов, так почему вы пытаетесь ограничиться одним из них?
Отвечая на ваш вопрос: нет, я не сплетаю какие-либо конкретные состояния через логику приложения, но использую то, что подходит для текущего варианта использования, и применяю доступные методы наиболее подходящим способом.
C # не готов (пока) к использованию так, как вам хотелось бы.
источник