代码之家  ›  专栏  ›  技术社区  ›  Adam Gent

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

  •  17
  • Adam Gent  · 技术社区  · 16 年前

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

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

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

    如何将此问题最小化?

    这似乎是公认的问题 Xavier Leroy states nicely on page 24 of "Objects and Classes vs. Modules" -为了总结那些没有PostScript浏览器的人,它基本上说 对于在数据对象上添加新行为,FP语言优于OOP语言,但是OOP语言更适合添加新数据对象/属性。

    在FP语言中有没有任何设计模式可以帮助缓解这个问题?

    我读过菲利普·韦德勒的 recommendation of using Monads 帮助解决这个模块化问题,但我不确定我如何理解?

    5 回复  |  直到 12 年前
        1
  •  22
  •   C. A. McCann Ravikant Cherukuri    16 年前

    AS 大流士培根 注意到,这本质上是表达问题,一个长期存在的问题,没有普遍接受的解决方案。然而,缺乏两全其美的方法并不能阻止我们有时想走一条或另一条路。现在,你要求 “功能语言的设计模式” 我们来试试看。下面的示例是用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 Field只承诺给我们提供正确类型的东西,它是如何提供它自己的业务的。由于懒惰的评估,该字段的内容甚至可能是一个精细的计算,只在需要时执行。安 Actions a 价值对于外部检查是完全不透明的,只向外部世界呈现定义的行为。

    这种编程风格——将简单的数据替换为一束“操作”,同时将实际的实现细节隐藏在一个黑盒中,使用类似构造函数的函数来构建新的数据位,能够用相同的“操作”集交换非常不同的“值”,等等——非常有趣。可能有个名字,但我不太记得…

        2
  •  6
  •   zrr    16 年前

    我听过这个抱怨不止几次,总是让我困惑。提问者写道:

    在函数式编程中 添加新的数据属性 需要大量的代码更改,因为 模式匹配和数据 施工人员(Haskell,ML)。

    但这大体上是一个特性,而不是一个bug!例如,当您更改变量中的可能性时,通过模式匹配访问该变量的代码将被迫考虑出现新可能性的事实。这是很有用的,因为实际上,您确实需要考虑代码是否需要更改以响应它所处理的类型中的语义更改。

    我会反驳这种说法,即需要“大量的代码更改”。对于编写良好的代码,类型系统通常能够突出需要考虑的代码,而不是更多。

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

        3
  •  4
  •   Darius Bacon    16 年前

    这种权衡在编程语言理论文献中被称为 expression problem :

    其目标是按事例定义数据类型,在这种情况下,可以在数据类型上添加新事例和新函数,而不必重新编译现有代码,同时保持静态类型安全(例如,不强制转换)。

    有人提出了解决办法,但我没有研究过。(很多讨论 at Lambda The Ultimate )

        4
  •  3
  •   stonemetal    16 年前

    在haskell中,至少我会创建一个抽象数据类型。即创建不导出构造函数的类型。该类型的用户缺乏对该类型进行模式匹配的能力,您必须提供使用该类型的函数。作为回报,您可以得到一个更容易修改的类型,而无需更改该类型的用户编写的代码。

        5
  •  0
  •   xpmatteo    12 年前

    如果新的数据意味着没有新的行为,比如在一个应用程序中,我们被要求将“生日”字段添加到“个人”资源中,然后我们所要做的就是将其添加到作为个人资源一部分的字段列表中,那么在功能和OOP世界中都很容易解决。不要把“生日”作为代码的一部分,它只是数据的一部分。

    让我解释一下:如果生日意味着一种不同的应用行为,例如,如果该人未成年,我们会做一些不同的事情,那么在OOP中,我们会向Person类添加一个生日字段,在FP中,我们会向Person数据结构添加一个类似的生日字段。

    如果“生日”没有附加的行为,那么代码中不应该有名为“生日”的字段。字典(地图)等数据结构将保存各种字段。添加一个新的将不需要程序更改,无论您是OOP还是FP。通过附加验证regexp或使用类似的验证小语言来表示 在数据中 验证行为应该是什么。

    推荐文章