Статьи

Функциональное программирование с помощью лямбда-выражений Java 8 — монады

Что такое монада ?: Монада — это концепция шаблонов проектирования, используемая в большинстве функциональных языков программирования, таких как lisp или в современном мире clojure или scala. (Я бы на самом деле скопировал несколько вещей из Scala.) Теперь, почему это становится важным в Java? Потому что в java появилась новая лямбда-функция с версии 8. Лямбда или замыкание — это функциональная функция программирования. Это позволяет вам использовать блоки кода в качестве переменных и позволяет передавать их как таковые. Я обсуждал Java Project Project Lambda в моей предыдущей статье Что готовит в Java 8 — Project Lambda . Теперь вы можете попробовать его в предварительной версии JDK 8, доступной здесь . Теперь мы можем сделать монады до Java 8? Конечно, в конце концов, лямбда в Java семантически — это просто еще один способ реализации интерфейса (на самом деле это не так, потому что компилятор знает, где он используется), но это будет намного более сложный код, который в значительной степени убьет его полезность.

Теперь, вместо того, чтобы описывать абстрактную и, казалось бы, бессмысленную для вас идею, позвольте мне установить вариант использования в Java, как это было бы без монад.

Досадные проверки на нуль: если вы написали какую-нибудь нетривиальную (например, Hello-World) Java-программу, вы, вероятно, сделали несколько проверок на ноль. Они как неизбежное зло программирования, без них вы не можете обойтись, но они делают вашу программу загроможденной шумом. Давайте возьмем следующий пример с набором объектов данных Java. Обратите внимание, я не использовал геттеры или сеттеры, которые в любом случае являются анти-шаблонами

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public static class Userdetails{
    public Address address;
    public Name name;
     
}
 
public static class Name{
    public String firstName;
    public String lastName;       
}
 
public static class Address{
    public String houseNumber;
    public Street street;
    public City city;
     
}
 
public static class Street{
    public String name;       
}
 
public static class City{
    public String name;       
}

Теперь предположим, что вы хотите получить доступ к названию улицы от пользователя UserDetails с возможностью того, что любое свойство будет нулевым. Без монад вы, вероятно, написали бы код, подобный следующему.

1
2
3
4
5
6
7
8
if(user == null )
    return null;
else if(user.address == null)
    return null;
else if(user.address.street == null)
    return null;
else
    return user.address.street.name;

В идеале он должен быть однострочным. У нас так много шума вокруг кода, который нас действительно волнует. Итак, давайте посмотрим, как мы можем это исправить. Давайте создадим класс Option, который представляет необязательное значение. И давайте тогда получим метод map, который запустит лямбда-выражение для своего упакованного значения и вернет другую опцию. Если перенесенное значение равно нулю, оно вернет Option, содержащий ноль, без обработки лямбда-выражения, таким образом избегая исключения нулевого указателя. Обратите внимание, что метод карты должен фактически принимать лямбду в качестве параметра, но нам нужно будет создать интерфейс SingleArgExpression для поддержки этого.

SingleArgExpression.java

1
2
3
4
5
6
7
package com.geekyarticles.lambda;
 
 
public interface SingleArgExpression<P, R> {
     
    public R function(P param);
}

Option.java

01
02
03
04
05
06
07
08
09
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
package com.geekyarticles.javamonads;
 
import com.geekyarticles.lambda.
 
public class Option<T> {
    T value;
     
    public Option(T value){
        this.value = value;
    }
    public <E> Option<E> map(SingleArgExpression<T,E> mapper){
        if(value == null){
            return new Option<E>(null);
        }else{
            return new Option<E>(mapper.function(value));
        }
         
    }   
     
    @Override
    public boolean equals(Object rhs){
        if(rhs instanceof Option){
            Option o = (Option)rhs;
            if(value == null)
                return (o.value==null);
            else{
                return value.equals(o.value);
            }
        }else{
            return false;
        }
         
    }
     
    @Override
    public int hashCode(){
        return value==null? 0 : value.hashCode();
    }
     
    public T get(){
        return value;
    }
     
}

OptionExample.java

01
02
03
04
05
06
07
08
09
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
package com.geekyarticles.javamonads.examples;
 
import com.geekyarticles.javamonads.
 
 
public class OptionExample{
    public static class Userdetails{
        public Option<Address> address = new Option<>(null);
        public Option<Name> name = new Option<>(null);
         
    }
     
    public static class Name{
        public Option<String> firstName = new Option<String>(null);
        public Option<String> lastName = new Option<String>(null);       
    }
     
    public static class Address{
        public Option<String> houseNumber;
        public Option<Street> street;
        public Option<City> city;
         
    }
     
    public static class Street{
        public Option<String> name;       
    }
     
    public static class City{
        public Option<String> name;       
    }
         
    public static void main(String [] args){
        Option<Userdetails> userOpt =  new Option<>(new Userdetails());
         
        //And look how simple it is now
        String streetName = userOpt.flatMap(user -> user.address).map(address -> address.street).map(street -> street.name).get();
        System.out.println(streetName);
         
    }
     
}

Так что теперь, в сущности, идея состоит в том, чтобы возвращать Option всякий раз, когда у метода есть шанс вернуть null. Это гарантирует, что потребитель метода понимает, что значение может быть нулевым, а также позволяет потребителю неявно проходить мимо нулевых проверок, как показано. Теперь, когда мы возвращаем Option из всех наших методов, которые могут возвращать null, вполне вероятно, что выражения внутри карты также будут иметь Option в качестве возвращаемого типа. Чтобы избежать вызова get () каждый раз, у нас может быть похожий метод flatMap, который совпадает с map, за исключением того, что он принимает Option в качестве возвращаемого типа для лямбда-выражения, переданного ему.

1
2
3
4
5
6
7
public <E> Option<E> flatMap(SingleArgExpression<T, Option<E>> mapper){
    if(value == null){
        return new Option<E>(null);
    }
    return  mapper.function(value);
     
}

Последний метод, о котором я бы сказал, — это фильтр. Это позволит нам поместить условие if в цепочку карт, чтобы значение получалось только тогда, когда условие истинно. Обратите внимание, что это также нуль-безопасно. Использование фильтра не очевидно в этой конкретной монаде, но мы увидим его использование позже. Ниже приведен пример, где все пустые поля были обновлены до Option, и, следовательно, flatMap используется instread of map.

Option.java

01
02
03
04
05
06
07
08
09
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
package com.geekyarticles.javamonads;
 
import com.geekyarticles.lambda.
 
public class Option<T> {
    T value;
     
    public Option(T value){
        this.value = value;
    }
    public <E> Option<E> map(SingleArgExpression<T,E> mapper){
        if(value == null){
            return new Option<E>(null);
        }else{
            return new Option<E>(mapper.function(value));
        }
         
    }
    public <E> Option<E> flatMap(SingleArgExpression<T, Option<E>> mapper){
        if(value == null){
            return new Option<E>(null);
        }
        return  mapper.function(value);
         
    }
    public Option<T> filter(SingleArgExpression<T, Boolean> filter){
        if(value == null){
            return new Option<T>(null);
        }else if(filter.function(value)){
            return this;
        }else{
            return new Option<T>(null);
        }
         
    }
     
    @Override
    public boolean equals(Object rhs){
        if(rhs instanceof Option){
            Option o = (Option)rhs;
            if(value == null)
                return (o.value==null);
            else{
                return value.equals(o.value);
            }
        }else{
            return false;
        }
         
    }
     
    @Override
    public int hashCode(){
        return value==null? 0 : value.hashCode();
    }
     
    public T get(){
        return value;
    }
     
}

OptionExample.java

01
02
03
04
05
06
07
08
09
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
package com.geekyarticles.javamonads.examples;
 
import com.geekyarticles.javamonads.
 
 
public class OptionExample{
    public static class Userdetails{
        public Option<Address> address = new Option<>(null);
        public Option<Name> name = new Option<>(null);
         
    }
     
    public static class Name{
        public Option<String> firstName = new Option<String>(null);
        public Option<String> lastName = new Option<String>(null);       
    }
     
    public static class Address{
        public Option<String> houseNumber;
        public Option<Street> street;
        public Option<City> city;
         
    }
     
    public static class Street{
        public Option<String> name;       
    }
     
    public static class City{
        public Option<String> name;       
    }
         
    public static void main(String [] args){
        //This part is just the setup code for the example to work
        Option<Userdetails> userOpt =  new Option<>(new Userdetails());
        userOpt.get().address = new Option<>(new Address());
        userOpt.get().address.get().street=new Option<>(new Street());
        userOpt.get().address.get().street.get().name = new Option<>("H. Street");
         
         
        //And look how simple it is now
        String streetName = userOpt.flatMap(user -> user.address).flatMap(address -> address.street).flatMap(street -> street.name).get();
        System.out.println(streetName);
         
    }
     
}

Коллекции и монады: Монады могут быть полезны и для каркасов коллекций. Хотя лучшим способом было бы, чтобы каждый класс коллекции был самим монадами для лучшей производительности (которым они могут стать в будущем), в настоящее время мы можем их обернуть. Это также создает проблему необходимости ломать систему проверки типов, потому что мы заранее не знаем универсальный тип возврата компоновщика.

NoArgExpression.java

1
2
3
4
5
6
7
package com.geekyarticles.lambda;
 
 
public interface NoArgExpression<R> {
     
    public R function();
}

SingleArgExpression.java

1
2
3
4
5
6
7
package com.geekyarticles.lambda;
 
 
public interface SingleArgExpression<P, R> {
     
    public R function(P param);
}

CollectionMonad.java

01
02
03
04
05
06
07
08
09
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
package com.geekyarticles.javamonads;
 
import com.geekyarticles.lambda.
import java.util.Collection;
import java.util.ArrayList;
import java.util.Arrays;
 
public class CollectionMonad<T> {
    Collection<T> value;
    NoArgExpression<Collection> builder;
     
    public CollectionMonad(Collection<T> value, NoArgExpression<Collection> builder){
        this.value = value;
        this.builder = builder;
    }
     
    public CollectionMonad(T[] elements){
        this.value = new ArrayList<T>(elements.length);
        this.value.addAll(Arrays.asList(elements));
        this.builder = () -> new ArrayList();
         
    }
     
    @SuppressWarnings("unchecked")
    public <E> CollectionMonad<E> map(SingleArgExpression<T,E> mapper){
         
        Collection<E> result = (Collection<E>)builder.function();       
        for(T item:value){
            result.add(mapper.function(item));
        }   
             
        return new CollectionMonad<E>(result, builder);       
         
    }
     
    //What flatMap does is to flatten out the CollectionMonad returned by the lambda that is provided
    //It really shrinks a nested loop.
    @SuppressWarnings("unchecked")
    public <E> CollectionMonad<E> flatMap(SingleArgExpression<T, CollectionMonad<E>> mapper){
         
        Collection<E> result = (Collection<E>)builder.function();       
        for(T item:value){
            CollectionMonad<E> forItem = mapper.function(item);
            for(E e : forItem.get()){
                result.add(e);
            }
        }
        return new CollectionMonad<E>(result, builder);
    }
     
    @SuppressWarnings("unchecked")
    public CollectionMonad<T> filter(SingleArgExpression<T, Boolean> filter){
         
        Collection<T> result = (Collection<T>)builder.function();       
        for(T item:value){
            if(filter.function(item)){
                result.add(item);
            }
             
        }               
        return new CollectionMonad<T>(result, builder);           
    }
     
    public Collection<T> get(){
        return value;
    }
     
    @Override
    public String toString(){       
        return value.toString();
    }
     
}

ListMonadTest.java

01
02
03
04
05
06
07
08
09
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
package com.geekyarticles.javamonads.examples;
 
import com.geekyarticles.javamonads.
import java.util.
 
 
public class ListMonadTest {
    public static void main(String [] args){
        mapExample();
        flatMapExample();
        filterExample();
         
    }
     
    public static void mapExample(){
        List<Integer> list = new ArrayList<>();
        list.add(10);
        list.add(1);
        list.add(210);
        list.add(130);
        list.add(2);
        CollectionMonad<Integer> c = new CollectionMonad<>(list, () -> new ArrayList());
         
        //Use of map
        System.out.println(c.map(v -> v.toString()).map(v -> v.charAt(0)));
        System.out.println();
         
    }
     
    public static void flatMapExample(){
        List<Integer> list = new ArrayList<>();
        list.add(10);
        list.add(1);
        list.add(210);
        list.add(130);
        list.add(2);
        CollectionMonad<Integer> c = new CollectionMonad<>(list, () -> new ArrayList());
         
        //Use of flatMap
        System.out.println(c.flatMap(v -> new CollectionMonad<Integer>(Collections.nCopies(v,v), () -> new ArrayList())));
        System.out.println();
         
    }
     
     
    public static void filterExample(){
        List<Integer> list = new ArrayList<>();
        list.add(10);
        list.add(1);
        list.add(210);
        list.add(130);
        list.add(2);
        CollectionMonad<Integer> c = new CollectionMonad<>(list, () -> new ArrayList());
         
        //Use of flatMap and filter
        System.out.println(c.flatMap(v -> new CollectionMonad<Integer>(Collections.nCopies(v,v), () -> new ArrayList())).filter(v -> v<=100));
        System.out.println();
         
    }
     
}

На первый взгляд может показаться, что использование flatmap представляет собой небольшую проблему, поскольку нам нужно создать CollectionMonad из лямбды. Но если вы подумаете об эквивалентном коде с вложенным циклом for, он все равно будет довольно аккуратным.

Потоки и монады. На данном этапе вы можете подумать о InputStream (s), но мы обсудим нечто более общее, чем это. Поток — это, по сути, последовательность, которая, возможно, бесконечна. Он может быть создан, например, с использованием формулы или действительно InputStream. У нас будут потоки с методами hasNext () и next (), как у Iterator. Фактически мы будем использовать интерфейс Iterator, чтобы мы могли использовать расширенный цикл for. Но мы также сделаем потоковые монады. Этот случай особенно интересен, потому что потоки, возможно, бесконечны, поэтому карта должна возвращать поток, который лениво обрабатывает лямбду. В нашем примере мы создали бы специализированный генератор случайных чисел с определенным распределением. Обычно все значения одинаково вероятны. Но мы можем изменить это путем картирования. Давайте посмотрим на пример, чтобы лучше понять.

Давайте создадим общий поток, который может обернуть произвольный итератор. Таким образом, мы можем использовать и существующую платформу коллекции.

Stream.java

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
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
package com.geekyarticles.javamonads;
 
import java.util.Iterator;
import com.geekyarticles.lambda.
import java.util.NoSuchElementException;
 
public class Stream<T> implements Iterable<Option<T>>, Iterator<Option<T>>{
     
    //Provides a map on the underlying stream
    private class MapperStream<T,R> extends Stream<R>{
        private Stream<T> input;
        private SingleArgExpression<T, R> mapper;
        public MapperStream(Stream<T> input, SingleArgExpression<T, R> mapper){
            this.input = input;
            this.mapper = mapper;
        }
        @Override
        public Option<R> next(){
            if(!hasNext()){
                //This is to conform to Iterator documentation
                throw new NoSuchElementException();
            }
            return input.next().map(mapper);
        }
         
        @Override
        public boolean hasNext(){
            return input.hasNext();
        }
    }
     
    //Provides a flatMap on the underlying stream
    private class FlatMapperStream<T,R> extends Stream<R>{
        private Stream<T> input;
        private SingleArgExpression<T, Stream<R>>  mapper;
        private Option<Stream<R>> currentStream = new Option<>(null);
        public FlatMapperStream(Stream<T> input, SingleArgExpression<T, Stream<R>> mapper){
            this.input = input;
            this.mapper = mapper;
        }
        @Override
        public Option<R> next(){
            if(hasNext()){
                return currentStream.flatMap(stream -> stream.next());
            }else{
                //This is to conform to Iterator documentation
                throw new NoSuchElementException();
            }
             
        }
         
        @Override
        public boolean hasNext(){
            if(currentStream.map(s -> s.hasNext()) //Now Option(false) and Option(null) should be treated same
                .equals(new Option<Boolean>(Boolean.TRUE))){
                return true;
            }else if(input.hasNext()){
                currentStream=input.next().map(mapper);
                return hasNext();
            }else{
                return false;
            }
        }
    }
     
