從五大語言看函式和lambda表示式

張風捷特烈發表於2019-03-06

零、前言

最近Kotlin看得挺爽,曾經比較Java和JavaScript,
遺憾過Java的函式太low,Kotlin在函式方面完全彌補了Java的缺憾。
雖然java8支援了lambda表示式,但是還是沒有kotlin爽。
今天只談函式和lambda,至於函數語言程式設計,就不班門弄斧了。


一、從Kotlin的函式說起

在java中似乎並不怎麼說函式,而是說方法,方法是物件的行為能力,那函式是什麼?

0.函式是什麼?

高中的數學是這樣定義函式這個概念的:

設A,B為非空的數集,如果按照某種確定的對應關係f,  
使對於集合A中的任意的任意一個數x,在集合B中都有唯一確定的數f(x)和它對應,
那麼就稱"f:A→B"為從集合A到集合B的一個函式,記作:
y=f(x),x∈A

其中,x叫做自變數,x的取值範圍叫做函式的[定義域]
與x的值對應的y值叫做函式值,函式值的集合{f(x)|x∈A}叫做函式的[值域]
複製程式碼

從五大語言看函式和lambda表示式

數學中一元函式的組成是兩個集合和一個對應法則,
每個自變數在對應法則的對映下都能獲得唯一因變數。 我更願意將數學中的函式看做對應法則下,自變數的所以變化集合
這貌似和程式設計中的函式是兩個概念,但是在思想上還是有相似之處的:

如果將自變數看做輸入狀態,在對應法則之下,每個輸入都對應著唯一對應的輸出狀態  
而程式設計中的函式也是做類似的事:將輸入的材料資料通過邏輯處理,形成特定輸出,只是變化維度(引數)比較多
複製程式碼

1.Kotlin中函式的形式

Kotlin函式定義的形式.png

拿下面的函式來說,對於輸入x總能保持唯一的y輸出

fun fx(x: Int): Int {
    val y = x + 2
    return y
}
複製程式碼

-- 也許你會說:"這TM不就是加個2嗎,需要講的這麼費勁?",
-- 我想說:"不要太糾結表象,我寫成val y = Math.sqrt(Math.exp(x) - 3 * Math.acos(x)) - Math.log(x)就會很高大上嗎?"
-- 在我眼中,這只是一種對應關係,它的本質和它的表示並沒有關係,就算寫成val y = 1,它的本質並不會改變:
-- 仍是對於輸入x總能保持唯一的y輸出,這就是抽象,太在意表象就會膚淺以致視野的侷限。


2.Kotlin中函式的型別

Kotlin中的函式也是一種資料型別,其型別為:(形參型別,形參型別)->返回值型別
在Kotlin中使用::函式名獲取一個函式的引用,函式是可以作為一個物件存在的

val line: (Double) -> Double
line = ::fx
line(8.0)//10.0

println(line)//fun fx(kotlin.Double): kotlin.Double
println(line is (Double) -> Double)//true

|-- 從效果上,普通視野來看就是讓入參+2,並沒有什麼了不起的
|-- 但從整個巨集觀來看該函式實現了一個 y = x + 2 的線性資料轉換器,是不是高大上了一點
複製程式碼

3.函式的入參

現在有一個gx,實現y=e^x的資料轉化器。

從五大語言看函式和lambda表示式

fun gx(x: Double): Double {
    val y = Math.exp(x)
    return y
}
複製程式碼

你也許可以想到:既然函式可以作為物件,那麼也可以當做入參
然後就一不小心拼出了下面這個看起來挺帥氣的函式,這時讓fx作為入參
腳指頭想想應該也知道是y = e^(x+2),這就實現了兩個函式的疊合。

從五大語言看函式和lambda表示式

fun gx(x: Double, f: (Double) -> Double): Double {
    val y = Math.exp(f(x))
    return y
}

println(gx(0.0, ::fx))//7.38905609893065
複製程式碼

4.Lambda閃亮登場

入參是函式,函式可以寫成Lambda表示式,這裡gx的函式入參型別:(Double) -> Double
對應的Lambda表示式形式為:{ 引數名:Double -> 若干語句 最後一句返回Double},
然後下面圖形的資料轉換器就ok了,將自變數x通過sin轉換器後,再通過exp轉化器,也可得到唯一的輸出

