Обработка инкрементного моделирования данных Изменения в функциональном программировании
Большинство проблем, которые я должен решить в своей работе в качестве разработчика, имеют отношение к моделированию данных.
Например, в мире веб-приложений OOP мне часто приходится изменять свойства данных, которые находятся в объекте для удовлетворения новых требований.
Если мне повезет, мне даже не нужно программно добавлять новый код поведения (функции, методы). Вместо этого я могу декларативно добавлять проверки и даже параметры пользовательского интерфейса, аннотируя свойство (Java).
В функциональном программировании кажется, что для добавления новых свойств данных требуется много изменений кода из-за соответствия шаблонов и конструкторов данных (Haskell, ML).
Как свести к минимуму эту проблему?
Это, по-видимому, признанная проблема: Ксавье Лерой хорошо говорит на стр. 24 "Объекты и классы против модулей"
- Чтобы обобщить те, у которых нет средства просмотра PostScript, в основном говорят, что языки FP лучше, чем языки ООП, для добавления нового поведения по объектам данных, но языки ООП лучше подходят для добавления новых объектов/свойств данных.
Есть ли какой-либо шаблон проектирования, используемый в языках FP, чтобы облегчить эту проблему?
Я читал Phillip Wadler рекомендацию использования Monads, чтобы помочь решить эту проблему модульности, но я не уверен, что понимаю, как?
Ответы
Ответ 1
Как отметил Darius Bacon, это, по сути, проблема выражения, давняя проблема без общепринятого решения. Отсутствие подхода "лучший из обоих миров" не мешает нам иногда хотеть идти так или иначе. Теперь вы попросили шаблон "для функциональных языков, поэтому давайте сделаем снимок. Следующий пример написан в Haskell, но не обязательно идиоматичен для Haskell (или любого другого языка).
Во-первых, быстрый обзор "проблемы выражения". Рассмотрим следующий тип алгебраических данных:
data Expr a = Lit a | Sum (Expr a) (Expr a)
exprEval (Lit x) = x
exprEval (Sum x y) = exprEval x + exprEval y
exprShow (Lit x) = show x
exprShow (Sum x y) = unwords ["(", exprShow x, " + ", exprShow y, ")"]
Это представляет собой простые математические выражения, содержащие только литеральные значения и дополнение. Имея здесь функции, мы можем взять выражение и оценить его или показать его как String
. Теперь, скажем, мы хотим добавить новую функцию - скажем, отобразить функцию по всем литеральным значениям:
exprMap f (Lit x) = Lit (f x)
exprMap f (Sum x y) = Sum (exprMap f x) (exprMap f y)
Легко! Мы можем продолжать писать функции весь день, не разбивая пота! Алгебраические типы данных являются удивительными!
На самом деле, они такие потрясающие, мы хотим, чтобы наш тип выражения был больше, errh, выразительный. Позвольте расширить его, чтобы поддержать умножение, мы просто... э-э... о, дорогая, это будет неудобно, не так ли? Мы должны изменить каждую функцию, которую мы только что написали. Отчаяние!
На самом деле, возможно, расширение самих выражений более интересно, чем добавление функций, которые их используют. Итак, допустим, мы готовы сделать компромисс в другом направлении. Как мы можем это сделать?
Ну, нет смысла делать что-то наполовину. Пусть обновить все и инвертировать всю программу. Что это значит? Ну, это функциональное программирование и что более функционально, чем функции более высокого порядка? Что мы сделаем, это заменить тип данных, представляющий значения выражения, одним из которых является действие над выражением. Вместо выбора конструктора нам понадобится запись всех возможных действий, примерно так:
data Actions a = Actions {
actEval :: a,
actMap :: (a -> a) -> Actions a }
Итак, как нам создать выражение без типа данных? Ну, теперь наши функции - данные, поэтому я думаю, что наши данные должны быть функциями. Мы создадим "конструкторы", используя регулярные функции, возвращая запись действий:
mkLit x = Actions x (\f -> mkLit (f x))
mkSum x y = Actions
(actEval x + actEval y)
(\f -> mkSum (actMap x f) (actMap y f))
Можем ли мы теперь добавить умножение? Конечно, может!
mkProd x y = Actions
(actEval x * actEval y)
(\f -> mkProd (actMap x f) (actMap y f))
О, но подождите - мы забыли добавить действие actShow
раньше, добавим, что в, мы просто... errh, хорошо.
Во всяком случае, как это выглядит, как использовать два разных стиля?
expr1plus1 = Sum (Lit 1) (Lit 1)
action1plus1 = mkSum (mkLit 1) (mkLit 1)
action1times1 = mkProd (mkLit 1) (mkLit 1)
В то же время, когда вы не расширяете их.
В качестве интересной заметки обратите внимание, что в стиле "действия" фактические значения в выражении полностью скрыты - поле actEval
только promises, чтобы дать нам что-то вроде правильного типа, как оно обеспечивает это его собственный бизнес. Благодаря ленивой оценке содержимое поля может быть даже сложным вычислением, которое выполняется только по требованию. Значение Actions a
полностью непрозрачно для внешнего контроля, представляя только определенные действия для внешнего мира.
Этот стиль программирования - замена простых данных пакетом "действий", скрывая фактические данные реализации в черном ящике, используя конструкторные функции для создания новых битов данных, способных обмениваться очень разными "значениями", с тем же набором "действий" и т.д. - интересно. Вероятно, это имя, но я не могу вспомнить...
Ответ 2
Я слышал эту жалобу более чем несколько раз, и это меня всегда смущает. Ответчик писал:
В функциональном программировании что добавление новых свойств данных требует много изменений кода, потому что соответствия шаблонов и данных конструкторы (Haskell, ML).
Но это по большому счету особенность, а не ошибка! Например, когда вы изменяете возможности в одном из вариантов, код, который обращается к этому варианту посредством сопоставления с образцом, вынужден учитывать тот факт, что появились новые возможности. Это полезно, потому что действительно вам нужно подумать, нужно ли изменить этот код, чтобы реагировать на семантические изменения в типах, которыми он управляет.
Я бы поспорил с утверждением, что требуется "много изменений кода". С хорошо написанным кодом система типов обычно делает впечатляюще хорошую работу по выведению на передний план кода, который необходимо учитывать, и не намного больше.
Возможно, проблема заключается в том, что трудно ответить на вопрос без более конкретного примера. Подумайте о том, чтобы предоставить часть кода в Haskell или ML, что вы не знаете, как развиваться чисто. Думаю, вы получите более точные и полезные ответы таким образом.
Ответ 3
Этот компромисс известен в литературе по теории программирования как проблема выражения:
Цель состоит в том, чтобы определить тип данных по случаям, где можно добавить новые случаи к типу данных и новым функциям над типом данных без перекомпиляции существующего кода и при сохранении безопасности статического типа (например, без приведения).
Решения были выдвинуты, но я их не изучал. (Большое обсуждение в Lambda The Ultimate.)
Ответ 4
В Haskell, по крайней мере, я бы сделал абстрактный тип данных. Это создать тип, который не экспортирует конструкторы. Пользователи этого типа теряют возможность сопоставления шаблонов по типу, и вам необходимо предоставить функции для работы с типом. Взамен вы получаете тип, который легче изменить без изменения кода, написанного пользователями этого типа.
Ответ 5
Если новые данные не подразумевают никакого нового поведения, как в приложении, где нас просят добавить поле "дата рождения" к ресурсу "человек", а затем все, что нам нужно сделать, это добавить его в список полей, которые являются частью ресурса человека, тогда его легко решить как в функциональном, так и в ООП мире. Просто не рассматривайте "дату рождения" как часть вашего кода; это просто часть ваших данных.
Позвольте мне объяснить: если дата рождения является чем-то, что подразумевает другое поведение приложения, например. что мы делаем что-то по-другому, если человек несовершеннолетний, то в ООП мы добавим поле рождения в класс человека, а в FP мы добавим аналогичное поле родительской записи в структуру данных человека.
Если нет никакого поведения, связанного с "birthdate", тогда в коде не должно быть поля с именем "birthdate". Структура данных, такая как словарь (карта), будет содержать различные поля. Добавление нового не потребует никаких изменений в программе, независимо от того, вы ли это OOP или FP. Оценки будут добавлены аналогичным образом, добавив регулярное выражение проверки или используя аналогичный малый язык для проверки, чтобы выразить в данных, каково должно быть поведение проверки.