Рубрики
Без рубрики

Простое введение в Монады – С примерами Java

Простое введение в монады для разработчиков, имеющих опыт работы с не чисто функциональными языками программирования, такими как C #, Java и т.д. Помеченный тегами monad, bind, java, functional.

Было написано гораздо больше 100 статей о монадах! Есть даже Временная шкала учебных пособий по монаде (с 1992 по 2015 год).

Зачем же тогда еще одна статья о монаде?

Многие люди (включая меня) заинтригованы и озадачены множеством экзотических, многословных или упрощенных попыток объяснить монады. Объяснения типа “Монада в X – это просто моноид в категории эндофункторов X, где произведение × заменено составом эндофункторов и единицей, заданной идентификатором endofunctor. “не помогай. Они усиливают и без того распространенный синдром самозванца. Когда я начал свои попытки “получить это” Я часто задавался вопросом, действительно ли монады непостижимы для нормальных людей, или я просто глупый нуб.

С другой стороны, некоторые люди, похоже, считают, что понять монады так же просто, как понять буррито – идея, к которой пользователь molemind комментарии на Reddit: “Ах, круто, теперь я понимаю, что такое буррито!”.

Во всяком случае, я не смог найти введение в монады, которое было бы легко понять новичкам с хорошим опытом работы в популярных, но не чисто функциональных языках программирования, таких как C #, Java, Javascript, Python и т.д. Если вы находитесь в той же лодке, то эта статья, надеюсь, восполнит этот пробел и поможет ответить на следующие вопросы:

  • Что такое монада?

  • Зачем мне использовать монаду? Что в этом для меня?

  • Правда ли, что “Как только вы понимаете монады, вы теряете способность объяснять их другим”?

Предположим, нам нужно написать простую функцию, чтобы “привести в восторг” предложение. Функция должна сделать это, преобразовав входную строку следующим образом:

  • удалите начальные и конечные пробелы

  • преобразовать все буквы в верхний регистр

  • добавить восклицательный знак

Например, передача функции с помощью "Привет, Боб" должна возвращать "ПРИВЕТ, БОБ!" .

В большинстве языков программирования это тривиально сделать. Вот решение на Java:

static String enthuse ( String sentence ) {
    return sentence.trim().toUpperCase().concat ( "!" );
}

Примечание: Примеры в этой статье показаны на языке Java. Но читатели не обязаны быть экспертами по Java. Используется только базовая Java, и примеры могут быть легко переписаны на C# и на других языках, поддерживающих универсальные типы и функции более высокого порядка (функции, которые могут принимать функцию в качестве входного аргумента). Полный исходный код доступен по адресу Gitlab .

Чтобы написать полное Java-приложение, включающее элементарный тест, мы можем поместить приведенный ниже код в файл MonadTest_01.java :

public class MonadTest_01 {

    static String enthuse ( String sentence ) {
        return sentence.trim().toUpperCase().concat ( "!" );
    }

    public static void main ( String[] args ) {
        System.out.println ( enthuse ( "  Hello bob  " ) );
    }
}

Затем мы можем скомпилировать и запустить программу с помощью следующих команд:

javac MonadTest_01.java
java MonadTest_01

Результат выглядит так, как и ожидалось:

HELLO BOB!

Пока все хорошо.

В предыдущем примере Java мы использовали объектные методы (например, sentence.trim() ). Однако, поскольку эта статья посвящена монадам, мы должны знать, что чисто функциональные языки программирования не имеют методов (функций), выполняемых над объектами. Чистый функциональный язык программирования (основанный на лямбда-исчислении) имеет только функции без побочных эффектов, которые принимают входные данные и возвращают результат.

Поэтому давайте перепишем предыдущий код (все еще на Java), используя только чистые функции. Это важно, потому что мы должны использовать функции, чтобы окончательно понять, почему были изобретены монады.

Вот новый код:

static String trim ( String string ) {
    return string.trim();
}

static String toUpperCase ( String string ) {
    return string.toUpperCase();
}