    //Puts a filter on the underlying stream
    private class FilterStream<T> extends Stream<T>{
        private Stream<T> input;
        private SingleArgExpression<T, Boolean> filter;
        private Option<T> next = new Option<>(null);
        public FilterStream(Stream<T> input, SingleArgExpression<T, Boolean> filter){
            this.input = input;
            this.filter = filter;
            updateNext();
        }
         
        public boolean hasNext(){
            return next != null;           
        }
         
        //We always keep one element calculated in advance.
        private void updateNext(){
            next = input.hasNext()? input.next(): new Option<T>(null);
            if(!next.map(filter).equals(new Option<Boolean>(Boolean.TRUE))){
                if(input.hasNext()){
                    updateNext();                   
                }else{
                    next = null;                   
                }
                                         
            }
        }
         
        public Option<T> next(){
            Option<T> res = next;
            updateNext();       
            if(res == null){
                throw new NoSuchElementException();
            }   
            return res;
        }
         
    }
     
    protected Iterator<T> input;
     
    public Stream(Iterator<T> input){
        this.input=input;
    }
     
    //Dummy constructor for the use of subclasses
    protected Stream(){
    }
     
    @Override
    public boolean hasNext(){
        return input.hasNext();
    }
     
    @Override
    public Option<T> next(){
        return new Option<>(input.next());
    }
     
    @Override
    public void remove(){
        throw new UnsupportedOperationException();
    }
     
    public <R> Stream<R> map(SingleArgExpression<T,R> mapper){
        return new MapperStream<T, R>(this, mapper);
    }
     
    public <R> Stream<R> flatMap(SingleArgExpression<T, Stream<R>> mapper){
        return new FlatMapperStream<T, R>(this, mapper);
    }
     
    public Stream<T> filter(SingleArgExpression<T, Boolean> filter){       
        return new FilterStream<T>(this, filter);
    }
     
    public Iterator<Option<T>> iterator(){
        return this;
    }
     
}

StreamExample.java

01
02
03
04
05
06
07
08
09
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
package com.geekyarticles.javamonads.examples;
 
import com.geekyarticles.javamonads.
import java.util.
 
 
public class StreamExample{
    public static void main(String [] args){
        iteratorExample();
        infiniteExample();
    }
    static void iteratorExample(){
        System.out.println("iteratorExample");
        List<Integer> l = new ArrayList<>();
        l.addAll(Arrays.asList(new Integer[]{1,2,5,20,4,51,7,30,4,5,2,2,1,30,9,2,1,3}));
        Stream<Integer> stream = new Stream<>(l.iterator());
         
        //Stacking up operations
        //Multiply each element by 10 and only select if less than 70
        //Then take the remainder after dividing by 13
        for(Option<Integer> i : stream.map(i -> i*10).filter(i ->  i < 70).map(i -> i%13)){
            System.out.println(i.get());
        }
        System.out.println();
    }
     
     
    static void infiniteExample(){
        System.out.println("infiniteExample");
        Iterator<Double> randomGenerator = new Iterator<Double>(){
            @Override
            public Double next(){
                return Math.random();
            }
             
            @Override
            public boolean hasNext(){
                //Infinite iterator
                return true;
            }
             
            public void remove(){
                throw new UnsupportedOperationException();
            }
             
        };
         
        Stream<Double> randomStream = new Stream<>(randomGenerator);
         
        //Now generate a 2 digit integer every second, for ever.
        for(Option<Integer> val:randomStream.map(v -> (int)(v*100))){
            System.out.println(val.get());
            try{
                Thread.sleep(1000);
            }catch(InterruptedException ex){
                ex.printStackTrace();
            }
        }
         
    }
     
}

Этот пример довольно сложный, поэтому потратьте некоторое время на его чтение. Однако класс Stream необходимо создать только один раз. Оказавшись там, он может использоваться, чтобы обернуть любой итератор, и он предоставит вам все монадические функции бесплатно.

В моем следующем посте я бы объяснил еще несколько монад.