Обработка инкрементного моделирования данных Изменения в функциональном программировании
Большинство проблем, которые я должен решить в своей работе в качестве разработчика, имеют отношение к моделированию данных. Например, в мире веб-приложений OOP мне часто приходится изменять свойства данных, которые находятся в объекте для удовлетворения новых требований.
Если мне повезет, мне даже не нужно программно добавлять новый код поведения (функции, методы). Вместо этого я могу декларативно добавлять проверки и даже параметры пользовательского интерфейса, аннотируя свойство (Java).
В функциональном программировании кажется, что для добавления новых свойств данных требуется много изменений кода из-за соответствия шаблонов и конструкторов данных (Haskell, ML).
- Начало работы с Haskell
- Возможно ли программирование графического интерфейса?
- Почему java.util.Collection не реализует новый интерфейс Stream?
- Разница между сокращением и foldLeft / fold в функциональном программировании (в частности, Scala и Scala API)?
- Каково точное определение замыкания?
Как свести к минимуму эту проблему?
Это, по-видимому, признанная проблема, так как Xavier Leroy хорошо говорит на стр. 24 «Объекты и classы против модhive». Чтобы обобщить те, у которых нет средства просмотра PostScript, в основном говорят, что языки FP лучше, чем языки ООП, для добавления новых поведение над объектами данных, но языки ООП лучше подходят для добавления новых объектов / свойств данных.
Есть ли какой-либо шаблон проектирования, используемый в языках FP, чтобы помочь смягчить эту проблему?
Я прочитал рекомендацию Филлипа Вадлера использовать Monads для решения этой проблемы модульности, но я не уверен, что понимаю, как это сделать?
- Какую задачу лучше всего выполнять в стиле функционального программирования?
- Каков способ Scala для реализации повторного вызова, подобного этому?
- Что такое «n + k patterns» и почему они запрещены в Haskell 2010?
- Объяснение «привязка узла»
- Почему функциональные языки?
- Поток Zipping с использованием JDK8 с lambda (java.util.stream.Streams.zip)
- F # разработка и модульное тестирование?
- Функциональное реактивное программирование в Scala
Как отметил Дариус Бэкон , это, по сути, проблема выражения, давняя проблема без общепринятого решения. Отсутствие подхода «лучший из обоих миров» не мешает нам иногда хотеть идти так или иначе. Теперь вы попросили «шаблон дизайна для функциональных языков» , поэтому давайте сделаем снимок. Следующий пример написан в Haskell, но не обязательно идиоматичен для Haskell (или любого другого языка).
Во-первых, быстрый обзор «проблемы выражения». Рассмотрим следующий тип алгебраических данных:
data Expr a = Lit a | Sum (Expr a) (Expr a) exprEval (Lit x) = x exprEval (Sum xy) = exprEval x + exprEval y exprShow (Lit x) = show x exprShow (Sum xy) = unwords ["(", exprShow x, " + ", exprShow y, ")"]
Это представляет собой простые математические выражения, содержащие только буквенные значения и дополнение. С помощью функций, которые мы имеем здесь, мы можем взять выражение и оценить его или показать его как String
. Теперь, скажем, мы хотим добавить новую функцию – скажем, отобразить функцию по всем литеральным значениям:
exprMap f (Lit x) = Lit (fx) exprMap f (Sum xy) = Sum (exprMap fx) (exprMap fy)
Легко! Мы можем продолжать писать функции весь день, не разбивая пота! Алгебраические типы данных являются удивительными!
На самом деле, они такие потрясающие, мы хотим сделать наш тип выражения более выразительным, ошибочным, выразительным. Давайте продолжим его, чтобы поддержать умножение, мы просто … э-э … о, дорогая, это будет неудобно, не так ли? Мы должны изменить каждую функцию, которую мы только что написали. Отчаяние!
На самом деле, возможно, расширение самих выражений более интересно, чем добавление функций, которые их используют. Итак, допустим, мы готовы сделать компромисс в другом направлении. Как мы можем это сделать?
Ну, нет смысла делать что-то наполовину. Давайте расширим все и инвертируем всю программу. Что это значит? Ну, это функциональное программирование, и что более функционально, чем функции более высокого порядка? Что мы сделаем, это заменить тип данных, представляющий значения выражения, одним из которых является действие над выражением . Вместо выбора конструктора нам понадобится запись всех возможных действий, примерно так:
data Actions a = Actions { actEval :: a, actMap :: (a -> a) -> Actions a }
Итак, как мы можем создать выражение без типа данных? Ну, теперь наши функции – данные, поэтому я думаю, что наши данные должны быть функциями. Мы будем создавать «конструкторы» с использованием регулярных функций, возвращая запись действий:
mkLit x = Actions x (\f -> mkLit (fx)) mkSum xy = Actions (actEval x + actEval y) (\f -> mkSum (actMap xf) (actMap yf))
Можем ли мы теперь добавить умножение? Конечно, может!
mkProd xy = Actions (actEval x * actEval y) (\f -> mkProd (actMap xf) (actMap yf))
О, но подождите – мы забыли добавить действие actShow
раньше, давайте добавим это, мы просто … errh, хорошо.
Во всяком случае, как это выглядит, как использовать два разных стиля?
expr1plus1 = Sum (Lit 1) (Lit 1) action1plus1 = mkSum (mkLit 1) (mkLit 1) action1times1 = mkProd (mkLit 1) (mkLit 1)
В значительной степени то же самое, когда вы не расширяете их.
В качестве интересной заметки обратите внимание, что в стиле «действия» фактические значения в выражении полностью скрыты – поле actEval
только обещает дать нам что-то вроде правильного типа, как оно обеспечивает его собственный бизнес. Благодаря ленивой оценке содержимое поля может быть даже сложным вычислением, которое выполняется только по требованию. Actions a
Значение полностью непрозрачно для внешнего контроля, представляя только определенные действия для внешнего мира.
Этот стиль программирования – замена простых данных пакетом «действий», скрывая фактические данные реализации в черном ящике, используя конструктор-подобные функции для создания новых битов данных, имея возможность обменивать очень разные «значения» с тем же множество «действий» и т. д. – интересно. Вероятно, для этого есть имя, но я не могу вспомнить …
Я слышал эту жалобу больше, чем несколько раз, и это меня всегда смущает. Вопросник писал:
В функциональном программировании кажется, что для добавления новых свойств данных требуется много изменений кода из-за соответствия шаблонов и конструкторов данных (Haskell, ML).
Но это по большому счету особенность, а не ошибка! Например, когда вы изменяете возможности в одном из вариантов, код, который обращается к этому варианту посредством сопоставления с образцом, вынужден учитывать тот факт, что появились новые возможности. Это полезно, потому что вам действительно нужно подумать, нужно ли изменить этот код, чтобы реагировать на семантические изменения в типах, которыми он управляет.
Я бы поспорил с утверждением, что требуется «много изменений кода». Имея хорошо написанный код, система типов обычно делает впечатляюще хорошую работу по выведению на передний план кода, который необходимо учитывать, и не намного больше.
Возможно, проблема в том, что на этот вопрос трудно ответить без более конкретного примера. Подумайте о том, чтобы предоставить часть кода в Haskell или ML, что вы не знаете, как развиваться чисто. Думаю, вы получите более точные и полезные ответы таким образом.
Этот компромисс известен в литературе по теории программирования как проблема выражения :
objective состоит в том, чтобы определить тип данных по случаям, где можно добавить новые случаи к типу данных и новым функциям над типом данных, без перекомпиляции существующего кода и при сохранении безопасности статического типа (например, без приведения).
Решения были выдвинуты, но я их не изучал. (Большое обсуждение в Lambda The Ultimate .)
В Haskell, по крайней мере, я бы сделал абстрактный тип данных. Это создать тип, который не экспортирует конструкторы. Пользователи этого типа теряют возможность сопоставления шаблонов по типу, и вам необходимо предоставить функции для работы с типом. Взамен вы получаете тип, который легче изменить без изменения кода, написанного пользователями этого типа.
Если новые данные не предполагают никакого нового поведения, как в приложении, где нас просят добавить поле «дата рождения» к ресурсу «человек», а затем все, что нам нужно сделать, это добавить его в список полей, которые являются частью ресурс человека, тогда его легко решить как в функциональном, так и в ООП мире. Просто не рассматривайте «дату рождения» как часть вашего кода; это всего лишь часть ваших данных.
Позвольте мне объяснить: если рождение является чем-то, что подразумевает поведение другого приложения, например, что мы делаем что-то по-другому, если человек несовершеннолетний, тогда в ООП мы добавим поле родительской линии в class человека, а в FP мы добавим аналогично поле даты рождения для структуры данных человека.
Если нет никакого поведения, связанного с «birthdate», тогда в коде не должно быть поля с именем «birthdate». Структура данных, такая как словарь (карта), будет содержать различные поля. Добавление нового не потребует никаких изменений в программе, независимо от того, вы ли это OOP или FP. Оценки будут добавляться аналогичным образом, добавив регулярное выражение проверки или используя аналогичный малый язык для проверки, чтобы выразить в данных, каково должно быть поведение проверки.