static String appendExclam ( String string ) {
    return string.concat ( "!" );
}

static String enthuse ( String sentence ) {
    return appendExclam ( toUpperCase ( trim ( sentence ) ) );
}

public static void test() {
    System.out.println ( enthuse ( "  Hello bob  " ) );
}

Код начинается с трех чистых функций ( trim , toUpperCase и appendExclam ), которые принимают строку в качестве входных данных и возвращают строку в качестве результата. Обратите внимание, что я немного схитрил, потому что я все еще использую объектные методы в телах функции (например, string.trim() ). Но здесь это не имеет значения, потому что в этом упражнении нас не волнуют реализации этих трех функций – мы заботимся об их сигнатурах .

Интересная часть – это тело функции восторгаться :

return appendExclam ( toUpperCase ( trim ( sentence ) ) );

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

  • шаг 1: обрезать ( предложение) выполняется

  • шаг 2: результат шага 1 передается в toUpperCase

  • шаг 3: результат шага 2 передается в appendExclam

  • наконец, результат шага 3 возвращается как результат функции enthuse

На следующем рисунке показано, как преобразуется исходный ввод путем объединения трех функций в цепочку:

Чтобы убедиться, что все по-прежнему работает нормально, мы можем выполнить функцию test . Результат остается тем же самым:

HELLO BOB!

В языках функционального программирования вызовы вложенных функций (например, наше appendix ( toUpperCase ( trim ( sentence ) ) ) ) вызываются композицией функций .

Важно : Функция композиция – это хлеб с маслом функциональных языков программирования. В лямбда-исчислении тело функции представляет собой одно выражение. Сложные выражения могут быть созданы с помощью составных функций.

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

Каналы Unix

Во-первых, интересно отметить, что идея композиции функций аналогична pipes в Unix/Linux . Выходные данные первой команды передаются в качестве входных данных во вторую команду. Затем выходные данные второй команды передаются в качестве входных данных в третью команду и так далее. В Unix/Linux символ | используется для передачи команд по каналу. Вот пример канала, который подсчитывает количество файлов, содержащих "страница" в их названии (пример заимствован из Как использовать каналы в Linux ):

ls - | grep "page" | wc -l

Оператор труб

Поскольку каналы полезны во многих контекстах, некоторые языки программирования имеют определенный оператор канала . Например, F# использует |> для цепочки вызовов функций. Если бы в Java был этот оператор, то функция enthuse могла бы быть записана как:

static String enthuse ( String sentence ) {
    return trim ( sentence ) |> toUpperCase |> appendExclam;
}

… который семантически был бы таким же, но немного более читаемым, чем реальная Java, использующая вызовы вложенных функций:

static String enthuse ( String sentence ) {
    return appendExclam ( toUpperCase ( trim ( sentence ) ) );
}

Оператор композиции функций

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

Например, в Haskell точка ( . ) используется для составления функций (производных от символа кольцевого оператора ∘, используемого в математике). Точка сама по себе является функцией, сигнатура которой определяется следующим образом:

(.) :: (b -> c) -> (a -> b) -> a -> c

Это говорит нам о том, что функция принимает две функции в качестве входных данных ( b -> c и a -> b ) и возвращает другую функцию ( a -> c ), которая представляет собой композицию двух входных функций.

Следовательно, чтобы указать, что функция h – это состав функций f и g , в Haskell вы можете просто написать:

h = f . g

Обратите внимание на совершенно иную семантику оператора dot в Haskell и объектно-ориентированных языках, таких как C#, Java и т.д. На языке Java, f.g означает нанесение g на объект f (например, person.name ). В Haskell это означает составление функций f и g .

F# использует >> для создания функций. Он определяется следующим образом:

let (>>) f g x = g(f(x))

И он используется следующим образом:

let h = f >> g

Примечание: Оператор F# >> не следует путать с Оператором последовательности монад в Haskell, который также использует символ >> .

Если бы Java имела синтаксис, аналогичный F# для композиции функций, то функция enthuse могла бы быть записана как:

static String enthuse ( String sentence ) = trim >> toUpperCase >> appendExclam;

Ради этого руководства предположим, что наши функции могут завершиться сбоем следующими способами:

  • Функция trim завершается ошибкой, если входная строка пуста или содержит только пробелы (т.е. результат не может быть пустой строкой).

  • Функция Ввод в верхнем регистре завершается ошибкой, если входная строка пуста или содержит символы, отличные от букв или пробелов.

  • Функция appendExclam завершается ошибкой, если длина входной строки превышает 20 символов.

В идиоматической Java выдается исключение, указывающее на ошибку. Но чисто функциональные языки программирования не поддерживают исключения, потому что функция не может иметь побочного эффекта. Функция, которая может завершиться ошибкой, должна возвращать информацию об ошибке как часть результата, возвращаемого функцией. Например, функция возвращает строку в случае успешного выполнения или данные об ошибке в случае ошибки.

Итак, давайте сделаем это на Java.

Сначала мы определяем простой класс ошибок с полем info , которое описывает ошибку:

public class SimpleError {

    private final String info;

    public SimpleError ( String info ) {
        this.info = info;
    }

    public String getInfo() { return info; }

    public String toString() { return info; }
}

Как уже было сказано, функции должны иметь возможность возвращать строку в случае успеха, иначе объект ошибки. Для достижения этой цели мы можем определить класс Результат Или Ошибка :

public class ResultOrError {

    private final String result;
    private final SimpleError error;

    public ResultOrError ( String result ) {
        this.result = result;
        this.error = null;
    }

    public ResultOrError ( SimpleError error ) {
        this.result = null;
        this.error = error;
    }

    public String getResult() { return result; }

    public SimpleError getError() { return error; }

    public boolean isResult() { return error == null; }

    public boolean isError() { return error != null; }

    public String toString() {
        if ( isResult() ) {
            return "Result: " + result; 
        } else {
            return "Error: " + error.getInfo(); 
        }
    }
}

Как мы можем видеть:

  • Класс имеет два неизменяемых поля для хранения либо результата, либо ошибки

  • Есть два конструктора.

    Первый используется в случае успеха (например, возвращает новый результат или ошибку ( "привет"); ).

    Второй конструктор используется в случае сбоя (например, возвращает новый результат или ошибку ( новая ошибка ( "Что-то пошло не так")); ).

  • является результатом и isError являются служебными функциями

  • toString переопределяется для целей отладки

Теперь мы можем переписать три служебные функции, чтобы включить обработку ошибок:

public class StringFunctions {

    public static ResultOrError trim ( String string ) {

        String result = string.trim();
        if ( result.isEmpty() ) {
            return new ResultOrError ( new SimpleError (
                "String must contain non-space characters." ) );
        }

        return new ResultOrError ( result );
    }

    public static ResultOrError toUpperCase ( String string ) {

        if ( ! string.matches ( "[a-zA-Z ]+" ) ) {
            return new ResultOrError ( new SimpleError (
                "String must contain only letters and spaces." ) );
        }

        return new ResultOrError ( string.toUpperCase() );
    }

    public static ResultOrError appendExclam ( String string ) {

        if ( string.length() > 20 ) {
            return new ResultOrError ( new SimpleError (
                "String must not exceed 20 characters." ) );
        }

        return new ResultOrError ( string.concat ( "!" ) );
    }
}

Примечание: Чтобы упростить код этого упражнения, мы не проверяем и не обрабатываем значения null (как мы бы делали в производственном коде). Например, если функция вызывается с null в качестве входных данных, мы просто принимаем, что выдается NullPointerException .

Важно то, что три функции, которые ранее возвращали строку, теперь возвращают Результат или ошибку объекта.

Как следствие, функция вызывает энтузиазм это было определено следующим образом:

static String enthuse ( String sentence ) {
    return appendExclam ( toUpperCase ( trim ( sentence ) ) );
}

… больше не работает.

К сожалению, композиция функций теперь недействительна, потому что функции теперь возвращают Результат или Ошибку объекта, но требуют string в качестве входных данных. Типы вывода/ввода больше не совпадают. Функции больше нельзя изменять.

В предыдущей версии, когда функции возвращали строки, выходные данные функции могли передаваться в качестве входных данных следующей функции:

Но теперь этого больше нельзя делать:

Тем не менее, мы все еще можем реализовать enthuse на языке Java вот так:

static ResultOrError enthuse ( String sentence ) {

    ResultOrError trimmed = trim ( sentence );
    if ( trimmed.isResult() ) {
        ResultOrError upperCased = toUpperCase ( trimmed.getResult() );
        if ( upperCased.isResult() ) {
            return appendExclam ( upperCased.getResult() );
        } else {
            return upperCased;
        }
    } else {
        return trimmed;
    }
}

Нехорошо! Первоначальный простой однострочник превратился в уродливого монстра.

Мы можем немного улучшить ситуацию:

static ResultOrError enthuse_2 ( String sentence ) {

    ResultOrError trimmed = trim ( sentence );
    if ( trimmed.isError() ) return trimmed;

    ResultOrError upperCased = toUpperCase ( trimmed.getResult() );
    if ( upperCased.isError() ) return upperCased;

    return appendExclam ( upperCased.getResult() );
}

Этот вид кода работает на Java и многих других языках программирования. Но это, конечно, не тот код, который мы хотим писать снова и снова. Код обработки ошибок, смешанный с обычным потоком, затрудняет чтение, запись и обслуживание кода.

Что еще более важно, мы просто не можем писать подобный код на чисто функциональных языках программирования. Помните: выражение, возвращаемое функцией, состоит из состава функции .

Легко представить себе другие случаи, которые приводят к той же дилемме. И что нам следует делать в ситуациях, когда одна и та же проблема возникает во многих вариациях? Да, мы должны попытаться найти общее решение, которое можно использовать в максимальном количестве случаев.

Монады спешат на помощь!

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

Некоторые люди говорят: “Вы могли бы изобрести монады, если бы их не существовало”. (например, Брайан Бекман в своей превосходной презентации Не бойтесь Монады )

Это правда!

Итак, давайте попробуем найти решение сами, игнорируя тот факт, что монада решила бы нашу проблему.

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

Давайте вызовем эту функцию bind , потому что ее роль заключается в том, чтобы связать две функции, которые не могут быть составлены напрямую.

Далее мы должны решить, каким должен быть ввод bind и что он должен возвращать. Давайте рассмотрим случай цепочки функций trim и верхний регистр :

Логика для реализации должна работать следующим образом:

  • Если trim возвращает строку, то можно вызвать toUpperCase , поскольку он принимает строку в качестве входных данных. Таким образом, конечным результатом будет результат toUpperCase

  • Если trim возвращает ошибку, то toUpperCase не может быть вызван, и ошибка должна быть просто перенаправлена. Таким образом, конечным результатом будет результат trim

Мы можем сделать вывод, что bind требуется два входных аргумента:

  • результат trim , который имеет тип Результат или ошибка

  • функция Верхний регистр , потому что если trim возвращает строку , то привязать необходимо вызвать верхний регистр

Тип вывода bind легко определить. Если trim возвращает строку, то вывод bind является выводом toUpperCase , который имеет тип Результат Или Ошибка . Если trim завершается ошибкой, то вывод bind является выводом trim , который также имеет тип Результат Или Ошибка . Поскольку тип вывода – Результат Или Ошибка в обоих случаях, тип вывода bond должен быть ResultOrError .

Итак, теперь мы знаем сигнатуру bind :

В Java это записывается как:

ResultOrError bind ( ResultOrError value, Function function )

Реализовать bind легко, потому что мы точно знаем, что нужно сделать:

static ResultOrError bind ( ResultOrError value, Function function ) {

    if ( value.isResult() ) {
        return function.apply ( value.getResult() );
    } else {
        return value;
    }
}

