为什么要学习函数式编程

172 阅读5分钟

函数式编程

为什么要学习函数式编程

为什么用不好函数式编程

想象一下自己是一个伐木工人,拿着斧头砍树,突然有人拿着电锯给我们砍树,而我们依旧按照斧子的方法用电锯,很快电锯被用坏了,然后我们抱怨这东西真差.

习惯于命令式编程的我们也拿着命令式的方式处理函数式编程

学习一种全新的编程范式,困难并不在于掌握新的语言.语法只是小细节,真正考验人的,是学会用另一种方式去思考.

范式转变

先上一段代码吧,对比一下传统编程和函数式编程

题目: 读入一个文本文件,确定所有单词的使用频率从高到低的使用频率排序

传统做法:

public class Words {
    /**
     * 虚词集合
     */
    private Set<String> NON_WORDS = new HashSet<>(){{
        add("the");add("and");add("of");add("to");add("a");
        add("i");add("it");add("in");add("or");add("is");
        add("i");add("s");add("as");add("so");add("but");add("be");
    }};

    public Map<String,Integer> wordFreq(String words){
        Map<String, Integer> wordMap = new TreeMap<>();
        Matcher m = Pattern.compile("\\w+").matcher(words);
        while (m.find()){
            String word = m.group().toLowerCase();
            if(!NON_WORDS.contains(word)){
                if(wordMap.get(word) == null){
                    wordMap.put(word,1);
                } else {
                    wordMap.put(word,wordMap.get(word) + 1);
                }
            }
        }
        return wordMap;
    }
}

函数式做法:

采用了java8中Stream API 和 以lambda块方式实现高阶函数

public class Words {

    /**
     * 虚词集合
     */
    private Set<String> NON_WORDS = new HashSet<>(){{
        add("the");add("and");add("of");add("to");add("a");
        add("i");add("it");add("in");add("or");add("is");
        add("i");add("s");add("as");add("so");add("but");add("be");
    }};
  
    private List<String> regexToList(String words,String regex){
        List<String> wordList = new ArrayList<>();
        Matcher m = Pattern.compile(regex).matcher(words);
        while(m.find()){
            wordList.add(m.group());
        }
        return wordList;
    }

    public Map<String> wordFred1(String words){

        TreeMap<String,Integer> wordMap = new TreeMap<>();
        regexToList(words,"\\w+").stream()
                .map(w -> w.toCharArray())
                .filter(w -> !NON_WORDS.contains(w))
                .forEach(w -> wordMap.put(w,wordMap.getOrDefault(w,0) + 1));

        return wordMap;
    }


}

对比一下,两种写法: 第一种写法牺牲了代码的清晰来换取执行性能 第二种写法在性能上确实惨不忍睹,先经find产生的匹配结果转换成stream,然后再一步一步往下走,把原先一个循环转成了三次循环

命令式编程常常迫使我们处于性能考虑,把不同的任务 交织 起来,以便能够用一次循环来完成多个任务

而函数式编程用 map(), filter() 这些高阶函数把我们解放出来,让我们站在更高的抽象层次上去考虑问题,把问题看得更清楚

跟上语言发展的潮流

现在主流语言都进行了函数式方面的扩展,java也通过纳入 lambda 块(也就是高阶函数) 实现了函数式编程

将控制权让渡给语言/运行时

人总是越来越懒,从第一代编程语言发展到第四代编程语言,开发者们越来越多地把乏味单调的任务托付给语言和运行时(如Java中的泛型)

放弃例如内存管理,垃圾回收这类工作,把时间花在更高层次的抽象上,专注于更重要的问题,

函数式编程语言用高阶抽象从容取代基本的控制结构,将琐碎的细节交托给运行时,令繁琐的实现化作轻巧

简洁

**面向对象编程通过封装不确定因素来使代码能被人理解; 函数式编程通过尽量减少不确定因素来使代码能被人理解 ** —— working with legacy code 作者 Michael Feathers

所谓 不确定因素 就是为了精细地控制状态和改变方法而存在的封装,作用域,可见性等OOP的构造,以及多线程中对状态的控制.

而在函数式语言在这个问题上采用了另一种做法,他们认为,与其建立种种机制来控制可变状态,不如尽可能消灭可变的状态这个不确定因素.

其立论的根据在于 假如语言不对外暴露那么多有出错可能的特性,那么开发者就不那么容易犯错 这刚好很像 本质安全性 这个概念

在 面向对象的命令式编程语言里,重用的单元是类和类之间沟通用的消息,例如UML中依赖,继承

OOP的世界提倡开发者针对具体问题建立专门的数据结构,相关的专门操作以 方法 的形式附加在数据结构上

而函数式编程语言实现重用的思路很不一样; 函数式语言提倡在有限的几种关键数据结构(如list set map)上运用针对这些数据结构高度优化过的操作,以此构成基本的运转机构; 开发者再根据具体用途,插入自己的数据结构和高阶函数去调整机构的运转方式.

OOP 不断创建新的数据结构和附属操作,因为压倒一切的面向对象编程范式就是建立新的类和类之间的消息 把所有的数据结构都封装成类,一方面压制了方法层面的重用,另一方面鼓励了大粒度的框架式的重用

而函数式的程序构造更方便我们在比较细小的层面上重用代码.