Обработка инкрементальных изменений моделирования данных в функциональном программировании - PullRequest
17 голосов
/ 11 мая 2010

Большинство проблем, которые мне приходится решать на работе разработчика, связано с моделированием данных. Например, в мире веб-приложений ООП мне часто приходится изменять свойства данных, которые находятся в объекте, чтобы соответствовать новым требованиям.

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

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

Как мне минимизировать эту проблему?

Кажется, это признанная проблема, так как Ксавье Леруа хорошо заявляет на странице 24 "Объекты и классы против модулей" - Подводя итог для тех, у кого нет средства просмотра PostScript, в основном говорится, что языки FP лучше, чем языки ООП, для добавления нового поведения над объектами данных, но языки ООП лучше для добавления новых объектов / свойств данных. 1013 * Существует ли какой-либо шаблон проектирования, используемый в языках FP для смягчения этой проблемы?

Я прочитал рекомендацию Филиппа Вадлера об использовании Монад , чтобы решить эту проблему модульности, но я не уверен, что понимаю, как?

Ответы [ 5 ]

22 голосов
/ 11 мая 2010

Как заметил Дариус Бэкон , это по сути проблема выражения, давняя проблема, не имеющая общепринятого решения. Отсутствие подхода «лучший из обоих миров» не мешает нам иногда хотеть идти тем или иным путем. Теперь вы запросили «шаблон проектирования для функциональных языков» , поэтому давайте попробуем. Следующий пример написан на 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)

Легко! Мы можем продолжать писать функции весь день, не потревожив! Алгебраические типы данных потрясающие!

На самом деле, они такие классные, что мы хотим сделать наш тип выражения более выразительным. Давайте расширим это, чтобы поддержать умножение, мы просто ... э-э ... о, это будет неловко, не так ли? Мы должны изменить каждую функцию, которую мы только что написали. Отчаяние!

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

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

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 ранее, давайте добавим это, мы просто ... эээ, хорошо.

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

expr1plus1 = Sum (Lit 1) (Lit 1)
action1plus1 = mkSum (mkLit 1) (mkLit 1)
action1times1 = mkProd (mkLit 1) (mkLit 1)

Примерно так же, когда вы их не расширяете.

В качестве интересного примечания отметим, что в стиле "actions" действительные значения в выражении полностью скрыты - поле actEval только обещает дать нам что-то правильного типа То, как это обеспечивает, это его собственное дело. Благодаря ленивой оценке содержимое поля может быть даже сложным вычислением, выполненным только по требованию. Значение Actions a полностью непрозрачно для внешнего осмотра, представляя только определенные действия для внешнего мира.

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

6 голосов
/ 11 мая 2010

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

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

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

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

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

4 голосов
/ 11 мая 2010

Этот компромисс известен в литературе по теории языка программирования как проблема выражения :

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

Решения были предложены, но я их не изучал. (Большая дискуссия в Lambda The Ultimate .)

3 голосов
/ 14 мая 2010

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

0 голосов
/ 02 февраля 2014

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

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

Если к «дата рождения» не привязано поведение, то в коде не должно быть поля с именем «дата рождения». Структура данных, такая как словарь (карта), будет содержать различные поля. Добавление нового не потребует изменений в программе, независимо от того, является ли это ООП или ФП. Валидации будут добавляться аналогичным образом, добавляя регулярное выражение проверки или используя подобный небольшой язык проверки для выражения в данных , каким должно быть поведение проверки.

...