哪些功能特性值得为它们带来的好处感到困惑?


13

在使用Haskell和F#学习了函数式编程之后,OOP范例似乎在类,接口和对象方面倒退了。我的同事可以理解FP的哪些方面?是否有任何FP风格值得与我的老板谈谈对我的团队进行再培训,以便我们可以使用它们?

FP的可能方面:

  • 不变性
  • 部分应用和咖喱
  • 一流的功能(功能指针/功能对象/策略模式)
  • 惰性评估(和Monad)
  • 纯功能(无副作用)
  • 表达式(相对于语句-每行代码都会产生一个值,而不是引起副作用,或者除了引起副作用之外)
  • 递归
  • 模式匹配

它是一种免费的所有人,我们可以在编程语言支持的范围内做任何事情吗?还是有更好的指南?


6
我也有类似的经历。经过大约2个月的痛苦,我开始在“映射到对象的东西”和“映射到函数的东西”之间找到了很好的平衡。使用支持两者的语言来进行一些严肃的黑客编程是有帮助的。最后,我的FP和OOP技能都得到了极大的提高
Daniel Gratzer

3
FWIW,Linq既是函数式的又是惰性的,您可以使用静态方法并避免状态持久化,从而在C#中模拟函数式编程。
罗伯特·哈维

1
此时,您应该阅读sicp。它是免费的,写得很好。它提供了两种范例之间的很好的比较。
西蒙·贝格