從五大語言看函式和lambda表示式

|-- 使用匿名函式,不用Lambda
gx(5.0, fun(x: Double): Double {
    return Math.sin(x)
})

|-- 使用已存在的函式,不用Lambda
gx(5.0, ::sin)

|-- 使用Lambda,標準型--------------------
gx(5.0, { x: Double ->
    Math.sin(x)
})//0.3833049951722714

|-- Lambda特性:作為最後一參可置後--------------
gx(5.0) { x: Double ->
    Math.sin(x)
}//0.3833049951722714

|-- 可推匯出變數型別,變數型別可省略------------------
gx(5.0) { x ->
    Math.sin(x)
}//0.3833049951722714

|-- 只有一個引數時可以用it代替,省略變數---------------
|-- 這樣一看是不是對Kotlin的Lambda語法有了些認識
gx(5.0) {Math.sin(it)}//0.3833049951722714
複製程式碼

好了,Lambda的引入完成,也許你有點暈,沒關係,繼續看


二、從map函式看lambda表示式

1.基上所有的語言都有map等操作符,拿Kotlin來看
val ints = IntArray(10) { it }//初始化 0 1 2 3 4 5 6 7 8 9

ints.map {
    it * it
}.forEach { print("$it "); }//0 1 4 9 16 25 36 49 64 81
複製程式碼

2.Array的map函式原始碼分析
---->[_Arrays.kt#map]-----------------------
public inline fun <R> IntArray.map(transform: (Int) -> R): List<R> {
    return mapTo(ArrayList<R>(size), transform)
}
|-- map函式的入參是 (Int) -> R 型別的函式,返回值是 List<R>
|-- 它呼叫了mapTo方法

---->[_Arrays.kt#mapTo]-----------------------
public inline fun <R, C : MutableCollection<in R>> IntArray.mapTo(destination: C, transform: (Int) -> R): C {
    for (item in this)
        destination.add(transform(item))
    return destination
}
|-- 這方法頭有點長,仔細看看:方法入參 destination,型別C,其中C是MutableCollection型別的  
|-- 從上面傳入的ArrayList<R>(size)來看,是一個size尺寸的空列表,第二參仍是剛才的函式transform
|-- 讓this的所有元素經過transform方法,然後加入到空列表裡,再將destination返回出去
|-- 這樣一看map方法也沒有想象中的那麼神奇,也可以看出map並不會汙染原陣列
複製程式碼

3.Java中的stream流中的map

關於lambda表示式在Java中最常見的應數一個方法的介面,在stream流中便是家常便飯

List<Integer> ints = Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9);
List<Integer> list = ints.stream()
        .map((e) -> {
            return e * e;
        })
        .collect(Collectors.toList());

|-- 簡寫形式
List<Integer> list = ints.stream()
        .map(e -> e * e)
        .collect(Collectors.toList());

---->[Java中的lambda表示式是什麼?]----------------
|-- 原始碼:Stream#map------------
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
|-- 可以看出入參是一個Function的型別,有兩個泛型 T 和 R

|-- 那Function物件又是什麼鬼?
@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
    default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
        Objects.requireNonNull(before);
        return (V v) -> apply(before.apply(v));
    }

    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (T t) -> after.apply(apply(t));
    }
    
    static <T> Function<T, T> identity() {
        return t -> t;
    }
}
|-- Functions是一個介面,有兩個泛型:T和R ,apply函式出入T型別引數,返回一個R 型別值
 * @param <T> the type of the input to the function 輸入的型別
 * @param <R> the type of the result of the function 輸出的型別
|-- 其中有 compose和andThen兩個預設的構造介面,看樣子compose可以截胡,先走一波before的Function  
|-- andThen相反,先走自己的apply,然後再走after的apply
|-- 打個比方,我有一塊糖,compose是吃了吐出來再給我吃,andThen是我吃了,吐出來給她吃

|-- 變數提取一下,可以看出這裡是一個Function<Integer, Integer>的物件
Function<Integer, Integer> fn = e -> e * e; 
fn.apply(8);//64
fn.compose((Integer e) -> {
            System.out.println();
            return e * 2;
        }).apply(8)//256 = (8*2)^2

