函數式編程:如何高效簡潔地對數據查詢與變換

摘要:一提到編程範式,很容易聯想到宗教的虔誠,每種宗教所表達信條都有必定合理性,但若是一直只遵循一種教條,可能也被讓本身痛苦不堪,編程範式也是如此。

案例1

案例一,代碼摘抄來自一企業培訓材料,主要代碼邏輯是打印每課成績,並找出學生非F級別課程統計平均分數:算法

class CourseGrade {
 public String title;
 public char grade;
}

public class ReportCard {
 public String studentName;
 public ArrayList<CourseGrade> cliens;

 public void printReport() {
        System.out.println("Report card for " + studentName);
        System.out.println("------------------------");
        System.out.println("Course Title       Grade");
        Iterator<CourseGrade> grades = cliens.iterator();
        CourseGrade grade;
 double avg = 0.0d;
 while (grades.hasNext()) {
            grade = grades.next();
            System.out.println(grade.title + "    " + grade.grade);
 if (!(grade.grade == 'F')) {
                avg = avg + grade.grade - 64;
            }
        }
        avg = avg / cliens.size();
        System.out.println("------------------------");
        System.out.println("Grade Point Average = " + avg);
    }
}

上面的代碼有哪些問題呢:編程

  • 成員變量採用public,缺乏數據封裝性
  • 沒有判斷cliens是否爲空,可能除以0值。注:假定它不會爲空,另外邏輯可能有問題,爲何統計總分是非F課程,除數倒是全部課程Size,先忽略這個問題
  • avg這個變量多個用途,便是總分,又是平均分
  • cliens變量名難以理解
  • !(grade.grade == 'F') 有點反直覺
  • while循環幹了兩件事,打印每課的成績,也統計了分數

培訓材料並未給標準解題,嘗試優化一下代碼,採用Java8的Stream來簡化計算過程,並對代碼進行了分段:segmentfault

public void printReport2() {
        System.out.println("Report card for " + studentName);
        System.out.println("------------------------");

        System.out.println("Course Title       Grade");
        cliens.forEach(it -> System.out.println(it.title + "    " + it.grade));

 double total = cliens.stream().filter(it -> it.grade != 'F')
                .mapToDouble(it -> it.grade - 64).sum();
        System.out.println("------------------------");
        System.out.println("Grade Point Average = "  + total / cliens.size());
    }

進一步優化,把各種打印抽取各自函數:設計模式

private void printHeader() {
        System.out.println("Report card for " + studentName);
        System.out.println("------------------------");   
    }

 private void printGrade() {
        System.out.println("Course Title       Grade");
        cliens.forEach(it -> System.out.println(it.title + "    " + it.grade));
    }

 private void printAverage() {
 double total = cliens.stream().filter(it -> it.grade != 'F')
                .mapToDouble(it -> it.grade - 64).sum();
        System.out.println("------------------------");
        System.out.println("Grade Point Average = "  + total / cliens.size());
    }

 public void printReport3() {
        printHeader();
        printGrade();
        printAverage();
    }

注:若是隻算非F的平均分,能夠一行搞定:閉包

double avg = cliens.stream().filter(it -> it.grade != 'F').mapToDouble(it -> it.grade - 64).average().orElse(0.0d);

案例二:再看一段代碼:

List<Integer> tanscationsIds = transcations.parallelStream()
        .filter(it -> it.getType() == Transcation.GROCERY)
        .sorted(comparing(Transcation::getValue).resersed())
        .map(Transcation::getId)
        .collect(Collectors::toList());

代碼很是清晰:編程語言

  • 過濾出類型爲GROCERY的交易記錄
  • 按其value值進行倒排序
  • 各自取其Id字段
  • 輸出Id列表

這看起來是否是像這樣一條SQL語句:select t.id from tanscations t where t.type == 'GROCERY' order by t.value desc函數式編程

1 背後的知識

目前Java8已普遍使用,對於Stream與Lambda應習覺得常了,而再也不是一種炫技。網上也有很是多的教程,如有同窗還不熟悉他們的用法,能夠多找找材料熟悉一下。函數

Stream正如其名,像一條數據生產流水線,逐步疊加中間操做(算法和計算),把數據源轉換爲另外一個數據集。工具

筆者很早之前學過C#,接觸過LINQ(Language Integrated Query),它比Java的Stream和Lambda用法更爲清晰簡潔,先給個簡單示例:學習

var result = db.ProScheme.OrderByDescending(p => p.rpId).Where(p => p.rpId > 10).ToList();