Функция enthuse теперь может быть переписана следующим образом:

static ResultOrError enthuse ( String sentence ) {

    ResultOrError trimmed = trim ( sentence );

    ResultOrError upperCased = bind ( trimmed, StringFunctions::toUpperCase );
    // alternative:
    // ResultOrError upperCased = bind ( trimmed, string -> toUpperCase(string) );

    ResultOrError result = bind ( upperCased, StringFunctions::appendExclam );
    return result;
}

Но это все еще императивный код (последовательность инструкций). Если мы проделали хорошую работу, то мы должны быть в состоянии переписать enthuse , просто используя композицию функций. И действительно, мы можем сделать это так:

static ResultOrError enthuse_2 ( String sentence ) {

    return bind ( bind ( trim ( sentence ), StringFunctions::toUpperCase ), StringFunctions::appendExclam );
}

На первый взгляд тело bind может немного сбить с толку. Мы изменим это позже. Дело в том, что мы используем только функциональную композицию в теле enthuse .

Примечание: Если вы никогда раньше не видели такого рода код, пожалуйста, не торопитесь, чтобы переварить и полностью понять, что здесь происходит. Понимание bind – это ключ к пониманию монад! bind – это имя функции, используемое в Haskell. Существуют альтернативные имена, такие как: flatMap , chain , а Затем .

Правильно ли это работает? Давайте проверим это. Вот класс, содержащий bind , две вариации enthuse и некоторые упрощенные тесты, которые охватывают успешные и все пути ошибок:

public class MonadTest_04 {

    // start bind
    static ResultOrError bind ( ResultOrError value, Function function ) {

        if ( value.isResult() ) {
            return function.apply ( value.getResult() );
        } else {
            return value;
        }
    }
    // end bind

    // start enthuse_1
    static ResultOrError enthuse ( String sentence ) {

        ResultOrError trimmed = trim ( sentence );

        ResultOrError upperCased = bind ( trimmed, StringFunctions::toUpperCase );
        // alternative:
        // ResultOrError upperCased = bind ( trimmed, string -> toUpperCase(string) );

        ResultOrError result = bind ( upperCased, StringFunctions::appendExclam );
        return result;
    }
    // end enthuse_1

    // start enthuse_2
    static ResultOrError enthuse_2 ( String sentence ) {

        return bind ( bind ( trim ( sentence ), StringFunctions::toUpperCase ), StringFunctions::appendExclam );
    }
    // end enthuse_2

    private static void test ( String sentence ) {

        System.out.println ( enthuse ( sentence ) );
        System.out.println ( enthuse_2 ( sentence ) );
    }

    public static void tests() {

        test ( "  Hello bob  " );
        test ( "   " );
        test ( "hello 123" );
        test ( "Krungthepmahanakhon is the capital of Thailand" );
    }
}

Запущенная функция тесты выходные данные:

Result: HELLO BOB!
Result: HELLO BOB!
Error: String must contain non-space characters.
Error: String must contain non-space characters.
Error: String must contain only letters and spaces.
Error: String must contain only letters and spaces.
Error: String must not exceed 20 characters.
Error: String must not exceed 20 characters.    

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

Примечание: Использование bind , как показано выше, является распространенным способом решения нашей проблемы композиции функций. Но это не единственный способ. Альтернатива называется Композиция Клейсли (выходит за рамки данной статьи).

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

Наша цель в этой главе ясна: увидеть закономерность и улучшить Результат Или Ошибка , чтобы мы могли наконец прийти в восторг:

“МОНАДА!”

Первое улучшение:

В предыдущей главе мы определили связывать как изолированная функция, которая удовлетворяла нашим конкретным потребностям. Первым улучшением является перемещение bind в класс Result Или Error . Функция bind должна быть частью нашего класса monad. Причина в том, что реализация bind зависит от монады, которая использует bind . В то время как подпись из связывать всегда одно и то же, разные виды монад используют разные реализации .

Второе Улучшение:

