V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
hansonwang99
V2EX  ›  程序员

函数式编程思维在三行代码情书中的应用

  •  
  •   hansonwang99 ·
    hansonwang99 · 2018-05-30 06:40:10 +08:00 · 4024 次点击
    这是一个创建于 2150 天前的主题,其中的信息可能已经有所发展或是发生改变。

    42:9



    函数式编程概述

    如今主流的编程语言,函数式编程范式或多或少都融入其中成了“标配”,或者说主流语言都在进行函数式方面的扩充,这是一个大趋势。以 Java 为例,随着 Lambda 块Stream API 等这种高阶函数的加持,Java 总算是装备了函数式这一利器;博大精深的 C++也在 2011 版的语言标准里加入了 Lambda 块的支持;再比如前一段时间我初步体验了的 Groovy 语言,虽然其运行于 JVM 之上,然而其对 动态语言函数式编程范式 以及 元编程功能 的加持所带来的表现力和简洁性可以说甩了 Java 几条街,可以利用 Groovy 的所有动态功能构建高性能的 JVM 应用、将开发效率提高几个数量级。语言的例子有很多,我不一一枚举。



    为什么要使用函数式编程范式

    这里讲几个函数式编程的典型特点,区别的对象那就是传统的命令式编程

    命令式编程 VS 函数式编程

    • 0x01. 更高层次的抽象(高阶函数)

    用高阶抽象来取代基本的控制结构本身就是一个全新的思考方式,这样可以让开发者聚焦精力于业务场景而无需费心复杂地层运作

    举个栗子:将一个字符串集合中的所有单词转为大写,我们用 Java 语言来实现

    如果按照传统的命令式编程的解法,那接下来不出意外我们得来写循环、遍历这种迭代操作了:

    for (int i=0; i<wordList.size(); i++) {
       wordList.get(i).toUpperCase();
    }
    

    但如果使用 Java 的函数式编程范式,一切都是那么的优雅,一句话搞定

    wordList.stream.map( w -> w.toUpperCase() )
    

    这里的 map()函数就是所谓的高阶函数,我们用高阶函数代替了底层的迭代,因为我们并没有处理细节,我们仅仅定义了映射的逻辑,迭代由高阶函数来自动完成!

    • 0x02. 提升代码信噪比(简洁性)

    区别于面向对象语言用抽象来封装不确定因素,函数式编程通过尽量减少不确定因素来使代码极度简洁

    上面的例子对于本条优点的展现我想应该也不必多说了

    • 0x03. 控制权转交于运行时(动态性)

    区别于传统的编译形语言,配备函数式编程范式的动态语言更多的将控制权转交到语言运行时手里,获得的则是更高的灵活性、表现力和性能权衡。

    这三点优点将在接下来的例子中切实的感受并领会!



    函数式编程例析

    举例 1:词频统计

    做的事情很简单:给定一个单词集合,统计出集合中除了助词(如ofonthe等)之外的单词出现的频次,不区分大小写

    命令式解法: 至少分为以下几大步

    • 先进行循环迭代
    • 然后统一将单词转为小写
    • 然后判断单词是否是助词
    • 最后进行词频统计
    public class WordCount {
    
        // 定义一个助词集合,这些单词不参与计数
        private Set<String> auxiliaryWordSet = new HashSet<String>() {{
           add("of"); add("the"); add("to"); add("and"); add("so"); add("are ”);
        }};
    
        // 传统命令式解法实现的词频统计函数
        public Map doWordCount( List<String> context ) {
            Map<String,Integer> result = new HashMap<String, Integer>();
            for ( String word:context ) {  // 循环迭代
                String lowerCaseWord = word.toLowerCase();  // 将单词统一转换为小写
                if( !auxiliaryWordSet.contains(lowerCaseWord) ) {
                    if( null == result.get(lowerCaseWord) )
                        result.put( lowerCaseWord, 1 );
                    else
                        result.put( lowerCaseWord, result.get(lowerCaseWord)+1 );
                }
            }
            return result;
        }
    
        // main() 函数
        public static void main(String[] args) {
            List<String> wordList = new ArrayList<String>() {{
                add("The"); add("Products"); add("of"); add("Samsung"); add("and"); add("Apple ”);
                add("are"); add("so"); add("amazing"); add("especially"); add("Apple ”);
            }};
    
            WordCount wordCount = new WordCount();
            Map res = wordCount.doWordCount( wordList );
            System.out.print(res); // 打印:{apple=2, amazing=1, samsung=1, especially=1, products=1}
        }
    }
    

    函数式解法:

    如果我们用 Java 的 Stream API 和 Lambda 块所构成的函数式范式来重写 doWordCount() 函数,一切都将如此简洁:

    public Map doWordCount2( List<String> context ) {
        Map<String,Integer> result = new HashMap<String, Integer>();
        context.stream().map( w -> w.toLowerCase() )
                .filter( w -> !auxiliaryWordSet.contains(w) )
                .forEach( w -> result.put( w, result.getOrDefault(w,0) + 1 ) );
        return result;
    }
    

    备注:这里的getOrDefault是 Java 的 Map 提供的一个便利函数,意思是:在 Map 中若没有找到给定的 key 时,返回一个“默认值”

    对比命令式解法,用户省去了很多繁琐的迭代和判断,我们只讲焦点聚焦在业务逻辑之上,代码信噪比提升不小吧!


    举例 2:连词成句

    给定一个离散的单词集合,我们想将字母数大于 1 的单词的首字母大写后,用 短横线- 连接起来成为一个句子

    命令式解法:

    public class WordConnect {
    
        // 将单词的首字母大写
        public String capitalizeFirstLetter( String s ) {
            return s.substring(0,1).toUpperCase() + s.substring(1,s.length() );
        }
    
        // 连词成句
        public String connectWord( List<String> context ) {
            StringBuilder result = new StringBuilder();
            for ( String word: context ) {
                if ( word.length() > 1 ) {
                    result.append( capitalizeFirstLetter(word) );
                    result.append("-“);
                }
            }
            return result.substring(0,result.length()-1).toString();
        }
    
        // main()函数
        public static void main(String[] args) {
            List<String> wordList = new ArrayList<String>() {{
                add("The"); add("Products"); add("of"); add("Samsung"); add("and"); add("Apple ”);
                add("are"); add("so"); add("amazing"); add("especially"); add("Apple ”);
            }};
    
            WordConnect wordConnect = new WordConnect();
            String res = wordConnect.connectWord( wordList );
            System.out.print(res); // 打印:The-Products-Of-Samsung-And-Apple-Are-So-Amazing-Especially-Apple
        }
    }
    

    函数式解法 1: Java Steam API 和 Lambda 块实现

    public String connectWord( List<String> context ) {
        return context.stream().filter( w -> w.length()>1 )
                .map( w -> capitalizeFirstLetter(w) )
                .collect( Collectors.joining("-") );
    }
    

    我什么都不想说了,这不要太简洁好吧!

    函数式解法 2: Groovy 语言实现

    public String connectWord( context ) {
        context.findAll { it.length() >1 }
        .collect { it.capitalize() }
        .join ‘-‘
    }
    

    关于 Groovy 语言的初体验,可以参考我的文章:Groovy 初体验:构建高性能 JVM 应用



    函数式最佳实践:高效编写三行情书

    还记得去年的 520,为了表达心中对于老婆无限的、无法表达的爱,我想写一封不超过三行的代码情书,我更想用尽可能短的代码来尽可能多地表达,于是我选择了函数式编程。

    我的 520 三行代码情书在此:

    public TimeRiver timeFlow( List<DaysMeetYou> days ) {
        return (TimeRiver)days.stream()
            .filter( n->theDaysNotWithYou(n) )
            .map( e->accompanyByMyLove(e) )
            .collect( Collectors.joining(“”) );
    }
    

    我的 520 三行代码情书



    后记

    文中提到的 Groovy 动态编程语言,作者体验过一点,可以参考:Groovy 初体验

    作者更多的原创文章:在 V2EX 作者主页

    如果有兴趣,也来看看作者一些关于容器化、微服务化方面的文章:


    16 条回复    2018-05-31 09:11:51 +08:00
    hansonwang99
        1
    hansonwang99  
    OP
       2018-05-30 07:39:32 +08:00 via iPhone
    哎,V 站这个手机 App 的代码排版......
    hansonwang99
        2
    hansonwang99  
    OP
       2018-05-30 08:22:14 +08:00 via iPhone
    还有就是英文引号粘过来又变成一边是中文引号...
    shalk
        3
    shalk  
       2018-05-30 08:56:58 +08:00 via iPhone
    内容不错,请教楼主,groovy,scala,kotlin 这几种 jvm 语系楼主怎么看,想尝试一种,有什么建议么
    AltairT
        4
    AltairT  
       2018-05-30 09:09:06 +08:00 via iPhone
    @hansonwang99 v 站并没有官方 app😀

    学到了,可惜小公司不用 java8 新特性
    Mistwave
        5
    Mistwave  
       2018-05-30 09:13:06 +08:00 via iPhone
    说得好,我选择 Scala🤣
    hansonwang99
        6
    hansonwang99  
    OP
       2018-05-30 09:22:43 +08:00 via iPhone   ❤️ 1
    @shalk 我选 kotlin
    FrailLove
        7
    FrailLove  
       2018-05-30 09:28:48 +08:00
    说得好,我选择 Clojure🤣
    tanranran
        8
    tanranran  
       2018-05-30 10:30:51 +08:00
    说得好,我选择 Kotlin🤣
    KDr2
        9
    KDr2  
       2018-05-30 10:57:02 +08:00   ❤️ 1
    C++ 那个 new 出来的不是 pointer ?
    windsage
        10
    windsage  
       2018-05-30 11:42:19 +08:00
    我居然在群里看到这个文章了...难道在一个群??
    ghos
        11
    ghos  
       2018-05-30 15:08:40 +08:00
    说得好,我选择 Kotlin🤣
    inflationaaron
        12
    inflationaaron  
       2018-05-30 21:02:03 +08:00
    说得好,我选择 Haskell(eta) 🤣
    hitmanx
        13
    hitmanx  
       2018-05-30 21:30:25 +08:00
    template <typename Container, typename Fn>
    Container& map(Container& c, const Fn& fn)
    {
    std::transform(std::begin(c), std::end(c), std::begin(c), fn);
    return c;
    }

    template <typename Container, typename PredicateFn>
    Container& filter(Container& c, const PredicateFn& predicateFn)
    {
    c.erase(std::remove_if(std::begin(c), std::end(c), predicateFn), std::end(c));
    return c;
    }
    0x11901
        14
    0x11901  
       2018-05-31 00:08:55 +08:00
    @KDr2 是的,所有 auto 关键字多么的有用
    0x11901
        15
    0x11901  
       2018-05-31 00:11:13 +08:00
    @hitmanx 这几个高阶函数没必要改名字用吧_(:_」∠)_大家都知道 std::transform 是干什么的(●°u°●)​ 」
    Chingim
        16
    Chingim  
       2018-05-31 09:11:51 +08:00 via Android
    大部分安利函数式编程的文章,都不提副作用,Functor,IO,Manad,Applicative。让我有了一种只要用上 filter,map,foreach,reduce,compose 就是函数式编程的错觉。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   3522 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 29ms · UTC 11:02 · PVG 19:02 · LAX 04:02 · JFK 07:02
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.