fn.andThen((Integer e) -> {
            System.out.println();
            return e * 2;
        }).apply(8));//128 = 8*8 *2
複製程式碼

4.JavaScript中的lambda表示式

類似,也是不會改變原陣列

let arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
let result = arr.map(e => {
    return e * e;
});

console.log(arr);//[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
console.log(result);//[ 0, 1, 4, 9, 16, 25, 36, 49, 64, 81 ]

|-- 簡寫形式:
let result = arr.map(e => e * e);
複製程式碼

5.Python中的lambda表示式

Python的lambda表示式怎麼多行語句...還望指點,網上的都是一行...

arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
result = map(lambda e: {e * e}, arr)
print(arr)# [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(list(result))  # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

|-- 簡寫
result = map(lambda e: e * e, arr)
複製程式碼

6.Dart中的lambda
var arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
var result = arr.map((e) => (e * e));
print(arr);//[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(result);//(0, 1, 4, 9, 16, 25, 36, 49, 64, 81)

|-- 簡寫
var result = arr.map((e) => e * e);
複製程式碼

可見,每種語言對於lambda表示式的表示形式都有區別,
下面是各語言未簡寫的完整和簡寫的lambda表示式

|-- Kotlin
val fn = { e: Int -> {
        e * e
    }
}
簡寫:val fn = { e: Int -> e * e }

|-- Java
Function<Integer, Integer> fn = (Integer e) -> {
    return e * e;
};
簡寫:Function<Integer, Integer> fn =  e -> e * e;

|-- JavaScript
let fn = (e) => {
    return e * e
};

簡寫:let fn = (e) => e * e;

|-- Python
fn = lambda e: {
    e * e
}
簡寫:fn = lambda e: e * e

|-- Dart
var fn = (e) => (
    e * e
);
簡寫:var fn = (e) => e * e;
複製程式碼

三、從加法來看lambda表示式

lambda表示式只是函式的一種特別的書寫格式,它本身還是函式,可以賦給變數以及呼叫

1.Kotlin版
|-- 加法函式
fun add(x: Int, y: Int): Int {
    return x + y
}

|-- 轉化為lambda表示式
val add = { x: Int, y: Int -> { x + y } }
簡寫:val add = { x: Int, y: Int ->  x + y }
|-- 可以將lambda表示式當做普通的函式來呼叫
add(3, 5)//8

|-- 再看傳入一個函式如參的add方法,它在加之前先對x,y進行處理
fun add(x: Int, y: Int, fn: (Int) -> Int): Int {
    return fn(x) + fn(y)
}

|-- 這樣就可以計算x,y的平方和:(-3)^2+4^2=25  
val result = add(-3, 4) { e -> e * e }

|-- 這樣就可以計算x,y的絕對值和:|-3|+|4| = 7
val result = add(-3, 4) { e -> Math.abs(e) }
|-- 好處不言而喻,可以自定義擴充用法,應你所需

|-- 當然如果你覺得麻煩,就像加一下而已,也可以設個預設值
fun add(x: Int, y: Int, fn: (Int) -> Int = { e -> e }): Int {
    return fn(x) + fn(y)
}
val result = add(-3, 4) //1
複製程式碼

2.Java版

Java中並不像當代語言那麼隨性,由上面的Function也可以看出,
是介面讓Java支援lambda表示式的,既然Java有Function介面,我們當然也可以自定義

---->[定義方法介面]------------------
public interface AddFun<T, R> {
    R apply(T x, T y);
}

|-- 使用--------------------
AddFun<Integer, Integer> add = (x, y) -> x + y;//加法的lambda表示式
Integer result = add.apply(4, 5);

|-- 如何向上面那樣自定義擴充加法呢? 
|-- 也就是再加一個(函式)入參,可以傳入lambda表示式
public interface AddFun<T, R> {
    R apply(T x, T y, Function<? super T, ? extends R> rule);
}

AddFun<Integer, Integer> add = (x, y, rule) -> rule.apply(x) + rule.apply(y);//加法的lambda表示式
Integer result = add.apply(3, 4, e -> e * e);//25
Integer result = add.apply(-3, 4, e -> Math.abs(e));//7
Integer result = add.apply(-3, 4, Math::abs);//7  簡寫
複製程式碼

