有了面向對象編程,還須要函數式編程嗎 ? 函數式編程,有什麼妙處 ?java
函數式的理念主要是:編程
這些理念致使的結果是:函數式編程,更容易達到程序行爲可預測和推斷的目標。
數組
本文將帶着「函數式編程的有色眼鏡」,從新審視平常的編程結構。重點是以一種新的視角來看待問題,暫不論及性能、軟件工程等因素。示例代碼採用 Java 編寫,其目的是貼近 Java 開發者的閱讀習慣。同時,也應注意,因爲函數式並非 Java 的主要特性,所以在 Java 中是受限的能力。
網絡
賦值是程序中最基礎的行爲。從函數視角來看,賦值其實是一個常量函數。以下代碼所示。 int i = 100
,實際上能夠用 Supplier<Integer> f = () -> 100
來替代。任何值出現的地方,均可以用值提供器來替代。這樣作,能夠加強函數的靈活性:由於值是固定的,但值源能夠多種多樣:變量、函數、文件、網絡等。app
public class Assignment { public static void main(String[]args) { int i = 100; System.out.println(i); Supplier<Integer> f = () -> 100; System.out.println(f.get()); } }
來看條件怎麼用函數式編程來實現。以下代碼所示,是一個極爲普通的函數,分別根據三種不一樣的條件返回不一樣的值。函數式編程
public static Integer plainIfElse(int x) { if (x > 0) { return 1; } else if (x < 0) { return -1; } else { return 0; } }
怎麼用函數式編程來改造呢 ? 前面說了,函數式編程是將程序當作是一系列函數的組合。首先從plainIfElse 提取出六個基本函數:函數
public static boolean ifPositive(int x) { return x > 0; } public static boolean ifNegative(int x) { return x < 0; } public static boolean ifZero(int x) { return x == 0; } public static Integer positiveUnit() { return 1; } public static Integer negativeUnit() { return -1; } public static Integer zero() { return 0; }
如今的問題是:怎麼組合這些基本函數獲得與 plainIfElse 同樣的效果呢?很容易,將 if-elseif-else 解析爲:性能
if (A) { actA } if (B) { actB } if (C) { actC }
這是一個 Map 結構。key 是條件函數,value 是行爲函數。所以能夠考慮用 Map[Predicate,Supplier] 來模擬,以下代碼所示:3d
public static Supplier<Integer> mapFunc(int x) { Map<Predicate<Integer>, Supplier<Integer>> condMap = new HashMap<>(); condMap.put(Condition::ifPositive, Condition::positiveUnit); condMap.put(Condition::ifNegative, Condition::negativeUnit); condMap.put(Condition::ifZero, Condition::zero); return travelWithGeneric(condMap, x); }
接下來,只要遍歷全部的 key ,找到知足條件函數的第一個 key ,而後調用 value 便可:code
public static <T,R> Supplier<R> travelWithGeneric(Map<Predicate<T>, Supplier<R>> map, T x) { return map.entrySet().stream().filter((k) -> k.getKey().test(x)).findFirst().map((k) -> k.getValue()).get(); }
Emmm ... Seems Perfect .
不過,徹底消除了 if-else 了嗎?並無。 事實上 filter + findFirst 隱式地含有了 if-else 的味道。這說明:沒法完全地消除條件,只是在適當的抽象層次上隱藏了。
進一步思考,if-else 能夠拆成兩個 if-then 語句。 if-then 能夠說是最原子的操做了。順序語句,本質上也是 if-then : if exec current ok , then next ; if exec not ok, exit .
通用IF
既然 if-then 是原子操做,能夠提供幾個方便的函數:
public class CommonIF { public static <T, R> R ifElse(Predicate<T> cond, T t, Function<T, R> ifFunc, Supplier<R> defaultFunc ) { return cond.test(t) ? ifFunc.apply(t) : defaultFunc.get(); } public static <T, R> R ifElse(Predicate<T> cond, T t, Supplier<R> ifSupplier, Supplier<R> defaultSupplier ) { return cond.test(t) ? ifSupplier.get() : defaultSupplier.get(); } public static <T, R> Supplier<R> ifElseReturnSupplier(Predicate<T> cond, T t, Supplier<R> ifSupplier, Supplier<R> defaultSupplier ) { return cond.test(t) ? ifSupplier : defaultSupplier; } public static <T> void ifThen(Predicate<T> cond, T t, Consumer<T> action) { if (cond.test(t)) { action.accept(t); } } public static <T> boolean alwaysTrue(T t) { return true; } }
應用 CommonIF 的函數,能夠改寫 if-elseif-else 爲嵌套的 if-else :
public static Supplier<Integer> ifElseWithFunctional(int x) { return CommonIF.ifElseReturnSupplier(Condition::ifPositive, x, Condition::positiveUnit, CommonIF.ifElseReturnSupplier(Condition::ifNegative, x, Condition::negativeUnit, Condition::zero ) ); }
如今,來看下循環。下面是一段很普通的循環代碼:
Integer sum = 0; List<Integer> list = Arrays.asList(1,2,3,4,5); for (int i=0; i < list.size(); i++) { sum += list.get(i); } System.out.println(sum); Integer multiply = 1; for (int i=0; i < list.size(); i++) { multiply *= list.get(i); } System.out.println(multiply);
聰明的讀者很快就看出了:上述兩段代碼的結構都是對容器的元素進行 reduce ,差別只在於:一個初始值和 reduce 操做符。怎麼將 reduce 這種類似性抽離出來呢 ? 下面的代碼展現了:
public static <T> T reduce(List<T> list, BinaryOperator<T> bifunc, Supplier<T> init) { return list.stream().reduce(init.get(), bifunc); }
如今能夠寫做:
System.out.println("func sum:" + reduce(list, (x,y) -> x+y, () -> 0)); System.out.println("func multiply: " + reduce(list, (x,y) -> x*y, () -> 1));
是否是足夠簡潔 ?咱們發現了函數式編程的一大妙處。
在運用函數視角解析基本編程結構以後,來看一點實際應用。
PipeLine 是函數式編程的典型應用。PipeLine 通俗地說,就是一個流水線,經過一系列工序共同協做完成一個肯定目標。好比說,導出功能,就是「查詢-詳情-過濾-排序-格式化-生成文件-上傳文件」的流水線。以下代碼,展現瞭如何用函數式實現一個 PipeLine : supplier 提供的數據集,通過一系列的 filters 加工,最後通過 format 格式化輸出。
public class PipeLine { public static void main(String[] args) { List<String> result = pipe(PipeLine::supplier, Arrays.asList(PipeLine::sorter, PipeLine::uniq), PipeLine::format); System.out.println(result); } public static <T,R> R pipe(Supplier<List<T>> supplier, List<Function<List<T>, List<T>>> filters, Function<List<T>,R> format) { List<T> result = supplier.get(); for (Function<List<T>, List<T>> filter: filters) { result = filter.apply(result); } return format.apply(result); } public static List<String> supplier() { return Arrays.asList("E20191219221321025200001", "E20181219165942035900001", "E20181219165942035900001", "E20191119165942035900001"); } public static List<String> sorter(List<String> list) { Collections.sort(list); return list; } public static List<String> uniq(List<String> list) { return list.stream().distinct().collect(Collectors.toList()); } public static List<String> format(List<String> list) { return list.stream().map( (s) -> s + " " + s.substring(1,5) + " " + s.substring(6,8) + ":" + s.substring(9,11) + ":" + s.substring(12,14) ).collect(Collectors.toList()); } }
最後,來看一個裝飾器栗子,展現函數組合的強大威力。
你們還隱約記得一個公式: (sinx)^2 + (cosx)^2 = 1。若是要寫成程序,也是很容易的:
double x = Math.pow(sin(x),2) + Math.pow(cos(x), 2);
若是我須要的是 f(x)^2 + g(x)^2 呢?細心的讀者發現了,這裏的結構都是 f(x)^n ,所以將這個結構抽離出來。 pow 對 f 作了個冪次封裝,如今咱們獲得了 F(x) = f(x)^n + g(x)^n 的能力。
/** 將指定函數的值封裝冪次函數 pow(f, n) = (f(x))^n */ public static <T> Function<T, Double> pow(final Function<T,Double> func, final int n) { return x -> Math.pow(func.apply(x), (double)n); }
如今能夠寫做:double x = pow(Math::sin, 2).apply(x) + pow(Math::cos, 2).apply(x);
請注意,這裏 + 仍然是固定的,我但願也不侷限於加號,而是任意可能的操做符,也就是想構造: H(x) = Hop(f(x), g(x))。這樣,就須要支持將 + 這個操做符,以函數參數的形式傳入:
public static <T> Function<BiFunction<T,T,T>, Function<T,T>> op(Function<T,T> funcx, Function<T,T> funcy) { return opFunc -> aT -> opFunc.apply(funcx.apply(aT), funcy.apply(aT)); }
如今能夠寫做:
Function<Double,Double> sumSquare = op(pow(Math::sin, 2), pow(Math::cos, 2)).apply((a,b)->a+b); System.out.println(sumSquare.apply(x));
對於 f(x)^n ,事實上,能夠寫成更抽象的形式: f(g(x)) = y -> f(y), y = x -> g(x) :
/** 將兩個函數組合成一個疊加函數, compose(f,g) = f(g) */ public static <T> Function<T, T> compose(Function<T,T> funcx, Function<T,T> funcy) { return x -> funcx.apply(funcy.apply(x)); } /** 將若干個函數組合成一個疊加函數, compose(f1,f2,...fn) = f1(f2(...(fn))) */ public static <T> Function<T, T> compose(Function<T,T>... extraFuncs) { if (extraFuncs == null || extraFuncs.length == 0) { return x->x; } return x -> Arrays.stream(extraFuncs).reduce(y->y, FunctionImplementingDecrator::compose).apply(x); }
如今,咱們得到了更強的靈活性,能夠任意構造想要的函數:
Function<Double,Double> another = op(compose((d)->d*d, Math::sin), compose((d)->d*d, Math::cos)).apply((a,b)->a+b); System.out.println(another.apply(x)); Function<Double,Double> third = compose(d->d*d, d->d+1, d->d*2, d->d*d*d); // (2x^3+1)^2 System.out.println(third.apply(3d));
這裏展現了函數式編程的強大之處:經過短小的簡單函數,很容易組合出具備強大功能的複合函數。
函數式編程經過任意組合短小簡單的函數,構造具備強大能力的複合函數,同時能夠保持代碼很是簡潔。經過函數式編程訓練,能夠逐步收穫更強大的結構抽象和提煉能力。
讀完本文,你是否從中受到了啓發呢 ?