4
FP和OOP在某种意义上同时是正交的,在某种意义上是双重的。OOP与数据抽象有关,FP与(没有)副作用有关。是否有副作用与抽象数据的方式正交。Lambda微积分既是功能性的又是面向对象的。是的,FP通常使用抽象数据类型,而不使用对象,但是在不减少FP的情况下,也可以使用对象代替。OTOH,也有深刻的关系:功能同构于一个对象只有一个方法(这就是它们是如何“伪装”在Java中,并在实施Java8,...
约尔格W¯¯米塔格

3
我认为您问题的最强方面与可读性有关。“什么样的函数式编程风格才适合在面向对象的商店上班?” 或者什么功能特性值得OOP混淆,因为它们带来了好处。
GlenPeterson 2013年

Answers:


13

函数式编程与面向对象的编程是不同的范例(不同的思维方式和关于程序的不同思考方式)。您已经开始意识到,这里考虑问题及其解决方案的方式(面向对象)不止一种。还有其他的(想到程序和通用编程)。您如何应对这些新知识,是否接受和将这些新工具和方法整合到您的技能中,将决定您是否成长并成为更完整,更有技能的开发人员。

我们所有人都经过培训,能够处理一定水平的复杂性并感到满意。我喜欢称其为人的身高限制(从Watership Down,您可以算出多少)。扩大您的思维,考虑更多选项的能力以及拥有更多解决和解决问题的工具的能力是一件很棒的事情。但这是一个变化,它使您脱离舒适区。

您可能会遇到的一个问题是,您对跟随“一切都是对象”人群的满足感将降低。当您与可能不了解(或不想了解)为什么软件开发的功能性方法可以很好地解决某些问题的人员一起工作时,您可能需要提高耐心。就像通用编程方法可以很好地解决某些问题一样。

祝好运!


3
我想补充一点,当使用Haskell或Clojure等功能语言时,可以更好地理解一些传统的OOP概念。我个人意识到多态性确实是一个重要的概念(Java中的Interfaces或Haskell中的类型类),而继承(我认为是定义性概念)则是一种怪异的抽象。
wirrbel

6

函数式编程在日常的代码编写中提供了非常实用,务实的生产力:某些功能有利于简洁,这很棒,因为编写的代码越少,执行的故障就越少,所需的维护也越少。

作为一个数学家,我发现花哨的功能性东西很吸引人,但是在设计应用程序时通常很有用:这些结构可以在程序结构中编码程序的许多不变量,而不必用变量表示这些不变量。

我最喜欢的组合可能看起来微不足道,但是我认为它对生产率有很高的影响。这种组合是部分应用程序和Currying 函数以及一流的函数,我不会重新标记它们,因此再也不会写for循环:而是将循环体传递给迭代或映射函数。我最近被录用为C ++职位,但有趣的是,我完全失去了编写for循环的习惯

递归模式匹配的结合消除了该访问者设计模式的需要。只需比较一下您需要为布尔表达式的评估器编程的代码:在任何函数式编程语言中,这应该是大约15行代码,在OOP中,正确的做法是使用该Visitor设计模式,这将变成一个玩具示例。一篇广泛的论文。优势是显而易见的,我不感到任何不便。


2
我完全同意,但是我受到了业内人士的反对:他们知道他们的访客模式,他们已经看过并使用过很多次了,因此其中的代码是他们理解并熟悉的,尽管非常简单和容易,但另一种方法是外国的,因此对他们来说更困难。这是一个不幸的事实,因为业界已经经​​历了15年以上的OOP冲击每个程序员的脑海,因为他们记住十多年重复了100多行,所以100行以上的代码比10行更容易理解。
Jimmy Hoffa 2013年

1
-1-简洁的代码并不意味着您正在编写“更少”的代码。您正在使用更少的字符来编写相同的代码。如果有的话,您会犯更多的错误,因为(通常)很难阅读代码。
Telastyn

8
@Telastyn:简洁与不可读相同。同样,大量的ated肿样板有其自身的无法读取的方式。
迈克尔·肖

1
@Telastyn我想您只是在这里触及了真正的症结所在,是的,简洁可能不好并且不可读,readable肿也可能不好并且不可读,但是关键不是可变长度和令人费解的编写代码。正如您在上面提到的那样,关键在于操作数量,我不同意操作数量与可维护性无关,我认为减少工作量(使用清晰编写的代码)确实有助于提高可读性和可维护性。显然,使用单字母函数和变量名执行相同数量的操作将无济于事,好的FP需要相当少的操作(仍然要清楚地编写)
Jimmy Hoffa 2013年

2
@ user949300:如果您想要一个完全不同的示例,那么该Java 8示例如何呢?:list.forEach(System.out::println);从FP的角度来看,println是一个带有两个参数,一个目标PrintStream和一个值的函数,Object但是CollectionforEach方法期望一个仅带有一个参数的函数可以应用于每个元素。因此,第一个参数绑定到在System.out产生具有一个参数的新函数时发现的实例。它比BiConsumer<…> c=PrintStream::println; PrintStream a1=System.out; list.forEach(a2 -> c.accept(a1, a2));
Holger

5

您可能必须限制自己在工作中使用的知识的哪些部分,为了享受正常生活的好处,超人必须伪装成克拉克·肯特。但是,了解更多信息永远不会伤害您。就是说,函数式编程的某些方面适合于面向对象的商店,而其他方面可能值得与老板讨论,以便您可以提高商店的平均知识水平并因此编写更好的代码。

FP和OOP不互斥。看Scala。有些人认为这是最糟糕的,因为它是不纯FP,但出于同样的原因,有些人认为它是最好的。

以下是与OOP结合使用的一些方面:

  • 纯函数(无副作用)-我所知道的每种编程语言都支持此功能。它们使您的代码更容易理解,应在可行时使用。您不必将其称为FP。只是称其为良好的编码习惯。

  • 不变性:字符串可以说是最常用的Java对象,并且是不变的。我在博客上介绍了不可变的Java对象不可变的Java集合。其中一些可能适用于您。

  • 一流的功能(函数指针/功能对象/策略模式)-自1.1版以来,Java有了一个笨拙的变体版本,其中大多数API类(并且有数百种)实现了Listener接口。可运行的可能是最常用的功能对象。头等函数需要更多的工作来使用本机不支持它们的语言进行编码,但是当它们简化代码的其他方面时,有时值得付出额外的努力。

  • 递归对于处理树很有用。在OOP商店中,这可能是递归的主要适当用法。如果出于大多数OOP语言默认没有堆栈空间的原因而没有其他原因,那么最好在OOP中使用递归来娱乐,这是一个好主意。

  • 表达式(相对于语句-代码的每一行都会产生一个值,而不是引起副作用,或者除了引起副作用外)-C,C ++和Java中唯一的求值运算符是Ternary运算符。我在博客上讨论了适当的用法。您可能会发现自己编写了一些高度可重用和评估的简单函数。

  • 延迟评估(和Monad)-主要限于OOP中的延迟初始化。没有语言功能来支持它,您可能会发现一些有用的API,但是编写自己的API却很困难。而是最大程度地使用流-有关示例,请参见Writer和Reader接口。

  • 部分应用和咖喱化-没有一流的功能不切实际。

  • 模式匹配-在OOP中通常不建议使用。

总而言之,我认为工作不应该是万能的,您可以在其中完成编程语言所支持的任何工作,而该语言所能支持的极限。我认为您的同事的可读性应该是您对要出租的代码的试金石。我最想让您感到满足的地方是开始进行一些工作上的教育,以拓宽您的同事的视野。


自从学习FP以来,我习惯于将事物设计为具有流畅的界面,从而产生类似于表达式的功能,该函数具有一个执行一堆事情的语句。它是您真正获得的最接近的方法,但是当您发现您不再具有任何void方法时,这种方法自然就会从纯度中产生,使用C#中的静态扩展方法将大大地帮助您。这样,您唯一的观点就是我不同意的观点,其他一切都基于我自己学习FP和从事.NET日间工作的经验
Jimmy Hoffa 2013年

现在在C#中真正困扰我的是,出于两个简单的原因,我不能使用委托而不是单方法接口:1.您无法在没有hack(首先分配为null,然后分配为lambda的情况)或Y-的情况下制作递归lambas组合器(在C#中像地狱一样丑陋)。2.在项目范围内没有类型别名可以使用,因此委托的签名很快就变得难以管理。因此,仅出于这两个愚蠢的原因,我再也无法使用C#了,因为我唯一能使它工作的方法是使用单方法接口,这是不必要的额外工作。
Trident D'Gao

@bonomo Java 8有一个通用的java.util.function.BiConsumer,在C#中可能有用:public interface BiConsumer<T, U> { public void accept(T t, U u); }java.util.function中还有其他有用的功能接口。
GlenPeterson

@bonomo嘿,我明白了,这是Haskell的痛苦。每当您读到有人说“学习FP使我在OOP方面变得更好”时,就意味着他们学习了Ruby或类似Haskell的非纯声明式语言。Haskell明确指出OOP处于低水平。您遇到的最大麻烦是,当您不在HM类型系统中时,基于约束的类型推断是无法确定的,因此,完全不会完成基于cosntraint的类型推断:blogs.msdn.com/b/ericlippert/archive / 2012/03/09 /…
Jimmy Hoffa

1
Most OOP languages don't have the stack space for it 真?您只需要30级递归就可以在平衡的二叉树中管理数十亿个节点。我很确定我的堆栈空间比这还适合更多级别。
罗伯特·哈维

3

除了功能编程和面向对象的编程外,还有声明性编程(SQL,XQuery)。学习每种样式可以帮助您获得新的见解,并且您将学习选择适合该工作的工具。

但是,是的,用一种语言编写代码可能非常令人沮丧,并且知道如果您正在使用其他内容,那么对于特定的问题域,您的工作效率可能会更高。但是,即使您使用的是Java之类的语言,也可以将FP中的概念应用到Java代码中,尽管采用的是回旋方式。例如,Guava框架就是其中的一部分。


2

作为程序员,我认为您永远都不应停止学习。就是说,学习FP污染您的OOP技能非常有趣。我倾向于将学习OOP看作是学习如何骑自行车。您永远不会忘记该怎么做。

当我了解FP的来龙去脉时,我发现自己在数学上进行了更多的思考,并且对编写软件的方法有了更好的了解。那是我的亲身经历。

随着您获得更多的经验,核心编程概念将更难失去。因此,我建议您在FP上放轻松,直到OOP概念完全被您牢记。FP是一个确定的范式转换。祝好运!


4
学习OOP就像学习爬网。但是一旦稳步站起来,就只会在醉酒的时候才开始爬行。当然,您不能忘记该怎么做,但通常不会。当您知道自己可以跑步时,与爬行者同行将是一种痛苦的经历。
SK-logic

@ SK-逻辑,我喜欢你的methaphor
三叉戟D'高

@ SK-Logic:学习命令式编程是什么样的?拖着自己的肚子吗?
罗伯特·哈维

@RobertHarvey试图用生锈的勺子和一副打孔卡钻到地下。
Jimmy Hoffa 2013年

0

已经有很多好的答案,因此我将解决您的部分问题;也就是说,我谨代表您提出问题的前提,因为OOP和功能部件不是互斥的。

如果使用C ++ 11,则语言/标准库中内置了许多此类功能编程功能,这些功能可以与OOP很好地协同(很好)。当然,我不确定您的老板或同事对TMP的接受程度如何,但是重点是您可以以某种形式或以其他非功能性/ OOP语言(例如C ++)获得许多这些功能。

使用具有编译时间递归的模板取决于您的前三点,

  • 不变性
  • 递归
  • 模式匹配

由于模板值是不可变的(编译时常量),因此任何迭代都使用递归完成,而分支则使用(或多或少)模式匹配来完成,并以过载解析的形式进行。

至于其他要点,使用std::bindstd::function给您提供部分函数应用程序,并且函数指针是该语言的内置函数。可调用对象是功能对象(以及部分功能应用程序)。请注意,所谓可调用对象,是指定义其的对象operator ()

惰性求值和纯函数会有点困难;对于纯函数,可以使用仅按值捕获的lambda函数,但这是不理想的。

最后,这是在部分函数应用程序中使用编译时递归的示例。这是一个有些人为的例子,但它说明了上面的大多数观点。它将递归地将给定元组中的值绑定到给定函数,并生成(可调用的)函数对象

#include <iostream>
#include <functional>

//holds a compile-time index sequence
template<std::size_t ... >
struct index_seq
{};

//builds the index_seq<...> struct with the indices (boils down to compile-time indexing)
template<std::size_t N, std::size_t ... Seq>
struct gen_indices
  : gen_indices<N-1, N-1, Seq ... >
{};

template<std::size_t ... Seq>
struct gen_indices<0, Seq ... >
{
    typedef index_seq<Seq ... > type;
};


template <typename RType>
struct bind_to_fcn
{
    template <class Fcn, class ... Args>
    std::function<RType()> fcn_bind(Fcn fcn, std::tuple<Args...> params)
    {
        return bindFunc(typename gen_indices<sizeof...(Args)>::type(), fcn, params);
    }

    template<std::size_t ... Seq, class Fcn, class ... Args>
    std::function<RType()> bindFunc(index_seq<Seq...>, Fcn fcn, std::tuple<Args...> params)
    {
        return std::bind(fcn, std::get<Seq>(params) ...);
    }
};

//some arbitrary testing function to use
double foo(int x, float y, double z)
{
    return x + y + z;
}

int main(void)
{
    //some tuple of parameters to use in the function call
    std::tuple<int, float, double> t = std::make_tuple(1, 2.04, 0.1);                                                                                                                                                                                                      
    typedef double(*SumFcn)(int,float,double);

    bind_to_fcn<double> binder;
    auto other_fcn_obj = binder.fcn_bind<SumFcn>(foo, t);
    std::cout << other_fcn_obj() << std::endl;
}
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.