3.JavaScript版
|-- 加法函式寫成lambda表示式
let la = (x, y) => x + y;
console.log(la(3, 4));//7

|-- 加法 + lambda表示式入參
function add(x, y, fn = e => e) {             
    return fn(x) + fn(y);
}

let a = add(-3, 4, e => e * e);
let b = add(-3, 4, e => Math.abs(e));

console.log(a);//25
console.log(b);//7

|-- 合在一起寫也可以
let la = (x, y, fn) => fn(x) + fn(y);
la(-3, 4,e => e * e);//25

複製程式碼

4.Python和Dart

套路都差不多,就不廢話了

|-- Python
add = lambda x, y: x + y
addex = lambda x, y, fn: fn(x) + fn(y)
a = add(3, 4)
b = addex(-3, -4, lambda e: e * e)
print(a)#7
print(b)#25

|-- Dart
var add = (x, y)=> x + y;
var addex = (x, y, fn) => fn(x) + fn(y);
var a = add(3, 4);
var b = addex(-3, -4, (e)=> e * e);
print(a);//7
print(b);//25
複製程式碼

四、最後講幾個高階函式吧

Java的stream流對叢集元素的操作,Kotlin對叢集元素的操作,傳入函式,使用lambda表示式很方便
另外JavaScript,Python,Dart操作叢集時或多或少都會涉及這些forEach,map,all,any,reduce等。

1.Java的stream
|-- forEach操作:遍歷元素
ints.stream().forEach(e->{
    System.out.println(e);
});

|-- allMatch操作:根據條件控制遍歷,看是否全部符合條件,只要有一個不合格,中斷遍歷並返回false
List<Integer> ints = Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9);
Stream<Integer> stream = ints.stream();
boolean b = stream.allMatch(e -> {
    System.out.println(e);
    return e < 5; //0 1 2 3 4 5
});
System.out.println(b);//false 返回是否全部都符合要求

|-- anyMatch操作:根據條件控制遍歷,看是否有符合條件,只要有一個合格,中斷遍歷並返回true
boolean has = ints.stream().anyMatch(e -> {
    System.out.println(e);
    return e > 5; //0 1 2 3 4 5 6
});
System.out.println(has);//true

|-- noneMatch操作:根據條件控制遍歷,看是否有符合條件,只要有一個合格,中斷遍歷並返回false
boolean hasNot = ints.stream().noneMatch(e -> {
    System.out.println(e);//0 1 2 3 4 5 6
    return e >5 ;
});
System.out.println(hasNot);//false

|-- filter操作:過濾出需要的元素,返回的仍是stream,所以可以連續使用
ints.stream().filter(e -> e % 2 == 0)
        .forEach(System.out::println);//0 2 4 6 8

|-- map操作:可以將所有的元素按照規則全體變化,返回的仍是stream
|-- collect操作:將一個stream變成Collector,容器物件
List<Integer> list = ints.stream()
        .map(e -> e * e)
        .collect(Collectors.toList());
System.out.println(list);

|-- flatMap操作:將層級結構扁平化。比如有三個小偷,每個人偷了幾個東西(集合元素)  
|-- 然後三個人被警察逮到了,三個人一次將自己偷得東西一個一個擺在桌子上,ok,這就是flatMap
List<Integer> int0to4 = Arrays.asList(0, 1, 2);
List<Integer> int3o7 = Arrays.asList(3, 4);
List<Integer> int4to8 = Arrays.asList(4, 5);
Stream.of(int0to4, int3o7, int4to8).flatMap(list -> list.stream())
        .forEach(System.out::println);//0 1 2 3 4 4 5

|-- limit操作:擷取前n個元素,返回的仍是stream
|-- skip操作:跳過前n個元素,返回的仍是stream
ints.stream()
        .limit(6)//擷取6個 0,1,2,3,4,5
        .skip(2)//跳過前兩個
        .forEach(System.out::println);//2 3 4 5
        
|-- findFirst:獲取流中的第一個元素
int str =  ints.stream()
        .filter(x->x<-3)//過濾流
        .findFirst()//第一個
        .orElse(10000);//預設值
System.out.println(str);//4