В нашем примере кода все составные функции принимают строку в качестве входных данных и возвращают либо строку, либо ошибку. Что, если нам придется создавать функции, которые принимают целое число и возвращают целое число или ошибку? Можем ли мы улучшить Результат Или Ошибка чтобы он работал с любым типом результата? Да, мы можем. Нам просто нужно добавить параметр типа в Result Или Error .

После перемещения bind в класс и добавления параметра типа новая версия теперь становится:

public class ResultOrErrorMona {

    private final R result;
    private final SimpleError error;

    public ResultOrErrorMona ( R result ) {
        this.result = result;
        this.error = null;
    }

    public ResultOrErrorMona ( SimpleError error ) {
        this.result = null;
        this.error = error;
    }

    public R getResult() { return result; }

    public SimpleError getError() { return error; }

    public boolean isResult() { return error == null; }

    public boolean isError() { return error != null; }

    static  ResultOrErrorMona bind ( ResultOrErrorMona value, Function> function ) {

        if ( value.isResult() ) {
            return function.apply ( value.getResult() );
        } else {
            return value;
        }
    }

    public String toString() {

        if ( isResult() ) {
            return "Result: " + result; 
        } else {
            return "Error: " + error.getInfo(); 
        }
    }
}

Обратите внимание на имя класса: Результат Или Ошибка Mona . Это не опечатка. Класс еще не является монадой, поэтому я называю его mona (просто для развлечения).

Третье Улучшение:

Предположим, нам нужно связать в цепочку следующие две функции:

ResultOrError f1 ( Integer value )
ResultOrError  f2 ( Integer value )

Вот картинка, иллюстрирующая это:

Наша текущая функция bind не способна обработать этот случай, поскольку типы выходных данных двух функций различны ( ResultOrError и ResultOrError<Строка> ). Мы должны сделать bind более общим, чтобы функции разных типов значений могли быть связаны цепочкой. Подпись bind должна быть изменена с

static  Monad bind ( Monad monad, Function> function )

… к

static  Monad bind ( Monad monad, Function> function )

Реализация bind должна быть адаптирована к. Вот новый класс:

public class ResultOrErrorMonad {

    private final R result;
    private final SimpleError error;

    public ResultOrErrorMonad ( R result ) {
        this.result = result;
        this.error = null;
    }

    public ResultOrErrorMonad( SimpleError error ) {
        this.result = null;
        this.error = error;
    }

    public R getResult() { return result; }

    public SimpleError getError() { return error; }

    public boolean isResult() { return error == null; }

    public boolean isError() { return error != null; }

    static  ResultOrErrorMonad bind ( ResultOrErrorMonad value, Function> function ) {

        if ( value.isResult() ) {
            return function.apply ( value.result );
        } else {
            return new ResultOrErrorMonad ( value.error );
        }
    }

    public String toString() {

        if ( isResult() ) {
            return "Result: " + result.toString(); 
        } else {
            return "Error: " + error.toString(); 
        }
    }
}

Еще раз обратите внимание на имя класса: Монада результата Или Ошибки .

Да, теперь это монада.

Примечание: В реальном мире мы не добавляем суффикс “монада” для типов, которые являются монадами. Я вызвал монаду класса Result Или Error (вместо просто Результат Или Ошибка ), чтобы было ясно, что класс является монадой.

Как мы можем быть уверены, что класс действительно является монадой?

В то время как термин “монада” имеет очень точное определение в математике (как и все в математике), этот термин еще не определен однозначно в мире языков программирования. Однако в Википедии указано общее определение . Монада состоит из трех частей:

  • Конструктор типа M, который создает монадический тип M T.

    Другими словами, существует параметр типа для значения, содержащегося в монаде.

    В нашем случае это параметр типа R в объявлении класса:

  • Преобразователь типов, часто называемый unit или return, который встраивает объект x в монаду: единица измерения(x): T → M T

    В Haskell преобразователь типов определяется как: return:: a -> m a

    В Java-подобных языках это означает, что должен существовать конструктор, который принимает значение типа R и возвращает монаду M который содержит это значение.

    В нашем конкретном случае это конструктор класса Результат или Ошибка Монады :

  • Комбинатор, обычно называемый bind (как при привязке переменной) и представленный оператором инфикса >>=, который разворачивает монадическую переменную, затем вставляет ее в монадическую функцию/выражение, в результате чего получается новое монадическое значение: (mx): (M T, T → M U) → M U

    В Haskell привязка определяется как: (>>=):: m a -> (a -> m b) -> m b

    В нашем примере это функция bind :

Затем в Википедии говорится: “Чтобы полностью квалифицироваться как монада, эти три части также должны соблюдать несколько законов: …”

В Haskell три закона определены следующим образом:

  • вернуть a a

  • m

  • m >>= (\x -> k x) = (m)

Обсуждение этих законов выходит за рамки данной статьи (это введение в монады). Законы гарантируют, что монады ведут себя хорошо во всех ситуациях. Нарушение их может привести к тонким и болезненным ошибкам, как описано здесь , здесь и здесь . Насколько я знаю, в настоящее время нет компилятора, способного обеспечить соблюдение законов монады. Следовательно, разработчик несет ответственность за проверку соблюдения законов монады. Достаточно сказать, что вышеупомянутая Монада Результат Или ошибка выполняет законы монады.

Хотя мы закончили, все еще есть возможности для улучшения.

Помимо наличия параметра типа для результирующего значения, мы могли бы также добавить параметр типа для значения ошибки. Это делает монаду более многоразовой, поскольку пользователи монады теперь могут свободно решать, какой тип ошибки они хотят использовать. Для примера вы можете посмотреть на F# ‘s Результат тип.

Наконец, мы могли бы сделать монаду еще более многоразовой, позволив пользователю определять значение двух значений. В нашем примере одно значение представляет результат, а другое – ошибку. Но мы можем абстрагироваться от большего. Мы можем создать монаду, которая просто содержит одно из двух возможных значений – либо value_1, либо value_2. И тип каждого значения может быть свободно определен параметром типа. Это действительно стандартная монада, поддерживаемая некоторыми функциональными языками программирования. В Хаскелле это называется Либо . Его конструктор определяется следующим образом:

data Either a b = Left a | Right b

Используя наш Результат или класс Монады ошибок в качестве отправной точки, было бы легко создать Либо монада в Java.

Примечание: В некоторых проектах используется Либо монада для функций, которые могут выйти из строя. Им мое мнение, используя более конкретный Результат или ошибку типа это лучший, менее подверженный ошибкам вариант (по причинам, не объясненным здесь).

Теперь, когда мы знаем, как работает монада в функциональных языках программирования, давайте вернемся в мир ООП (объектно-ориентированного программирования). Можем ли мы создать что-то вроде OO-монады?

Если мы посмотрим на монаду class Result Или Error , мы видим, что все в этом классе уже является стандартным Java, за одним исключением: Функция bind является статическим членом класса. Это означает, что мы не можем использовать точечный синтаксис объектных методов для bind . В настоящее время синтаксис для вызова bind является bind ( v, f ) . Но если бы bind был нестатическим членом класса, мы могли бы написать v.bind ( f ) . Это сделало бы синтаксис более читаемым в случае вызовов вложенных функций.

К счастью, легко сделать bind нестатичным.

Чтобы сделать монаду немного более универсальной, давайте также введем параметр второго типа для значений ошибок. Тогда пользователи не обязаны использовать Простая ошибка – они могут использовать свой собственный класс ошибок.

Вот код Результата или ошибки монады в стиле OO:

public class ResultOrError {

    private final R result;
    private final E error;

    private ResultOrError ( R result, E error ) {
        this.result = result;
        this.error = error;
    }

    public static  ResultOrError createResult ( R result ) {
        return new ResultOrError ( result, null );
    }

    public static  ResultOrError createError ( E error ) {
        return new ResultOrError ( null, error );
    }

    public R getResult() { return result; }

    public E getError() { return error; }

