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