|-- mapToInt:形成int流,好處在於有額外的API
IntSummaryStatistics stats = ints.stream().mapToInt((x) -> x).summaryStatistics();
System.out.println("max : " + stats.getMax());//9
System.out.println("min : " + stats.getMin());//0
System.out.println("sum : " + stats.getSum());//45
System.out.println("ave : " + stats.getAverage());//4.5
System.out.println("count : " + stats.getCount());//10

|-- max和min操作,兩者相反,傳入一個比較器,返回一個Optional物件
 int max = ints.stream().max((o1, o2) -> o1 - o2).get();
 int min = ints.stream().min((o1, o2) -> o1 - o2).get();
 System.out.println(max+"--"+min);//9--0

|-- reduce操作:
Integer reduce = ints.stream().reduce(0, (result, value) -> {
    System.out.println(result + "---" + value);
    return result + value;
});
System.out.println(reduce);
感覺reduce超有意思:感覺的話像貪吃蛇,一個一個吃,但吃下一個之前,吃前一個的效果還在  
其中第一參是偏移量,可以看成貪吃蛇得初始情況,在此基礎上,每遍歷一次,吃一個
            0---0                                        4---0
            0---1                                        4---1
            1---2                                        5---2
            3---3                                        7---3
初始值0      6---4                         初始值4        10---4
            10---5                                       14---5
            15---6                                       19---6
            21---7                                       25---7
            28---8                                       32---8
            36---9                                       40---9
            45                                           49
複製程式碼

2.Kotlin
|-- forEach操作:遍歷元素
ints.forEach {
    print("$it ")//0 1 2 3 4 5 6 7 8 9
}

|-- all操作:根據條件控制遍歷,看是否全部符合條件,只要有一個不合格,中斷遍歷並返回false
val b = ints.all {
    println(it);
    it < 5; //0 1 2 3 4 5
}
println(b) //false 

|-- any操作:根據條件控制遍歷,看是否有符合條件,只要有一個合格,中斷遍歷並返回true
 val any = ints.any {
     println(it);
     it > 5;////0 1 2 3 4 5 6
 }
 println(any)//true

|-- noneMatch操作:根據條件控制遍歷,看是否有符合條件,只要有一個合格,中斷遍歷並返回false
val any = ints.none() {
    println(it);
    it > 5;//0 1 2 3 4 5 6
}
println(any)//false

|-- filter操作:過濾出需要的元素,不損壞原陣列
ints.filter {
    it % 2 == 0
}.forEach { print("$it "); }//0 2 4 6 8

|-- map操作:可以將所有的元素按照規則全體變化,返回的仍是stream
ints.map {
    it * it
}.forEach { print("$it "); }//0 1 4 9 16 25 36 49 64 81

|-- dropWhile操作:知道滿足條件之前的元素都刪除
val list = ints.dropWhile { it < 6 }
println(list)//[6, 7, 8, 9]

|-- reduce操作:
val reduce = ints.reduce { result: Int, value: Int ->
    println("$result --- $value")
    result + value
}
println(reduce)
複製程式碼

最後總結一句:在Java中的lambda表示式表示一個介面物件,在各現代語言表示函式

var la={x: Int ,y:Int-> x +y}
println(la is (Int, Int) -> Int)//true
println(::add is (Int, Int) ->Int)//true

fun add(x: Int, y: Int): Int {
    return x + y
}

複製程式碼

關於各語言認識深淺不一,如有錯誤,歡迎批評指正。


後記:捷文規範

1.本文成長記錄及勘誤表
專案原始碼 日期 附錄
V0.1--無 2018-3-6

釋出名:從五大語言看函式和lambda表示式
捷文連結:https://juejin.im/post/5c7a9595f265da2db66df32c

2.更多關於我
筆名 QQ 微信
張風捷特烈 1981462002 zdl1994328

我的github:https://github.com/toly1994328
我的簡書:https://www.jianshu.com/u/e4e52c116681
我的簡書:https://www.jianshu.com/u/e4e52c116681
個人網站:http://www.toly1994.com

3.宣告

1----本文由張風捷特烈原創,轉載請註明
2----歡迎廣大程式設計愛好者共同交流
3----個人能力有限,如有不正之處歡迎大家批評指證,必定虛心改正
4----看到這裡,我在此感謝你的喜歡與支援

icon_wx_200.png

相關文章