我很难找到有关如何以功能样式编写程序的资源。我可以在网上找到讨论的最高级的话题是使用结构化类型来减少类层次结构。大多数只处理如何使用map / fold / reduce / etc替换命令式循环。
我真正想找到的是深入讨论非平凡程序的OOP实现,其局限性以及如何以功能样式重构它。不仅是算法或数据结构,还有一些具有不同角色和方面的东西-也许是电子游戏。顺便说一句,我确实读过Tomas Petricek撰写的《现实世界的函数编程》,但是我还想要更多。
我很难找到有关如何以功能样式编写程序的资源。我可以在网上找到讨论的最高级的话题是使用结构化类型来减少类层次结构。大多数只处理如何使用map / fold / reduce / etc替换命令式循环。
我真正想找到的是深入讨论非平凡程序的OOP实现,其局限性以及如何以功能样式重构它。不仅是算法或数据结构,还有一些具有不同角色和方面的东西-也许是电子游戏。顺便说一句,我确实读过Tomas Petricek撰写的《现实世界的函数编程》,但是我还想要更多。
Answers:
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。
我有个人经验“完成”这一点。最后,我没有想到纯粹是功能性的东西,但是我想出了我满意的东西。这是我的操作方式:
x
,则对其进行修改,以使该方法被传递x
而不是调用this.x
。x.methodThatModifiesTheFooVar()
成fooFn(x.foo)
map
,reduce
,filter
,等。我无法摆脱可变状态。在我的语言(JavaScript)中,这太不习惯了。但是,通过传递和/或返回所有状态,可以测试每个功能。这与OOP不同,在OOP中,建立状态将花费很长时间,或者分离依赖项通常需要先修改生产代码。
同样,我可能对定义有误,但我认为我的函数是参照透明的:给定相同的输入,我的函数将具有相同的效果。
正如您在此处看到的那样,不可能在JavaScript中创建真正的不可变对象。如果您很努力并且可以控制谁调用您的代码,则可以通过始终创建一个新对象而不是更改当前对象来做到这一点。这对我来说不值得。
但是,如果您使用的是Java ,则可以使用这些技术使您的类不可变。
我认为完全不可能完全重构该程序-您必须在正确的范例中重新设计和重新实现。
我已经看到代码重构被定义为“一种用于重组现有代码主体,改变其内部结构而不改变其外部行为的纪律技术”。
您可以使某些功能更实用,但是从本质上讲,您仍然有一个面向对象的程序。您不能仅仅改变一些零散的部分以使其适应不同的范例。
我认为这一系列文章正是您想要的:
纯功能复古游戏
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 后续行动
摘要是:
作者提出了一个带有副作用的主循环(副作用必须发生在某个地方,对吗?),大多数函数返回小的不变记录,详细记录了它们如何改变游戏状态。
当然,在编写实际程序时,您会混合并匹配几种编程样式,并在每种样式中最有帮助。但是,尝试以最实用/不变的方式编写程序,并以仅使用全局变量:-)的最意大利面条的方式编写程序,这是一种很好的学习经验(请作为实验,而不是在生产中进行)
由于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。