处理函数式编程中的增量数据建模更改

Ada*_*ent 17 oop ocaml haskell functional-programming data-modeling

在我作为开发人员的工作中我必须解决的大多数问题都与数据建模有关.例如,在OOP Web应用程序世界中,我经常需要更改对象中的数据属性以满足新的要求.

如果我很幸运,我甚至不需要以编程方式添加新的"行为"代码(函数,方法).相反,我可以通过注释属性(Java)来声明添加验证甚至UI选项.

在函数式编程中,由于模式匹配和数据构造函数(Haskell,ML),添加新数据属性似乎需要大量代码更改.

如何最大限度地减少此问题?

这似乎是一个公认的问题,因为Xavier Leroy在"对象和类与模块"的第24页很好地说明 - 总结那些没有PostScript查看器的人,它基本上说FP语言比OOP语言更好地添加新的对数据对象的行为,但OOP语言更适合添加新的数据对象/属性.

FP语言中是否有任何设计模式可以帮助缓解此问题?

我已经阅读了Phillip Wadler 建议使用Monads来帮助解决这个模块化问题,但我不确定我是怎么理解的?

C. *_*ann 22

正如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, ")"]
Run Code Online (Sandbox Code Playgroud)

这表示简单的数学表达式,仅包含文字值和加法.通过我们在这里的功能,我们可以采用表达式并对其进行评估,或将其显示为String.现在,假设我们要添加一个新函数 - 比如,将函数映射到所有文字值:

exprMap f (Lit x) = Lit (f x)
exprMap f (Sum x y) = Sum (exprMap f x) (exprMap f y)
Run Code Online (Sandbox Code Playgroud)

简单!我们可以整天写作功能而不会出汗!代数数据类型真棒!

事实上,它们太棒了,我们希望让我们的表达类型更多,更有表现力.让我们扩展它以支持乘法,我们只是......呃......亲爱的,那会很尴尬,不是吗?我们必须修改我们刚写的每个函数.绝望!

事实上,扩展表达式本身比添加使用它们的函数更有趣.所以,让我们说我们愿意在另一个方向进行权衡.我们怎么可能这样做?

好吧,没有意义中途做事.让我们上传所有内容并反转整个程序.那是什么意思?那么,这是函数式编程,还有什么比高阶函数更有用?我们要做的是将表示表达式值的数据类型替换为表示表达式上的操作的数据类型.我们不需要选择构造函数,而是需要记录所有可能的操作,如下所示:

data Actions a = Actions {
    actEval :: a,
    actMap  :: (a -> a) -> Actions a }
Run Code Online (Sandbox Code Playgroud)

那么我们如何创建一个没有数据类型的表达式呢?好吧,我们的功能现在是数据,所以我想我们的数据需要是函数.我们将使用常规函数创建"构造函数",返回操作记录:

mkLit x = Actions x (\f -> mkLit (f x))

mkSum x y = Actions 
    (actEval x + actEval y) 
    (\f -> mkSum (actMap x f) (actMap y f))
Run Code Online (Sandbox Code Playgroud)

我们现在可以更容易地添加乘法吗?当然可以!

mkProd x y = Actions 
    (actEval x * actEval y) 
    (\f -> mkProd (actMap x f) (actMap y f))
Run Code Online (Sandbox Code Playgroud)

哦,但是等等 - 我们忘了actShow早点添加一个动作,让我们补充说,我们只是......呃,好吧.

无论如何,使用两种不同风格的样子是什么样的?

expr1plus1 = Sum (Lit 1) (Lit 1)
action1plus1 = mkSum (mkLit 1) (mkLit 1)
action1times1 = mkProd (mkLit 1) (mkLit 1)
Run Code Online (Sandbox Code Playgroud)

当你没有扩展时,几乎一样.

作为一个非常有意思的是,考虑到在"行动"的风格,在表达的实际值完全隐藏 --the actEval场只承诺给我们正确类型的东西,它如何提供它自己的业务.由于延迟评估,该字段的内容甚至可以是精细的计算,仅在需要时执行.的Actions a值是完全不透明的外部检查,仅呈现所述定义的操作向外部世界.

这种编程风格 - 以"行动"捆绑代替简单的数据,而藏在黑盒子的实际实现细节,使用构造相似的功能来构建新的数据位,能够用相同的互换非常不同的"价值"一系列"行动",等等 - 很有趣.它可能有一个名字,但我似乎无法回想起......

  • @Adam Gent:我还应该注意到我的第二个例子是为了突出对称性而构建的,并展示了Haskell中表达式问题的典型OOP结尾.实际上在Haskell中使用这种风格有点痛苦,部分原因是因为Haskell糟糕的记录系统.但是,出于实际目的,一般观点认为:不要假设您应该用代数数据类型替换对象,而是考虑部分应用的高阶函数来封装对象的用途. (3认同)
  • @Adam Gent:有趣的讽刺是,对于一个"对象"作为一组方法 - 就像我的回答中的"动作a"一样 - 在OOP中进行日常编程会使*远*更广泛使用闭包和高阶函数而不是FP中常见的函数,尤其是强调模式匹配和代数数据类型的ML族语言. (2认同)

zrr*_*zrr 6

我听过这个抱怨多次了,总是让我感到困惑.提问者写道:

在函数式编程中,由于模式匹配和数据构造函数(Haskell,ML),添加新数据属性似乎需要大量代码更改.

但这基本上是一个功能,而不是一个错误!例如,当您更改变体中的可能性时,通过模式匹配访问该变体的代码将被迫考虑新的可能性出现的事实.这很有用,因为实际上您确实需要考虑该代码是否需要更改以对其操作的类型中的语义更改做出反应.

我认为需要"大量代码更改"的说法.通过编写良好的代码,类型系统通常能够很好地突出需要考虑的代码,而不是更多.

也许这里的问题是,如果没有更具体的例子,很难回答这个问题.考虑在Haskell或ML中提供一段代码,你不确定如何干净地进化.我想你会以这种方式获得更精确和有用的答案.


Dar*_*con 5

这种权衡在编程语言理论文献中被称为表达式问题

目标是按情况定义数据类型,其中可以向数据类型添加新情况并在数据类型上添加新函数,而无需重新编译现有代码,同时保留静态类型安全性(例如,无强制转换)。

解决方案已经提出了,但我没有研究过。(Lambda The Ultimate 上有很多讨论。)