固执的程序员学习函数式编程的收获 之 一

news/2024/7/7 10:33:11

最近因为写node js,开始有机会接触js的函数式写法。关于函数式语言,其实久闻其名,但只是大概了解过一些概念罢了。刚开始听到这个概念觉得不会就是面向过程编程的改良版吧?(自己还是太无知了…)
由于自己的编程语言主要是java,受面向对象语言的影响很深,也一度以为语言的进化已经到了尽头。将来的语言只是更容易写,提供更多的语法糖。导致自己对函数式语言并没太大的关注。于是写代码的时候各种被批,各种0分…
一个老朽的面向对象语言的使用者终于还意识到学习一下函数式语言。当然对很多东西还一知半解,也作为自己的学习笔记,在这里谈谈函数式语言,也说一下如何让自己去“函数式”地思考。

函数式编程

关于函数-1

于函数式编程的对比对象叫做命令式编程(imperative programming)。函数式编程与之的一个重大区别在于函数在语言中的地位。
在命令式编程语言中,函数是处理变量的特殊存在。变量是可以被传来传去的,并当作返回值返回。而函数是做不到的。(当然也有函数指针这种比较特殊的东西)

class Dog {
  void bite(Bone bone){
    //
  }
}
Dog dog = new Dog();
Object a = dog.bite;  // <-- 无法把函数作为一个变量传递。

而在函数式语言当中,函数相对于变量就没有那么特别,它们也可以被传来传去,当作返回值返回。
这样做有什么优点?
说说自己的理解。从于面向对象语言的角度来说,因为函数可以不依附于类,还可以当作变量,很直观的优点是代码会变得很简洁。
用一个定义回调函数的类的例子来说明一下。

public class SomeWork {
  public void doWork(Callback afterWorkDone){
     // do something
     cb.do();
  }
}
Interface Callback {
  void run();
}