LINQ爲數據查詢而生,能夠算是DSL(Domain Specific Language)了,背後也是函數式編程(FP)一套理念,先記住其中兩點:

  • Monad 是一種設計模式,表示將一個運算過程,經過函數拆解成互相鏈接的多個步驟
  • Lambda表達式 是一個匿名函數,Lambda表達式基於數學中的λ演算得名

FP還有其它的特性:模式匹配,柯里化,偏函數,閉包,尾遞歸等。對FP感受興趣的同窗不妨找找材料學習一下。

如今的主流語言,都引入一些FP特性來提高語言在數據上的表達能力。

C++11引入Lambda表達式,並提供<algorithm>,<functional>兩個基礎庫,一個簡單示例:

int foo[] = { 10, 20, 5, 15, 25 };
std::sort(foo, foo+5, [](int a,int b){return a > b;});

Python提供functools庫來簡化一些函數式編程(仍是至關的弱),一個簡單示例:

foo = ["A", "a", "b", "B"]
sorted(foo, key=functools.cmp_to_key(locale.strcoll))

2 函數式編程

固然,面嚮對象語言中增長lambda這類特徵不能就稱爲函數式編程了,大部分只不過是語法糖。是採用什麼編程範式不在於語言的語法,而是在於思惟方式。

面向對象編程(OOP)在過去20多年很是成功,而函數式編程(FP)也不斷地發展,他們相生相息,各自解決不一樣的場景問題:

  • 面向對象能夠理解爲是對數據的抽象,好比把一個事物抽象成一個對象,關注的是數據。
  • 函數式編程是一種過程抽象的思惟,就是對當前的動做去進行抽象,關注的是動做。

現實業務需求每每體現爲業務活動,它是面向過程的,即先輸入數據源,在必定條件下,進行一系列的交互,再輸出結果。那面向過程與函數式的的區別是什麼:

  • 面向過程能夠理解是把作事情的動做進行分解多個步驟,因此有if/while這類語法支撐,走不一樣的分支步驟。
  • 函數式相比面向過程式,它更加地強調執行結果而非執行過程,利用若干個簡單的執行單元讓計算結果不斷漸近,逐層推導複雜的運算,而不是像面向過程設計出複雜的執行過程,因此純函數式編程語言中不須要if/while這類語法,而是模式匹配,遞歸調用等。

面向對象的編程經過封裝可變的部分來構造可以讓人讀懂的代碼,函數式編程則是經過最大程度地減小可變的部分來構造出可以讓人讀懂的代碼。

咱們從Java的Stream實現也看到函數式的另外一個特色:

  • 函數不維護任何狀態,上下文的數據是不變的,傳入的參數據處理完成以後再扔出來。

結合上面的理解,咱們能夠先把世界事物經過OOP抽象爲對象,再把事物間的聯繫與交互經過FP抽象爲執行單元,這種結合或許是對業務活動的實現一種較好的解決方式。

3 避免單一範式

一提到編程範式,很容易聯想到宗教的虔誠,每種宗教所表達信條都有必定合理性,但若是一直只遵循一種教條,可能也被讓本身痛苦不堪。編程範式也是如此,正如Java在1.8以前是純面向對象式,你就會以爲它很是繁瑣。也如Erlang是純函數式,你就會發現有時簡單的邏輯處理會很是複雜。

近些年來,因爲數據分析、科學計算和並行計算的興起,讓人認識到函數式編程解決數據領域的魅力,它也愈來愈受歡迎。在這些領域,程序每每比較容易用數據表達式來表達,採用函數式能夠用不多代碼來實現。

現實的業務軟件,不少的邏輯其實也是對數據的處理,最簡單是對數據的CURD,以及數據的組合、過濾與查詢。因此函數式編程在許多語言中都獲得支持,提高了對數據處理的表達能力。

瞭解新的編程範式在適當的時候使用它們,這會使你事半功倍。不管什麼編程範式,他們都是工具,在你的工具箱中,可能有錘子,螺絲刀…,這個工具在何時使用,取決待解決的問題。

4 結語

本文的案例只是一個引子,主要是想給你帶來函數式編程的一些理念,函數式給咱們解決業務問題提供了另外一種思惟方式:如何高效簡潔地對數據查詢與變換。許多語言都支持函數式一些能力,須要咱們不斷地學習,在合理的場景下使用他們。

本文分享自華爲雲社區《飛哥講代碼16:函數式讓數據處理更簡潔》,原文做者:華爲雲專家。

點擊關注,第一時間瞭解華爲雲新鮮技術~

相關文章
相關標籤/搜索