如何将OO程序重构为功能性程序?


26

我很难找到有关如何以功能样式编写程序的资源。我可以在网上找到讨论的最高级的话题是使用结构化类型来减少类层次结构。大多数只处理如何使用map / fold / reduce / etc替换命令式循环。

我真正想找到的是深入讨论非平凡程序的OOP实现,其局限性以及如何以功能样式重构它。不仅是算法或数据结构,还有一些具有不同角色和方面的东西-也许是电子游戏。顺便说一句,我确实读过Tomas Petricek撰写的《现实世界的函数编程》,但是我还想要更多。


6
我认为这是不可能的。您必须重新设计(并重写)所有内容。
布赖恩·陈

18
-1,此文章是由错误的假设所致,即OOP和功能样式是相反的。这些大多是正交的概念,恕我直言,这不是一个神话。“功能性”与“过程性”相对,并且两种样式都可以与OOP结合使用。
布朗

11
@ DocBrown,OOP过于依赖可变状态。无状态对象不太适合当前的OOP设计实践。
SK-logic

9
@ SK-logic:密钥不是无状态对象,而是不可变对象。即使对象是可变的,只要在给定的上下文中不更改它们,它们通常也可以在系统的功能部分中使用。此外,我想您知道对象和闭包是可以互换的。因此,这一切都表明OOP和“功能性”并不矛盾。
布朗

12
@DocBrown:我认为语言结构是正交的,而思维方式往往会发生冲突。OOP人们倾向于问“对象是什么以及它们如何协作?”;机能正常的人们往往会问:“我的数据是什么,我想如何转换它?”。这些不是相同的问题,它们导致不同的答案。我也认为您误解了问题。不是“ OOP流口水和FP规则,我如何摆脱OOP?”,而是“我得到OOP而我没有FP,是否有办法将OOP程序转换为功能性程序,所以我可以一些见识?”。
迈克尔·肖

Answers:


31

函数式程序设计的定义

Clojure的喜悦简介介绍如下:

函数式编程是具有不确定定义的那些计算术语之一。如果您问100个程序员的定义,您可能会收到100个不同的答案...

功能编程关注并促进功能的应用和组成。为了使语言被视为功能语言,其功能概念必须是一流的。一流的函数可以像其他任何数据一样进行存储,传递和返回。除此核心概念外,[FP的定义可能包括]纯度,不变性,递归,惰性和参照透明性。

在Scala 2nd Edition中编程。10具有以下定义:

函数式编程以两个主要思想为指导。第一个想法是函数是一流的值...您可以将函数作为参数传递给其他函数,将它们作为函数的结果返回或存储在变量中...

函数式编程的第二个主要思想是程序的操作应将输入值映射到输出值,而不是就地更改数据。

如果我们接受第一个定义,那么使代码“起作用”的唯一要做的就是将循环内翻。第二个定义包括不变性。

一流的功能

想象一下,当前您从公共汽车对象中获得了一个乘客名单,然后对其进行迭代,从而使每个乘客的银行帐户减少了公共汽车票价的金额。执行相同操作的功能性方法是在Bus上有一个方法,该方法可能称为forEachPassenger,它具有一个参数的功能。然后,Bus会对其乘客进行迭代,但这是最好的方法,并且将向您收取车费的客户代码放入函数中,并传递给forEachPassenger。瞧!您正在使用函数式编程。

当务之急:

for (Passenger p : Bus.getPassengers()) {
    p.debit(fare);
}

功能(在Scala中使用匿名函数或“ lambda”):

myBus = myBus.forEachPassenger(p:Passenger -> { p.debit(fare) })

更多含糖的Scala版本:

myBus = myBus.forEachPassenger(_.debit(fare))

非一流的功能

如果您的语言不支持一流的功能,这可能会很丑陋。在Java 7或更早版本中,您必须提供一个“功能对象”接口,如下所示:

// Java 8 has java.util.function.Consumer, but in earlier
// versions you have to roll your own:
public interface Consumer<T> {
    public void accept(T t);
}

然后,Bus类提供一个内部迭代器:

public void forEachPassenger(Consumer<Passenger> c) {
    for (Passenger p : passengers) {
        c.accept(p);
    }
}

最后,将一个匿名函数对象传递给总线:

// Java 8 has syntactic sugar to make this look more like
// the Scala solution, but earlier versions require manually
// instantiating a "Function Object," in this case, a
// Consumer:
Bus.forEachPassenger(new Consumer<Passenger>() {
    @Override
    public void accept(final Passenger p) {
        p.debit(fare);
    }
}

Java 8允许在匿名函数的范围内捕获局部变量,但是在早期版本中,任何此类变量都必须声明为final。为了解决这个问题,您可能需要制作一个MutableReference包装器类。这是一个特定于整数的类,可让您向上述代码添加循环计数器:

public static class MutableIntWrapper {
    private int i;
    private MutableIntWrapper(int in) { i = in; }
    public static MutableIntWrapper ofZero() {
        return new MutableIntWrapper(0);
    }
    public int value() { return i; }
    public void increment() { i++; }
}

final MutableIntWrapper count = MutableIntWrapper.ofZero();
Bus.forEachPassenger(new Consumer<Passenger>() {
    @Override
    public void accept(final Passenger p) {
        p.debit(fare);
        count.increment();
    }
}

System.out.println(count.value());

即使很丑陋,通过提供一个内部迭代器,有时仍然可以消除遍及整个程序的循环中复杂且重复的逻辑。

Java 8中已修复了此丑陋问题,但在一流函数中处理检查的异常仍然非常丑陋,并且Java仍在其所有集合中都保留了可变性的假设。这将我们带到通常与FP相关的其他目标:

不变性

乔什·布洛赫(Josh Bloch)的第13项是“首选不变性”。尽管常见的垃圾谈话与此相反,但是OOP可以用不可变的对象完成,并且这样做会更好。例如,Java中的String是不可变的。StringBuffer,OTOH需要是可变的,以构建不可变的String。有些任务(例如使用缓冲区)固有地需要可变性。

纯度

每个函数至少应该是可记忆的-如果您给它提供相同的输入参数(并且除了它的实际参数外,它应该没有输入),它每次都应产生相同的输出而不会引起“副作用”,例如更改全局状态,执行I / O或引发异常。

有人说,在函数式编程中,“通常需要一些邪恶才能完成工作。” 100%的纯度通常不是目标。减少副作用是。

结论

确实,在上述所有想法中,就简化我的代码(无论是OOP还是FP)而言,不变性是实际应用中最大的胜利。将函数传递给迭代器是第二大胜利。在的Java 8 Lambda表达式的文档有为什么最好的解释。递归对于处理树非常有用。懒惰使您可以处理无限的集合。

如果您喜欢JVM,建议您看一下Scala和Clojure。两者都是对函数式编程的深刻理解。Scala具有类似于C的语法,是类型安全的,尽管它与Haskell的语法确实与C具有相同的语法。Clojure不是类型安全的,它是Lisp。我最近发布了Java,Scala和Clojure关于一个特定重构问题的比较。 洛根·坎贝尔(Logan Campbell)使用生命游戏进行的比较还包括Haskell和键入的Clojure。

聚苯乙烯

吉米·霍法(Jimmy Hoffa)指出,我的巴士课程是可变的。我认为这将证明该问题的重构方式,而不是解决原始问题。可以通过以下方法解决此问题:将“公共汽车”上的每种方法设为工厂以生产新的公共汽车,将“乘客”上的每种方法设为工厂以生产新的乘客。因此,我向所有内容添加了返回类型,这意味着我将复制Java 8的java.util.function.Function而不是Consumer接口:

public interface Function<T,R> {
    public R apply(T t);
    // Note: I'm leaving out Java 8's compose() method here for simplicity
}

然后在巴士上:

public Bus mapPassengers(Function<Passenger,Passenger> c) {
    // I have to use a mutable collection internally because Java
    // does not have immutable collections that return modified copies
    // of themselves the way the Clojure and Scala collections do.
    List<Passenger> newPassengers = new ArrayList(passengers.size());
    for (Passenger p : passengers) {
        newPassengers.add(c.apply(p));
    }
    return Bus.of(driver, Collections.unmodifiableList(passengers));
}

最后,匿名函数对象返回事物的修改状态(带有新乘客的新公共汽车)。假设p.debit()现在返回的新不变乘客的钱比原始人少:

Bus b = b.mapPassengers(new Function<Passenger,Passenger>() {
    @Override
    public Passenger apply(final Passenger p) {
        return p.debit(fare);
    }
}

希望您现在可以自行决定要使用命令式语言的功能,并决定使用功能性语言重新设计项目是否会更好。在Scala或Clojure中,集合和其他API旨在简化函数式编程。两者都有很好的Java互操作性,因此您可以混合和匹配语言。实际上,为了实现Java互操作性,Scala将其第一类函数编译为几乎与Java 8功能接口兼容的匿名类。您可以在深度部分的Scala中阅读有关详细信息1.3.2


我感谢此答案中的努力,组织和清晰的沟通;但是我必须对某些技术问题稍加考虑。顶部附近提到的关键之一是功能的组合,这又回到了为什么在对象内部大量封装功能不能达到目的的原因:如果一个功能在对象内部,则必须在该对象上起作用。如果它作用于该对象,则必须更改其内部。现在,我原谅并不是每个人都需要引用透明性或不变性,但是如果它更改了对象的位置,则不再需要返回它
Jimmy Hoffa 2013年

而且一旦函数不返回值,该函数突然就无法与其他函数组成,并且您将失去所有对函数组成的抽象。您可以让函数在适当的位置更改对象然后返回对象,但是如果这样做,为什么不仅仅让函数将对象作为参数并将其从父对象的范围中释放出来呢?从父对象中释放出来后,它也可以在其他类型上工作,这是您缺少的FP的另一个重要部分:类型抽象。您的forEachPasenger仅对乘客有效...
Jimmy Hoffa

1
您抽象要映射和简化的事物的原因以及这些函数未绑定到包含对象的原因是,它们可以通过参数多态性用于多种类型。真正定义FP并推动其具有价值的是这些在OOP语言中找不到的各种抽象的泛滥。这不是懒惰,引用透明,不变性,甚至HM型系统中需要创建FP,那些东西都是创造旨意功能性成分的语言相当的副作用,其中功能可以抽象过的类型一般
吉米·霍法

@JimmyHoffa您对我的榜样提出了非常公正的批评。Java8 Consumer接口使我着迷于可变性。另外,FP的chouser / fogus定义不包含不变性,我稍后添加了Odersky / Spoon / Venners定义。我离开了原始示例,但在底部的“ PS”部分下添加了一个新的,不变的版本。它很丑。但是我认为它演示了作用于对象以产生新对象的功能,而不是改变原始内部的功能。很棒的评论!
2013年


12

我有个人经验“完成”这一点。最后,我没有想到纯粹是功能性的东西,但是我想出了我满意的东西。这是我的操作方式:

  • 将所有外部状态转换为函数的参数。EG:如果修改了对象的方法x,则对其进行修改,以使该方法被传递x而不是调用this.x
  • 删除对象的行为。
    1. 使对象的数据可公开访问
    2. 将所有方法转换为对象调用的函数。
    3. 让调用该对象的客户端代码通过传入对象数据来调用新函数。EG:转换 x.methodThatModifiesTheFooVar()fooFn(x.foo)
    4. 从对象中删除原始方法
  • 许多迭代循环,你可以替换高阶功能,如mapreducefilter,等。

我无法摆脱可变状态。在我的语言(JavaScript)中,这太不习惯了。但是,通过传递和/或返回所有状态,可以测试每个功能。这与OOP不同,在OOP中,建立状态将花费很长时间,或者分离依赖项通常需要先修改生产代码。

同样,我可能对定义有误,但我认为我的函数是参照透明的:给定相同的输入,我的函数将具有相同的效果。

编辑

正如您在此处看到的那样,不可能在JavaScript中创建真正的不可变对象。如果您很努力并且可以控制谁调用您的代码,则可以通过始终创建一个新对象而不是更改当前对象来做到这一点。这对我来说不值得。

但是,如果您使用的是Java ,则可以使用这些技术使您的类不可变。


+1取决于您到底想做什么,这可能是您可以真正做到的,而无需进行超出“重构”范围的设计更改。
Evicatos

@Evicatos:我不知道,如果JavaScript对不可变状态有更好的支持,我认为我的解决方案将像使用Clojure这样的动态功能语言一样具有功能。除了重构之外,还需要其他东西的例子是什么?
Daniel Kaplan 2013年

我认为摆脱不稳定状态是有资格的。我认为这不仅仅是在语言中获得更好支持的问题,我认为从可变变为不可变基本上将始终需要基本的架构更改,这些更改本质上构成了重写。Ymmv取决于您对重构的定义。
Evicatos

@Evicatos看到我的编辑
Daniel Kaplan

1
@tieTYT是的,对于JS如此可变,这是可悲的,但是至少Clojure可以编译为JavaScript:github.com/clojure/clojurescript
GlenPeterson 2013年

3

我认为完全不可能完全重构该程序-您必须在正确的范例中重新设计和重新实现。

我已经看到代码重构被定义为“一种用于重组现有代码主体,改变其内部结构而不改变其外部行为的纪律技术”。

您可以使某些功能更实用,但是从本质上讲,您仍然有一个面向对象的程序。您不能仅仅改变一些零散的部分以使其适应不同的范例。


我要补充一点,一个很好的第一个标记是争取参照透明。一旦有了这些,您将获得功能编程的50%的好处。
Daniel Gratzer 2013年

3

我认为这一系列文章正是您想要的:

纯功能复古游戏

http://prog21.dadgum.com/23.html 第1部分

http://prog21.dadgum.com/24.html 第2部分

http://prog21.dadgum.com/25.html 第3部分

http://prog21.dadgum.com/26.html 第4部分

http://prog21.dadgum.com/37.html 后续行动

摘要是:

作者提出了一个带有副作用的主循环(副作用必须发生在某个地方,对吗?),大多数函数返回小的不变记录,详细记录了它们如何改变游戏状态。

当然,在编写实际程序时,您会混合并匹配几种编程样式,并在每种样式中最有帮助。但是,尝试以最实用/不变的方式编写程序,并以仅使用全局变量:-)的最意大利面条的方式编写程序,这是一种很好的学习经验(请作为实验,而不是在生产中进行)


2

由于OOP和FP有两种相反的组织代码的方法,因此您可能必须将所有代码全部翻过来。

OOP围绕类型(类)组织代码:不同的类可以实现相同的操作(具有相同签名的方法)。结果,当操作集变化不大而可以非常频繁地添加新类型时,OOP更合适。例如,考虑其中每个插件具有固定的一套方法的GUI库(hide()show()paint()move(),等等),但作为库被扩展,可以添加新的组件。在OOP中,很容易添加一个新类型(对于给定的接口):您只需要添加一个新类并实现其所有方法(更改本地代码)即可。另一方面,向接口添加新的操作(方法)可能需要更改实现该接口的所有类(即使继承可以减少工作量)。

FP围绕操作(功能)组织代码:每个功能实现一些可以以不同方式处理不同类型的操作。通常,这是通过模式匹配或其他机制对类型进行分派来实现的。因此,当类型集稳定并且更频繁地添加新操作时,FP更适用。例如,以一组固定的图像格式(GIF,JPEG等)和要实现的一些算法为例。每种算法都可以通过根据图像类型表现不同的功能来实现。添加新算法很容易,因为您只需要实现一个新功能(更改本地代码)即可。添加新格式(类型)需要修改您到目前为止已实现的所有功能以支持它(非本地更改)。

底线:OOP和FP在组织代码的方式上根本不同,将OOP设计更改为FP设计将涉及更改所有代码以反映这一点。但是,这可能是一个有趣的练习。另请参见迈克梅(Mikemay)引用的SICP书的这些讲义,特别是幻灯片13.1.5至13.1.10。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.