实际调用时。

  SomeWork someWork = new SomeWork();
  someWork.doWork(new Callback() {
    @Override
    public void run(){
      // things to do after work is done.
      // 匿名类好烦人...
    });

上面的代码,大致是我们为了回调处理定义了专门的接口,然后在调用时创建实现接口匿名类。
那么如果是函数式语言的话,会怎样呢?下面是伪代码,伪代码哦!

  doWork(Func afterWorkDone){
    // do something
    func.apply();
  }
  Func func = () -> { /* do something */};
  SomeWork someWork = new SomeWork();
  someWork.doWork(func);

我们可以不用再特意定义一个接口,也不用写丑陋的匿名类了(如果是函数式语言)。
java中现在也引入了函数式语言的特性,也以使用lambda了,可惜因为java自身语言限制,目前还是需要定义Callback这个接口,才能用lambda来写的。
这里稍微扯远一点,类似于Callback这样的类,其实真的因为面向对象语言的限制而被创造出的一种类。其实往往我们需要的就是一个回掉处理,而在面向对象的语言中,我们被迫要胡邹出一个类,然后定义一个行为,还要让人实现这个方法。为了一个回调处理,我们还得定义一个类,还得帮它想个名字,这些东西在使用函数式语言后就显得比较冗余。

关于函数-2

刚才说过,函数可以当作变量。变量也可以通过运算产生新的变量,也就是新的函数。
一般的语言并不提供函数组合的语法,所以还是使用一下伪代码。
假设a -> a,表示一个函数。它获得一个a类型的参数,返回一个类型的返回值。

 func1 = a -> a
 func2 = a -> a
 func3 = func1 $ func2 // $表示调用函数。func3也是一种a -> a的函数。

上面说的是func1是一种函数。接受a类型,返回a类型。
func2也是接受a类型,返回a类型。
那func1是可以接受func2的运算结果吧,那把它们连一块吧。
func2接受a,返回a,然后把这个a交给func1,func1也返回a。这一整个过程也成了一个接受a返回a的函数,叫它func3
如果语言上对这种组合有好的支持,我们就可以把函数给串起来,像下面这样。

  funcA $ funcB $ funcC

比如说关于组装自行车的函数。它接受一个Bike类,返回一个Bike类。 Bike -> Bike

  addWheel = Bike -> Bike  // 没有写实现
  addChain = Bike -> Bike  // 没有写实现
  addLamp = Bike -> Bike  // 没有写实现
  constructBike = addWheel $ addWheel $ addChain $ addLamp // 还是Bike -> Bike函数
  constructOneWheeledBike = addWheel $ addChain $ addLamp  // 还是Bike -> Bike函数

我们把函数组合后,有了组装二轮自行车的函数和独轮车的函数。
当然你可能觉得命令式语言也做得到啊。

  addWheel(addChain(addLamp(bike)));

当请注意,命令式语言中,函数是特殊的东西,所以当你对函数进行组合是,你必须生名一个新函数。

  public Bike constructBike(Bike bike){
      return addWheel(addChain(addLamp(bike)));
  }
  public Bike constructOneWheeledBike(Bike bike){
      return addWheel(addChain(addLamp(bike)));
  }

而且上面的做法需要调用的嵌套。写多了很难读。极端一点

 aMethod(bMethod(cMethod(dMethod(eMethod()))));

而函数式的写法会变成像一个链条。

  aFunc()
  .bFunc()
  .cFunc()
  .dFunc()
  .eFunc()

或者

 aFunc $ bFunc $ cFunc $ eFunc

你可能觉得这只是一个语法糖罢了。但这蕴含着函数式语言的一种思想,把小单位简单的处理组合起来,可以变成一个能应对复杂问题的处理。
当然面向对象也有这种思路,只不过面向对象语言中,这种简单的处理是由类来承担,或者类的方法,我们将类的各种方法组合调用来构建一个程序。而函数式编程中,这种简单处理的元素变成了函数。

关于副作用

另一个关于函数式编程的关键字便是副作用。当一个函数的处理改变了外部的任何状态那他就产生了副作用。

  void prank(List<Integer> nums) {
    nums.add(1); // 改变了参数的状态,产生了副作用
  }

对于nums这个list的改变,会影响到应用这个nums的处理。这种隐性的状态改变容易造成程序问题。那要写成没有副作用的处理,大概是下面这个样子。

  List<Integer> addElement(List<Integer> nums){
    List<Integer> newList = new List<>();
    newList.addAll(nums);
    newList.add(1);
    return newList;
  }

这样写还麻烦。而函数式语言会对变量的不可变性(immutability)有很好的支持。比如在函数式语言中

  nums.add(1);

这个调用会生成新的list,nums的状态不发生改变。
另外诸如IO操作,Http请求等都属于副作用。函数式编程对于副作用的限制也各不相同。如Haskell就完全不允许副作用。而javascript,在语言语法上对副作用没有限制。

关于函数-3

我们假定我们用了一种严格的函数式语言,无法产生任何副作用。那下面的函数就是没有意义的了。

  public class Example {
    public void doSomething(Job job){
      // whatever you want to do with job.
    }
  }
  Example ex = new Example();
  ex.doSomething(job1);

假设doSomething中对job的操作,改变了Job的状态,但因为语言不支持/不允许副作用,这个状态不会反映到参数的传递方的job1。而这个函数又没有返回值,本质上他对程序没有任何的影响。所以在函数式语言中,函数可定是有返回值的。(没有返回值,函数还不能进行组合呢~)

总结

这次谈了一下函数式语言中的函数。但目前很多的开发环境比较多的可能还是使用有函数式特性的命令式语言吧(比如java8)。那在使用这种语言时,如何能写”函数式“的程序呢。我给了自己以下建议逼迫自己来写程序。这样会用新的思想去思考程序该怎么写。
- 尽量不写返回值为void的方法
- 尽量不要写有副作用的方法
- 尽量不写for循环
不使用for循环?如果大家使用过函数式语言,这个好像是个很自然的选择。这个话题下次再展开吧。


http://www.niftyadmin.cn/n/4428415.html

相关文章

固执的程序员学习函数式编程的收获 之 二 说说monad

之前说了函数式编程的收获。比如说函数可以当作变量&#xff0c;然后尽量避免写副作用的程序。 之后可以说遇到了一个超级难理解的东西–monad。 一切要从和小田君的对话说起 当我在写java时&#xff0c;大概是下面的一段代码 List.map( item -> item.getName()); List.…

MAYA影视动漫高级模型制作全解析出_完整版PDF电子书下载 带索引书签目录高清版...

MAYA影视动漫高级模型制作全解析_页数384_出版日期2016.04_完整版PDF电子书下载 带索引书签目录高清版_13936277 下载链接 http://pan.baidu.com/s/1skA4FZf 【作 者】CGWANG动漫教育著【形态项】 384【出版项】 北京&#xff1a;人民邮电出版社 , 2016.04【ISBN号】7-115-41…

自己写deque

//deque /* what is a deque? In Chinese, its called "双端队列". Its different from a queue. Its elements can be added to or removed from either the front(head) or back(tail) ,called a head-tail linked list.输入限制deque An input-restricted deque …

ddd的战术篇: CQRS

之前的文章介绍了ddd在战术层面的要素&#xff0c;包括entity&#xff0c;value object&#xff0c;aggregate和一些设计模式如repository。在ddd中&#xff0c;repository几乎成为了等同于entity一样的基本要素。 关于aggregate与repository的回顾 aggregate是entity和value…

领域驱动设计(domain driven design)战略篇之一 战略 Bounded Context

之前的文章主要从战术层面的角度介绍了ddd。在岛国也被称为轻量级ddd。它提供了一些概念如aggregate, entity, domain event和一些设计模式如repository, specification来帮助我们建模和设计。各种战术还有能够扩展的地方&#xff0c;有机会还会再写下去。不过从这篇文章开始会…

Linux系统设置Tab键缩进为4个字符

Linux系统设置Tab键缩进为4个字符经常使用vi/vim的朋友可能会遇到&#xff0c;写脚本的时候发现按一次Tab键就缩进8个字符&#xff08;默认是8个字符&#xff09;&#xff0c;这样感觉缩进有点长了&#xff0c;这里我们可以设置下按一次Tab键&#xff0c;让它缩进4个字符&#…

领域驱动设计(domain driven design)战略篇之二 Bounded Context

之前的一篇文章谈了战略ddd的重要性与Bounded Context这个概念&#xff0c;最近在油管上看到一个2017年关于domain driven design的演讲。如下 感觉与自己现在讲的主题十分相关&#xff0c;正好在这里展开说一下。 他认为Bounded Context可能是ddd中最重要的概念。而悲剧地…

死锁及其解决方案(避免、预防、检测)

所谓死锁&#xff1a;是指两个或两个以上的进程在执行过程中&#xff0c;因争夺资源而造成的一种互相等待的现象&#xff0c;若无外力作用&#xff0c;它们都将无法推进下去。此时称系统处于死锁 死锁产生的原因&#xff1f; 1.因竞争资源发生死锁 现象&#xff1a;系统中供多…