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


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

案例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來簡化計算過程,並對代碼進行了分段:

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:函數式讓數據處理更簡潔》,原文作者:華為雲專家。

 

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM