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

2018-05-30 06:40:10 +08:00
 hansonwang99



函数式编程概述

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



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

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

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

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

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

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

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

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

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

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

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

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

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



函数式编程例析

举例 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(“”) );
}



后记

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

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

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


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

学到了,可惜小公司不用 java8 新特性
Mistwave
2018-05-30 09:13:06 +08:00
说得好,我选择 Scala🤣
hansonwang99
2018-05-30 09:22:43 +08:00
@shalk 我选 kotlin
FrailLove
2018-05-30 09:28:48 +08:00
说得好,我选择 Clojure🤣
tanranran
2018-05-30 10:30:51 +08:00
说得好,我选择 Kotlin🤣
KDr2
2018-05-30 10:57:02 +08:00
C++ 那个 new 出来的不是 pointer ?
windsage
2018-05-30 11:42:19 +08:00
我居然在群里看到这个文章了...难道在一个群??
ghos
2018-05-30 15:08:40 +08:00
说得好,我选择 Kotlin🤣
inflationaaron
2018-05-30 21:02:03 +08:00
说得好,我选择 Haskell(eta) 🤣
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
2018-05-31 00:08:55 +08:00
@KDr2 是的,所有 auto 关键字多么的有用
0x11901
2018-05-31 00:11:13 +08:00
@hitmanx 这几个高阶函数没必要改名字用吧_(:_」∠)_大家都知道 std::transform 是干什么的(●°u°●)​ 」
Chingim
2018-05-31 09:11:51 +08:00
大部分安利函数式编程的文章,都不提副作用,Functor,IO,Manad,Applicative。让我有了一种只要用上 filter,map,foreach,reduce,compose 就是函数式编程的错觉。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/458819

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX