函數語言程式設計:如何高效簡潔地對資料查詢與變換

華為雲開發者社群發表於2020-11-16

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

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

 

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

相關文章