Nếu đã là fan cứng của Java thì hẳn các bạn cũng biết là kể từ Java 8, Java đã hỗ trợ cho functional programming. Nhưng với những ngôn ngữ OOP không được thiết kế dành cho functional programming (như Java 7) thì thế nào? Có những thư viện mã nguồn mở hỗ trợ lập trình Java theo kiểu functional programming như functionaljava.org. Tuy nhiên, ở bài viết này mình chia sẻ cách áp dụng mà không dùng đến thư viện. Nếu các muốn tìm hiểu về functional programming trên Java 8 thì có thể tự tìm hiểu trên Google, hoặc theo link này.
Mục tiêu của bài viết.
Bài viết này không phải là tutorial, cũng không phải là chia sẻ kiến thức theo kiểu “sách giáo khoa”. Bài viết này chỉ chia sẻ về việc vọc code của mình. Việc vọc code này chỉ giúp chúng ta dễ hình dung hơn về functional programing, và cách mà chúng hoạt động trên Java 8. Chính vì vậy, mình không khuyến khích các bạn áp dụng những chia sẻ này vào thực tế.
Các khái niệm cơ bản của Functional Programming
Áp dụng các khái niệm cơ bản của functional Programming vào Java 7:
First-class functions
Java không phải là ngôn ngữ first-class functions. Chúng ta có thể coi method trong Java là function, tuy nhiên, ở Java 7, chúng ta chưa có cú pháp method reference. Mặc dù Java 8 đã hỗ trợ method reference, nhưng bản chất của nó vẫn là một object (implement functional interface). Tương tự như vậy, ở Java 7, chúng ta có cũng thể coi các object như là các method reference của các method mà nó implement.
Java 8 có hỗ trợ cú pháp lambda để định nghĩa một “function” mới. Trong khi đó Java 7 thì không. Vậy thì ở Java 7 chúng ta sử sụng anonymous inner class để thay cho cú pháp lambda.
Pure functions
Method trong Java có thể là pure function hoặc không. Nếu muốn là pure function, thì chúng ta phải tự giới hạn trong code, sao cho method của chúng ta thõa mãn điều kiện của một pure function là được.
Higher order functions
Khi đã có function (thực chất là Java object), thì những method chấp nhận tham số là function, được xem là higher order function.
Functional interfaces
Ở Java 8, chúng ta có khái niệm functional interface. Ở Java 7, chúng ta cũng có thể dùng khái niệm đó. Tuy nhiên ở Java 7 chúng ta không có sẵn annotation @FunctionalInterface. Ngoài ra Java 8 cũng định nghĩa sẵn các functional interface như Function, Predicate, Consumer… Trên Java 7, chúng ta cũng có thể tự định nghĩa những interface tương tự.
1 2 3 4 5 6 7 8 9 10 11 |
interface MyFunction<T, R> { R apply(T t); } interface MyPredicate<T> { boolean test(T t); } interface MyConsumer<T>{ void accept(T t); } |
Ta có thể định nghĩa higher order function, với tham số là functional interface như sau.
1 2 3 4 |
public static String formatMoney(Double value, String symbol, MyFunction<Double, String> numberFormatter) { return numberFormatter.apply(value) + " " + symbol; } |
Khi sử dụng:
1 2 3 4 5 6 7 8 |
double price = 10000.7; String text = formatMoney(price, "VND", new MyFunction<Double, String>() { @Override public String apply(Double t) { return t.toString(); } }); System.out.println(text); |
Code bên trên tương đương với việc dùng lambda ở Java 8:
1 2 3 |
double price = 10000.7; String text = formatMoney(price, "VND", t -> t.toString()); System.out.println(text); |
Functional Composition
Java 8 hỗ trợ interface default method và interface static method. Do đó ở Predicate, ngoài test, chúng ta còn có and, or, negative, isEqual; Hay ở Function, chúng ta có compose, andThen, identity… Vậy giải pháp thay thế ở Java 7 là gì?
Dùng util method
Trong Java 7 thì chúng ta có thể định nghĩa các util method để thay thế cho interface default method và interface static method.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
class FunctionalUtils { public static <T> MyPredicate<T> and( final MyPredicate<? super T> predicate1, final MyPredicate<? super T> predicate2) { Objects.requireNonNull(predicate1); Objects.requireNonNull(predicate2); return new MyPredicate<T>() { @Override public boolean test(T t) { return predicate1.test(t) && predicate2.test(t); } }; } public static <T> MyPredicate<T> isEqual(final Object o) { return new MyPredicate<T>() { @Override public boolean test(T t) { return Objects.equals(o, t); } }; } } |
Lấy ví dụ với phương thức check như sau:
1 2 3 |
public static boolean check(String text, MyPredicate<String> condition) { return condition.test(text); } |
Khi sử dụng (import static phương thức and):
1 2 3 4 5 6 7 8 9 10 11 12 13 |
MyPredicate<Object> nonNull = new MyPredicate<Object>() { @Override public boolean test(Object o) { return o != null; } }; MyPredicate<String> lowerCase = new MyPredicate<String>() { @Override public boolean test(String s) { return s.toLowerCase().equals(s); } }; boolean isValid = check("hello", and(nonNull, lowerCase)); |
Nếu phương thức check dùng Java 8 Predicate thay vì MyPredicate thì code Java 8 tương đương sẽ là:
1 2 3 |
Predicate<String> nonNull = o -> o != null; Predicate<String> lowerCase = s -> s.toLowerCase().equals(s); boolean isValid = check("hello", nonNull.and(lowerCase)); |
Nhưng khoan. Kiểu của nonNull là Predicate<String>. Thế khi kiểu của nó là Predicate<Object> thì sao? Thì chơi chiêu thôi, thế này nè:
1 2 3 4 5 |
Predicate<Object> nonNull = o -> o != null; Predicate<String> lowerCase = s -> s.toLowerCase().equals(s); //boolean isValid = check("hello", nonNull.and(lowerCase)); boolean isValid = check("hello", ((Predicate<String>) t -> true) .and(nonNull).and(lowerCase)); |
Dùng Inner class, giả default method
Chúng ta cũng có thể giả default method bằng cách tạo một interface có các default method giả. Dùng class Creator để tạo instance cho cho interface đó. Tương tự với static method, chúng ta cũng có thể đưa method vào inner class.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
interface MyFunctionWithDefault<T, R> extends MyFunction<T, R> { <V> MyFunctionWithDefault<T, V> andThen(MyFunctionWithDefault<? super R, ? extends V> function); <V> MyFunctionWithDefault<V, R> compose(MyFunctionWithDefault<? super V, ? extends T> function); public class Creator { public static <T, R> MyFunctionWithDefault<T, R> createFunction(final MyFunction<T, R> func) { return new MyFunctionWithDefault<T, R>() { @Override public <V> MyFunctionWithDefault<T, V> andThen( final MyFunctionWithDefault<? super R, ? extends V> var1) { Objects.requireNonNull(var1); return createFunction(new MyFunction<T, V>() { @Override public V apply(T o) { return var1.apply(func.apply(o)); } }); } @Override public <V> MyFunctionWithDefault<V, R> compose( final MyFunctionWithDefault<? super V, ? extends T> var1) { Objects.requireNonNull(var1); return createFunction(new MyFunction<V, R>() { @Override public R apply(V v) { return func.apply(var1.apply(v)); } }); } @Override public R apply(T o) { return func.apply(o); } }; } } public class Static { public static <T> MyFunction<T, T> identity() { return new MyFunction<T, T>() { @Override public T apply(T t) { return t; } }; } } } |
Sửa lại ví dụ formatMoney lúc nãy:
1 2 3 4 |
public static String formatMoney(Double value, String symbol, MyFunctionWithDefault<Double, String> numberFormatter) { return numberFormatter.apply(value) + " " + symbol; } |
Lúc này ta có thể sử dụng nó như sau (import static phương thức createFunction):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
double price = 10000.7; MyFunctionWithDefault<Double, Long> toLong = createFunction(new MyFunction<Double, Long>() { @Override public Long apply(Double aDouble) { return Math.round(aDouble); } }); MyFunctionWithDefault<Long, String> longFormatter = createFunction(new MyFunction<Long, String>() { @Override public String apply(Long aLong) { return aLong.toString(); } }); String text = formatMoney(price, "VND", toLong.andThen(longFormatter)); System.out.println(text); |
Nếu dùng lambda và reference method của Java 8 thì code sẽ dễ nhìn hơn:
1 2 3 4 5 6 7 |
double price = 10000.7; MyFunctionWithDefault<Double, Long> toLong = createFunction(Math::round); MyFunctionWithDefault<Long, String> longFormatter = createFunction(Object::toString); String text = formatMoney(price, "VND", toLong.andThen(longFormatter)); System.out.println(text); |
Stream API
Khi đã xác định được cách áp dụng functional programming vào Java 7 thì chúng ta cũng có thể áp dụng để implement một thứ gì đó na ná với Stream API. Ở đây, mình đã thử định nghĩa interface MyStream, có một số phương thức giống với Java 8 Stream, bao gồm filter, map, flatMap, reduce, count, forEach.
Interface MyStream được định nghĩa như sau:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
interface MyBinaryOperator<T> { T apply(T t1, T t2); } interface MyStream<T> { MyStream<T> filter(MyPredicate<? super T> condition); <R> MyStream<R> map(MyFunction<? super T, ? extends R> func); <R> MyStream<R> flatMap(MyFunction<? super T, ? extends MyStream<? extends R>> func); T reduce(T initialValue, MyBinaryOperator<T> op); long count(); void forEach(MyConsumer<? super T> consumer); } |
Mình kế thừa lớp ArrayList để làm nguồn cung cấp cho stream.
1 2 3 4 5 |
class MyArrayList<E> extends ArrayList<E> { public MyStream<E> streamMe() { return new ArrayListStream<E>(this); } } |
ArrayListStream sẽ implement MyStream. Tuy nhiên, để giúp tái sử dụng code tốt hơn, mình định nghĩa MyAbstractStream implement MyStream, rồi cho ArrayListStream kế thừa MyAbstractStream. Class MyAbstractStream mình impelent như sau:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
abstract class MyAbstractStream<E> implements MyStream<E> { @Override public MyStream<E> filter(MyPredicate<? super E> condition) { return new FilteredStream<E>(this, condition); } @Override public <R> MyStream<R> map(MyFunction<? super E, ? extends R> func) { return new MappedStream<R, E>(this, func); } @Override public <R> MyStream<R> flatMap(MyFunction<? super E, ? extends MyStream<? extends R>> func) { return new FlatMappedStream<R, E>(this, func); } public E reduce(final E initialValue, final MyBinaryOperator<E> op) { class ReduceConsumer implements MyConsumer<E> { E result = initialValue; @Override public void accept(E e) { result = op.apply(result, e); } } ReduceConsumer consumer = new ReduceConsumer(); forEach(consumer); return consumer.result; } public long count() { return map(new MyFunction<E, Long>() { @Override public Long apply(E e) { return 1l; } }).reduce(0l, new MyBinaryOperator<Long>() { @Override public Long apply(Long t1, Long t2) { return t1 + t2; } }); } } |
Các class FilteredStream, MappedStream, FlatMappedStream được implement như sau:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
class FilteredStream<E> extends MyAbstractStream<E> { private final MyPredicate<? super E> filter; private final MyStream<E> sourceStream; public FilteredStream(MyStream<E> sourceStream, MyPredicate<? super E> filter) { Objects.requireNonNull(filter); Objects.requireNonNull(sourceStream); this.filter = filter; this.sourceStream = sourceStream; } @Override public void forEach(final MyConsumer<? super E> consumer) { sourceStream.forEach(new MyConsumer<E>() { @Override public void accept(E e) { if (filter.test(e)) { consumer.accept(e); } } }); } } class MappedStream<E, S> extends MyAbstractStream<E> { private final MyFunction<? super S, ? extends E> mapper; private final MyStream<S> sourceStream; public MappedStream(MyStream<S> sourceStream, MyFunction<? super S, ? extends E> mapper) { Objects.requireNonNull(mapper); Objects.requireNonNull(sourceStream); this.mapper = mapper; this.sourceStream = sourceStream; } @Override public void forEach(final MyConsumer<? super E> consumer) { sourceStream.forEach(new MyConsumer<S>() { @Override public void accept(S e) { consumer.accept(mapper.apply(e)); } }); } } class FlatMappedStream<E, S> extends MyAbstractStream<E> { private final MyFunction<? super S, ? extends MyStream<? extends E>> mapper; private final MyStream<S> sourceStream; public FlatMappedStream(MyStream<S> sourceStream, MyFunction<? super S, ? extends MyStream<? extends E>> mapper) { Objects.requireNonNull(mapper); Objects.requireNonNull(sourceStream); this.mapper = mapper; this.sourceStream = sourceStream; } @Override public void forEach(final MyConsumer<? super E> consumer) { sourceStream.forEach(new MyConsumer<S>() { @Override public void accept(S e) { MyStream<? extends E> stream = mapper.apply(e); stream.forEach(new MyConsumer<E>() { @Override public void accept(E e) { consumer.accept(e); } }); } }); } } |
Vậy là chúng ta đã có thể sử dụng MyStream và các phương thức của nó rồi.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
MyArrayList<Integer> arrList = new MyArrayList<>(); arrList.add(1); arrList.add(2); arrList.add(3); arrList.add(-1); arrList.add(4); arrList.add(5); arrList.streamMe().filter(new MyPredicate<Integer>() { @Override public boolean test(Integer integer) { return integer >= 0; } }).flatMap(new MyFunction<Integer, MyStream<Float>>() { @Override public MyStream<Float> apply(Integer i) { MyArrayList<Float> tmp = new MyArrayList<>(); if (i != 3) { tmp.add((float) i); if (i % 2 == 0) { tmp.add(i + 0.5f); } } return tmp.streamMe(); } }).forEach(new MyConsumer<Float>() { @Override public void accept(Float aFloat) { System.out.println(aFloat); } }); |
Lưu ý: Những đoạn code này là những thứ na ná Java 8 Stream API. Nó không hoàn toàn giống Stream API và Stream API cũng không phải được implement theo cách này.
Nếu muốn giống Stream API hơn một chút nữa. Thì có thể làm cho nó có thứ na ná thế này:
Exception in thread “main” java.lang.IllegalStateException: stream has already been operated upon or closed
Như thế này:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 |
abstract class MyAbstractStream<E> implements MyStream<E> { protected boolean operatedOrClosed = false; protected void stateHandle() { if (operatedOrClosed) { throw new IllegalStateException("stream has already been operated upon or closed"); } operatedOrClosed = true; } protected MyAbstractStream<E> mFilter(MyPredicate<? super E> condition) { return new FilteredStream<E>(this, condition); } @Override public MyStream<E> filter(MyPredicate<? super E> condition) { stateHandle(); return mFilter(condition); } protected <R> MyAbstractStream<R> mMap(MyFunction<? super E, ? extends R> func) { return new MappedStream<R, E>(this, func); } @Override public <R> MyStream<R> map(MyFunction<? super E, ? extends R> func) { stateHandle(); return mMap(func); } protected <R> MyAbstractStream<R> mFlatMap( MyFunction<? super E, ? extends MyStream<? extends R>> func) { return new FlatMappedStream<R, E>(this, func); } @Override public <R> MyStream<R> flatMap( MyFunction<? super E, ? extends MyStream<? extends R>> func) { stateHandle(); return mFlatMap(func); } protected E mReduce(final E initialValue, final MyBinaryOperator<E> op) { class ReduceConsumer implements MyConsumer<E> { E result = initialValue; @Override public void accept(E e) { result = op.apply(result, e); } } ReduceConsumer consumer = new ReduceConsumer(); mForEach(consumer); return consumer.result; } @Override public E reduce(final E initialValue, final MyBinaryOperator<E> op) { stateHandle(); return mReduce(initialValue, op); } protected long mCount() { return mMap(new MyFunction<E, Long>() { @Override public Long apply(E e) { return 1l; } }).mReduce(0l, new MyBinaryOperator<Long>() { @Override public Long apply(Long t1, Long t2) { return t1 + t2; } }); } @Override public long count() { stateHandle(); return mCount(); } protected abstract void mForEach(MyConsumer<? super E> consumer); @Override public void forEach(MyConsumer<? super E> consumer) { stateHandle(); mForEach(consumer); } } class FilteredStream<E> extends MyAbstractStream<E> { private final MyPredicate<? super E> filter; private final MyAbstractStream<E> sourceStream; public FilteredStream(MyAbstractStream<E> sourceStream, MyPredicate<? super E> filter) { Objects.requireNonNull(filter); Objects.requireNonNull(sourceStream); this.filter = filter; this.sourceStream = sourceStream; } @Override protected void mForEach(final MyConsumer<? super E> consumer) { sourceStream.mForEach(new MyConsumer<E>() { @Override public void accept(E e) { if (filter.test(e)) { consumer.accept(e); } } }); } } class MappedStream<E, S> extends MyAbstractStream<E> { private final MyFunction<? super S, ? extends E> mapper; private final MyAbstractStream<S> sourceStream; public MappedStream(MyAbstractStream<S> sourceStream, MyFunction<? super S, ? extends E> mapper) { Objects.requireNonNull(mapper); Objects.requireNonNull(sourceStream); this.mapper = mapper; this.sourceStream = sourceStream; } @Override protected void mForEach(final MyConsumer<? super E> consumer) { sourceStream.mForEach(new MyConsumer<S>() { @Override public void accept(S e) { consumer.accept(mapper.apply(e)); } }); } } class FlatMappedStream<E, S> extends MyAbstractStream<E> { private final MyFunction<? super S, ? extends MyStream<? extends E>> mapper; private final MyAbstractStream<S> sourceStream; public FlatMappedStream(MyAbstractStream<S> sourceStream, MyFunction<? super S, ? extends MyStream<? extends E>> mapper) { Objects.requireNonNull(mapper); Objects.requireNonNull(sourceStream); this.mapper = mapper; this.sourceStream = sourceStream; } @Override protected void mForEach(final MyConsumer<? super E> consumer) { sourceStream.mForEach(new MyConsumer<S>() { @Override public void accept(S e) { MyStream<? extends E> stream = mapper.apply(e); stream.forEach(new MyConsumer<E>() { @Override public void accept(E e) { consumer.accept(e); } }); } }); } } class ArrayListStream<E> extends MyAbstractStream<E> { private final ArrayList<E> arrayList; public ArrayListStream(ArrayList<E> arrayList) { Objects.requireNonNull(arrayList); this.arrayList = arrayList; } @Override protected void mForEach(MyConsumer<? super E> consumer) { for (E e : arrayList) { consumer.accept(e); } } } |
Kết
Việc áp dụng functional programming cho ngôn ngữ lập trình hướng đối tượng là khả thi. Tuy nhiên việc này sẽ tiêu tốn nhiều thời gian viết code và khả năng tối ưu performance cũng sẽ bị hạn chế. Bởi vì cú pháp ngôn ngữ, và nền tảng không được thiết kế cho functional programming, cũng như thiếu các thư viện, API liên quan đến functional programming.
Bài viết này không định hướng “áp dụng” cho thực tế, mà chỉ định hướng “áp dụng” cho vọc code mà thôi. Chúc các bạn vọc code vui vẻ. Mọi ý kiến đóng góp hoặc thắc mắc, vui lòng comment hoặc inbox m.me/DiepEsc.