处理函数式编程中增量数据建模的变化 [英] Handling incremental Data Modeling Changes in Functional Programming

查看:124
本文介绍了处理函数式编程中增量数据建模的变化的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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



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

在函数式编程中,添加新数据属性似乎需要很多代码更改,因为模式匹配和数据构造器(Haskell,ML)。

我如何最小化这个问题?

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

有没有FP语言中使用的设计模式有助于缓解这个问题? 我已阅读Phillip Wadler的使用Monads的建议有助于解决这个模块化问题,但我不确定自己的理解如何?

解决方案

正如 Darius Bacon 所述,这实质上是表达问题,一个长期存在的问题,没有univ完全接受的解决方案。然而,缺乏两全其美的方法并不能阻止我们有时想走一条路或另一条路。现在,您要求提供功能语言的设计模式,所以让我们试试看。下面的例子是用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 。现在,假设我们想添加一个新的函数 - 比如说,将函数映射到所有的字面值上:
$ b $ pre $ exp $ c $ f $ (x)= Lit(fx)
exprMap f(Sum xy)= Sum(exprMap fx)(exprMap fy)

轻松!我们可以整天不停地写作,而不会冒汗!代数数据类型真棒!



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



事实上,扩展表达式本身比添加使用它们的函数更有趣。所以,假设我们愿意在另一个方向进行权衡。我们如何做到这一点?



好吧,半途而废。让我们将所有内容上传完毕,然后转换整个程序。这是什么意思?那么,这是函数式编程,还有什么比高阶函数更有用?我们要做的是将表示表达式值的数据类型替换为表达上的动作的数据类型。

 数据操作a =操作{ 
actEval :: a,
actMap ::(a - > a) - >动作a}

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

  mkLit x =动作x(\f - > mkLit(fx))

mkSum xy =动作
(actEval x + actEval y)
(\f - > mkSum(actMap xf)(actMap yf ))

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

  mkProd xy =动作
(actEval x * actEval y)
(\f - > mkProd(actMap xf)(actMap yf))

哦,但是等等 - 我们忘了要在之前添加一个 actShow 动作,让我们将其添加进去,我们只是... errh,好。

无论如何,它看起来像使用两种不同的样式?

  expr1plus1 =总和(点亮1)(点亮1)
action1plus1 = mkSum(mkLit 1)(mkLit 1)
action1times1 = mkProd(mkLit 1)(mkLit 1)

几乎相同,当你没有扩展它们时。



作为一个有趣的方面说明,考虑在动作风格,表达式中的实际值完全隐藏 - actEval 字段仅承诺给我们一些正确的类型,它提供的是它自己的业务。由于懒惰的评估,该领域的内容甚至可能是一个精细的计算,仅在需求时执行。一个操作一个值对外部检查是完全不透明的,只向外界显示定义的操作。

这种编程风格 - 用一堆动作替换简单的数据,同时将实际的实现细节隐藏在黑盒子中,使用类似构造函数的函数来构建新位的数据,能够将不同的值与同一组行为进行交换,等等 - 很有趣。可能有一个名字,但我似乎无法回想起......

Most of the problems I have to solve in my job as a developer have to do with data modeling. For example in a OOP Web Application world I often have to change the data properties that are in a object to meet new requirements.

If I'm lucky I don't even need to programmatically add new "behavior" code (functions,methods). Instead I can declarative add validation and even UI options by annotating the property (Java).

In Functional Programming it seems that adding new data properties requires lots of code changes because of pattern matching and data constructors (Haskell, ML).

How do I minimize this problem?

This seems to be a recognized problem as Xavier Leroy states nicely on page 24 of "Objects and Classes vs. Modules" - To summarize for those that don't have a PostScript viewer it basically says FP languages are better than OOP languages for adding new behavior over data objects but OOP languages are better for adding new data objects/properties.

Are there any design pattern used in FP languages to help mitigate this problem?

I have read Phillip Wadler's recommendation of using Monads to help this modularity problem but I'm not sure I understand how?

解决方案

As Darius Bacon noted, this is essentially the expression problem, a long-standing issue with no universally accepted solution. The lack of a best-of-both-worlds approach doesn't stop us from sometimes wanting to go one way or the other, though. Now, you asked for a "design pattern for functional languages", so let's take a shot at it. The example that follows is written in Haskell, but isn't necessarily idiomatic for Haskell (or any other language).

First, a quick review of the "expression problem". Consider the following algebraic data type:

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, ")"]

This represents simple mathematical expressions, containing only literal values and addition. With the functions we have here, we can take an expression and evaluate it, or show it as a String. Now, say we want to add a new function--say, map a function over all the literal values:

exprMap f (Lit x) = Lit (f x)
exprMap f (Sum x y) = Sum (exprMap f x) (exprMap f y)

Easy! We can keep writing functions all day without breaking a sweat! Algebraic data types are awesome!

In fact, they're so awesome, we want to make our expression type more, errh, expressive. Let's extend it to support multiplication, we'll just... uhh... oh dear, that's going to be awkward, isn't it? We have to modify every function we just wrote. Despair!

In fact, maybe extending the expressions themselves is more interesting than adding functions that use them. So, let's say we're willing to make the trade-off in the other direction. How might we do that?

Well, no sense doing things halfway. Let's up-end everything and invert the whole program. What does that mean? Well, this is functional programming, and what's more functional than higher-order functions? What we'll do is replace the data type representing expression values with one representing actions on the expression. Instead of choosing a constructor we'll need a record of all possible actions, something like this:

data Actions a = Actions {
    actEval :: a,
    actMap  :: (a -> a) -> Actions a }

So how do we create an expression without a data type? Well, our functions are data now, so I guess our data needs to be functions. We'll make "constructors" using regular functions, returning a record of actions:

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

mkSum x y = Actions 
    (actEval x + actEval y) 
    (\f -> mkSum (actMap x f) (actMap y f))

Can we add multiplication more easily now? Sure can!

mkProd x y = Actions 
    (actEval x * actEval y) 
    (\f -> mkProd (actMap x f) (actMap y f))

Oh, but wait--we forgot to add an actShow action earlier, let's add that in, we'll just... errh, well.

At any rate, what does it look like to use the two different styles?

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

Pretty much the same, when you're not extending them.

As an interesting side note, consider that in the "actions" style, the actual values in the expression are completely hidden--the actEval field only promises to give us something of the correct type, how it provides it is its own business. Thanks to lazy evaluation, the contents of the field may even be an elaborate computation, performed only on demand. An Actions a value is completely opaque to external inspection, presenting only the defined actions to the outside world.

This programming style--replacing simple data with a bundle of "actions" while hiding the actual implementation details in a black box, using constructor-like functions to build new bits of data, being able to interchange very different "values" with the same set of "actions", and so on--is interesting. There's probably a name for it, but I can't quite seem to recall...

这篇关于处理函数式编程中增量数据建模的变化的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