    public boolean isResult() { return error == null; }

    public boolean isError() { return error != null; }

    public  ResultOrError bind ( Function> function ) {

        if ( isResult() ) {
            return function.apply ( result );
        } else {
            return createError ( error );
        }
    }

    public String toString() {

        if ( isResult() ) {
            return "Result: " + result.toString(); 
        } else {
            return "Error: " + error.toString(); 
        }
    }
}

Теперь код для использования привязки в теле функции enthuse становится более читабельным. Вместо того, чтобы писать:

return bind ( bind ( trim ( sentence ), v -> toUpperCase(v) ), v -> appendExclam(v) );

… мы можем избежать вложенности и написать:

return trim ( sentence ).bind ( v -> toUpperCase(v) ).bind ( v -> appendExclam(v) );

Итак, могут ли монады быть полезны в реальных средах ООП?

Да, они могут/| .

Но слово “может” необходимо подчеркнуть, потому что это зависит (как это часто бывает) от того, чего мы хотим достичь. Допустим, например, что у нас есть несколько веских причин “не использовать исключения” для обработки ошибок.

Помните код обработки ошибок, который мы должны были написать в главе “Ошибки”, но без исключений:

static ResultOrError enthuse ( String sentence ) {

    ResultOrError trimmed = trim ( sentence );
    if ( trimmed.isResult() ) {
        ResultOrError upperCased = toUpperCase ( trimmed.getResult() );
        if ( upperCased.isResult() ) {
            return appendExclam ( upperCased.getResult() );
        } else {
            return upperCased;
        }
    } else {
        return trimmed;
    }
}

Использование монады удаляет шаблон:

static ResultOrError enthuse ( String sentence ) {
    return trim ( sentence ).bind ( v -> toUpperCase(v) ).bind ( v -> appendExclam(v) );
}

Мило!

Ключом к пониманию монад является понимание bind (также называемого chain , а затем и т.д.). Функция bind используется для создания двух монадических функций. Монадическая функция – это функция, которая принимает значение типа T и возвращает объект, содержащий значение ( a -> m a ). Монадические функции не могут быть составлены напрямую, поскольку тип вывода первой вызываемой функции несовместим с типом ввода второй функции. bind решает эту проблему.

Функция bind полезна сама по себе. Но это всего лишь одна часть монады.

В мире, подобном Java, монада – это класс (тип) М с:

  • параметр типа T , который определяет тип значения, хранящегося в монаде (например, M )

  • конструктор, который принимает значение типа T и возвращает монаду M , содержащую значение

    • Java-подобные языки: M create ( значение T )

    • Haskell: return:: a -> m a

  • функция bind , используемая для создания двух монадических функций

    • Java-подобные языки: M привязать ( M монада, Функция M> функция ) M> функция )

    • Хаскелл: (>>=):: m a -> (a -> m b) -> m b

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

Монады в основном используются в функциональных языках программирования, поскольку эти языки полагаются на функциональную композицию. Но они также могут быть полезны в контексте других парадигм, таких как объектно-ориентированные языки программирования, которые поддерживают универсальные типы и функции более высокого порядка.

Как намекается в названии, эта статья представляет собой введение в монады. Он не охватывает весь спектр монад, в нем не показаны другие полезные примеры монад (возможно, монада, монада ввода-вывода, монада состояния и т.д.), И он полностью игнорирует “теорию категорий”, математическую основу для монад. Для тех, кто хочет узнать больше, в сети уже доступно множество информации.

Надеюсь, эта статья поможет понять суть монад, увидеть их красоту и понять, как они могут улучшить код и упростить жизнь.

“СЧАСТЛИВОГО МОНАДИРОВАНИЯ!”

Примечание: Оригинальная версия этой статьи была написана в PML (Практический язык разметки). Вы можете посмотреть код PML на Gitlab . Примеры исходного кода, используемые в этой статье, хранятся на Gitlab .

Оригинал: “https://dev.to/practicalprogramming/simple-introduction-to-monads-with-java-examples-2aio”