Почему интерфейс Monad не может быть объявлен на Java?

Прежде чем вы начнете читать: этот вопрос касается не понимания монад, а определения ограничений системы типа Java, которые препятствуют объявлению интерфейса Monad.


В попытке понять монады я прочитал этот SO-ответ Эрика Липперта по вопросу, который спрашивает об простом объяснении монадов. Там он также перечисляет операции, которые могут выполняться на монаде:

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

После того, как вы прочли больше о монадах, я определил первую операцию как функцию return, а вторую операцию - как функцию bind. Я не смог найти обычно используемое имя для третьей операции, поэтому я просто назову его функцией unbox.

Чтобы лучше понять монады, я пошел вперед и попытался объявить общий Monad интерфейс в Java. Для этого я сначала рассмотрел подписи трех вышеперечисленных функций. Для Monad M это выглядит так:

return :: T1 -> M<T1>
bind   :: M<T1> -> (T1 -> M<T2>) -> M<T2>
unbox  :: M<T1> -> T1

Функция return не выполняется в экземпляре M, поэтому она не входит в интерфейс Monad. Вместо этого он будет реализован как конструктор или метод factory.

Также на данный момент я опускаю функцию unbox из декларации интерфейса, так как она не требуется. Будут различные реализации этой функции для различных реализаций интерфейса.

Таким образом, интерфейс Monad содержит только функцию bind.

Попробуйте объявить интерфейс:

public interface Monad {
    Monad bind();
}

Есть два недостатка:

  • Функция bind должна возвращать конкретную реализацию, однако она возвращает только тип интерфейса. Это проблема, так как мы имеем операции unbox, объявленные на конкретных подтипах. Я буду называть это проблемой 1.
  • Функция bind должна извлекать функцию в качестве параметра. Мы рассмотрим это позже.

Использование конкретного типа в объявлении интерфейса

Это устраняет проблему 1: Если мое понимание монад верно, то функция bind всегда возвращает новую монаду того же конкретного типа, что и монада, где она была вызвана. Итак, если у меня есть реализация интерфейса Monad под названием M, то M.bind вернет другой M, но не Monad. Я могу реализовать это с помощью дженериков:

public interface Monad<M extends Monad<M>> {
    M bind();
}

public class MonadImpl<M extends MonadImpl<M>> implements Monad<M> {
    @Override
    public M bind() { /* do stuff and return an instance of M */ }
}

Поначалу это работает, однако есть два недостатка:

  • Это прерывается, как только класс реализации не обеспечивает себя, а другой вариант реализации интерфейса Monad в качестве параметра типа M, потому что тогда метод bind вернет неправильный тип. Например,

    public class FaultyMonad<M extends MonadImpl<M>> implements Monad<M> { ... }
    

    вернет экземпляр MonadImpl, где он должен вернуть экземпляр FaultyMonad. Однако мы можем указать это ограничение в документации и рассмотреть такую ​​реализацию как ошибку программиста.

  • Второй недостаток сложнее разрешить. Я назову его проблемой 2. Когда я пытаюсь создать экземпляр класса MonadImpl, мне нужно указать тип M. Попробуем это:

    new MonadImpl<MonadImpl<MonadImpl<MonadImpl<MonadImpl< ... >>>>>()
    

    Чтобы получить допустимое объявление типа, это должно продолжаться бесконечно. Вот еще одна попытка:

    public static <M extends MonadImpl<M>> MonadImpl<M> create() {
        return new MonadImpl<M>();
    }
    

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

    public void createAndUseMonad() {
        MonadImpl<?> monad = create();
        // use monad
    }
    

    который, по существу, сводится к

    MonadImpl<?> monad = new MonadImpl<>();
    

    но это явно не то, что мы хотим.

Использование типа в его собственном объявлении со сдвинутыми параметрами типа

Теперь добавьте параметр функции в функцию bind: Как описано выше, подпись функции bind выглядит следующим образом: T1 -> M<T2>. В Java это тип Function<T1, M<T2>>. Вот первая попытка объявить интерфейс с параметром:

public interface Monad<T1, M extends Monad<?, ?>> {
    M bind(Function<T1, M> function);
}

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

public interface Monad<T1, M extends Monad<T2, ?, ?>,
                       T2> {
    M bind(Function<T1, M> function);
}

Теперь у нас возникает другая проблема. Мы добавили параметр третьего типа в интерфейс Monad, поэтому нам пришлось добавить новый ? к его использованию. Мы будем игнорировать новый ?, чтобы теперь исследовать теперь первый ?. Это M возвращенной монады типа M. Попробуйте удалить этот ?, переименовав M в M1 и введя еще один M2:

public interface Monad<T1, M1 extends Monad<T2, M2, ?, ?>,
                       T2, M2 extends Monad< ?,  ?, ?, ?>> {
    M1 bind(Function<T1, M1> function);
}

Знакомство с другим T3 приводит к:

public interface Monad<T1, M1 extends Monad<T2, M2, T3, ?, ?>,
                       T2, M2 extends Monad<T3,  ?,  ?, ?, ?>,
                       T3> {
    M1 bind(Function<T1, M1> function);
}

и введение другого M3 приводит к:

public interface Monad<T1, M1 extends Monad<T2, M2, T3, M3, ?, ?>,
                       T2, M2 extends Monad<T3, M3,  ?,  ?, ?, ?>,
                       T3, M3 extends Monad< ?,  ?,  ?,  ?, ?, ?>> {
    M1 bind(Function<T1, M1> function);
}

Мы видим, что это будет продолжаться вечно, если мы попытаемся разрешить все ?. Это проблема 3.

Подведение итогов

Мы определили три проблемы:

  • Использование конкретного типа в объявлении абстрактного типа.
  • Создает экземпляр типа, который получает себя как общий тип параметра.
  • Объявление типа, который использует себя в своем объявлении со сдвинутыми параметрами типа.

Вопрос: Какая функция отсутствует в системе типа Java? Поскольку существуют языки, которые работают с монадами, эти языки должны как-то объявить тип Monad. Как эти другие языки объявляют тип Monad? Я не смог найти информацию об этом. Я только нахожу информацию об объявлении конкретных монадов, таких как монада Maybe.

Я что-то пропустил? Могу ли я правильно решить одну из этих проблем с помощью системы типа Java? Если я не могу решить проблему 2 с помощью системы типа Java, есть ли причина, по которой Java не предупреждает меня о неинтересном объявлении типа?


Как уже было сказано, этот вопрос не о понимании монадов. Если мое понимание монад ошибочно, вы можете дать нам подсказку, но не пытайтесь дать объяснение. Если мое понимание монад ошибочно, описанные проблемы остаются.

Этот вопрос также не связан с тем, можно ли объявить интерфейс Monad в Java. Этот вопрос уже получил ответ Эрика Липперта в его SO-ответ, приведенный выше: "Нет". Этот вопрос касается того, что именно является ограничением, которое мешает мне это делать. Эрик Липперт относится к этому как к более высоким типам, но я не могу обойти их вокруг.

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

Ответ 1

Какая функция отсутствует в системе типа Java? Как эти другие языки объявляют тип Monad?

Хороший вопрос!

Эрик Липперт относится к этому как к более высоким типам, но я не могу обойти их вокруг.

Ты не одинок. Но они на самом деле не такие сумасшедшие, как они звучат.

Позвольте ответить на оба вопроса, взглянув на то, как Haskell объявляет "тип" монады - вы увидите, почему цитаты за минуту. Я несколько упростил его; стандартный шаблон монады также имеет пару других операций в Haskell:

class Monad m where
  (>>=) :: m a -> (a -> m b) -> m b
  return :: a -> m a

Мальчик, который выглядит одновременно невероятно простым и полностью непрозрачным, не так ли?

Здесь, позвольте мне упростить это немного больше. Haskell позволяет вам объявить свой собственный инфиксный оператор для привязки, но мы просто назовем его bind:

class Monad m where
  bind :: m a -> (a -> m b) -> m b
  return :: a -> m a

Хорошо, теперь, по крайней мере, мы видим, что есть две операции монады. Что это значит?

Первое, что нужно сделать, как вы заметили, - это "более высокие типы". (Как указывает Брайан, я несколько упростил этот жаргон в своем первоначальном ответе. Также довольно забавно, что ваш вопрос привлек внимание Брайана!)

В Java "класс" является своего рода "типом", и класс может быть общим. Итак, на Java у нас есть int и IFrob и List<IBar>, и они все типы.

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

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

В Haskell существуют также общие и не общие типы. Класс в Haskell не является не. В Java класс описывает набор значений; в любое время, когда вам нужен экземпляр класса, вы можете использовать значение этого типа. В Haskell класс описывает набор типов. Это ключевая особенность, которая отсутствует в системе типа Java. В Haskell класс выше, чем тип, который выше экземпляра. Java имеет только два уровня иерархии; У Хаскелла три. В Haskell вы можете выразить идею "в любое время, когда мне нужен тип, который имеет определенные операции, я могу использовать член этого класса".

(ASIDE: Я хочу указать здесь, что я делаю немного упрощения. Рассмотрим в Java, например, List<int> и List<String>. Это два "типа", но Java считает их "одним", класс ", поэтому в некотором смысле у Java также есть классы, которые являются" более высокими ", чем типы. Но опять же, вы можете сказать то же самое в Haskell, что list x и list y являются типами, а list - вещь это выше, чем тип, это вещь, которая может создать тип. Поэтому на самом деле было бы более точно сказать, что Java имеет три уровня, а у Haskell четыре. Точка остается, хотя: у Haskell есть концепция описания операций доступный по типу, который просто более мощный, чем Java. Мы рассмотрим это более подробно ниже.)

Итак, как это отличается от интерфейса? Это похоже на интерфейсы в Java - вам нужен тип, который имеет определенные операции, вы определяете интерфейс, описывающий эти операции. Мы увидим, чего не хватает на интерфейсах Java.

Теперь мы можем начать понимать этот Haskell:

class Monad m where

Итак, что такое Monad? Это класс. Что такое класс? Это набор типов, которые имеют что-то общее, так что всякий раз, когда вам нужен тип, который имеет определенные операции, вы можете использовать тип Monad.

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

  bind :: m a -> (a -> m b) -> m b
  return :: a -> m a

Название операции происходит слева от ::, а подпись - справа. Поэтому, чтобы быть Monad, тип m должен иметь две операции: bind и return. Каковы подписи этих операций? Сначала рассмотрим return.

  a -> m a

m a - это Haskell для того, что в Java было бы M<A>. То есть это означает, что m является общим типом, a является типом, m a является m параметризованным с помощью a.

x -> y в Haskell является синтаксисом для "функции, которая принимает тип x и возвращает тип y". Это Function<X, Y>.

Объедините его, и мы имеем return - это функция, которая принимает аргумент типа a и возвращает значение типа m a. Или в Java

static <A>  M<A> Return(A a);

bind немного сложнее. Я думаю, что OP хорошо понимает эту подпись, но для читателей, которые не знакомы с кратким синтаксисом Haskell, позвольте мне немного расшириться.

В Haskell функции принимают только один аргумент. Если вам нужна функция из двух аргументов, вы создаете функцию, которая принимает один аргумент и возвращает другую функцию одного аргумента. Поэтому, если у вас есть

a -> b -> c

Тогда что у тебя? Функция, которая принимает a и возвращает a b -> c. Предположим, вы хотели сделать функцию, которая взяла два числа и вернула их сумму. Вы должны сделать функцию, которая принимает первое число, и возвращает функцию, которая принимает второе число и добавляет его к первому числу.

В Java вы скажете

static <A, B, C>  Function<B, C> F(A a)

Итак, если вы хотели C, и у вас были и A, и B, вы могли бы сказать

F(a)(b)

Имеют смысл?

Хорошо, поэтому

  bind :: m a -> (a -> m b) -> m b

- фактически функция, которая принимает две вещи: a m a и a a -> m b и возвращает m b. Или, на Java, это прямо:

static <A, B> Function<Function<A, M<B>>, M<B>> Bind(M<A>)

Или, более идиоматично в Java:

static <A, B> M<B> Bind(M<A>, Function<A, M<B>>) 

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

Теперь вы можете сделать все монадические типы, которые вы хотите на Java. То, что вы не можете сделать, это создать интерфейс, представляющий идею "этот тип - тип монады". То, что вам нужно сделать, это что-то вроде:

typeinterface Monad<M>
{
  static <A>    M<A> Return(A a);
  static <A, B> M<B> Bind(M<A> m, Function<A, M<B>> f);
}

Посмотрите, как интерфейс типа говорит о самом родовом типе? Монадическим типом является любой тип m, который является общим с одним параметром типа и имеет эти два статических метода. Но вы не можете этого сделать в системах типа Java или С#. bind, конечно, может быть методом экземпляра, который принимает M<A> как this. Но нет никакого способа сделать return ничего, кроме статического. Java не дает вам возможности (1) параметризовать интерфейс неконструированным общим типом и (2) не позволяет указать, что статические члены являются частью контракта интерфейса.

Так как существуют языки, которые работают с монадами, эти языки должны каким-то образом объявить тип Monad.

Хорошо, что вы так думаете, но на самом деле нет. Во-первых, конечно, любой язык с достаточной системой типов может определять монадические типы; вы можете определить все монадические типы, которые вы хотите на С# или Java, вы просто не можете сказать, что у всех их есть в системе типов. Вы не можете создать общий класс, который может быть параметризирован только монадическими типами.

Во-вторых, вы можете встроить шаблон монады на языке другими способами. С# не может сказать, что "этот тип соответствует шаблону монады", но С# имеет понимание запросов (LINQ), встроенное в язык. Понимание запросов работает над любым монадическим типом! Это просто, что операция связывания должна называться SelectMany, что немного странно. Но если вы посмотрите на подпись SelectMany, вы увидите, что это просто bind:

  static IEnumerable<R> SelectMany<S, R>(
    IEnumerable<S> source,
    Func<S, IEnumerable<R>> selector)

Это реализация SelectMany для последовательности monad, IEnumerable<T>, но в С#, если вы пишете

from x in a from y in b select z

тогда тип a может иметь любой монадический тип, а не только IEnumerable<T>. Требуется, чтобы a был M<A>, что b является M<B> и что существует подходящий SelectMany, который следует за шаблоном монады. Так что другой способ вложения "распознавателя монады" в язык, не представляя его непосредственно в системе типов.

(предыдущий абзац на самом деле является ложью упрощения, шаблон привязки, используемый этим запросом, несколько отличается от стандартного монадического связывания по соображениям производительности. Концептуально это признает шаблон монады, в действительности детали немного отличаются. здесь http://ericlippert.com/2013/04/02/monads-part-twelve/, если вам интересно.)

Еще несколько мелких точек:

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

Хороший выбор; его обычно называют операцией "extract". Монаде не нужно открывать операцию извлечения, но, как бы то ни было, bind должен получить a из M<A>, чтобы вызвать Function<A, M<B>> на нем, поэтому логически какой-то извлечения работа обычно бывает.

Комонад - обратная монада, в некотором смысле - требует операции extract; extract по существу return назад. Для comonad также требуется операция extend, которая является видом bind, повернутой назад. Он имеет подпись static M<B> Extend(M<A> m, Func<M<A>, B> f)

Ответ 2

Если вы посмотрите, что делает проект AspectJ, он похож на применение монадов к Java. То, как они это делают, состоит в том, чтобы выполнить пост-обработку байтового кода классов, чтобы добавить дополнительные функциональные возможности, и причина, по которой они должны это делать, заключается в том, что нет способа в пределах языка без расширений AspectJ делать то, что им нужно, делать; язык недостаточно выразителен.

Конкретный пример: скажем, вы начинаете с класса A. У вас есть монада M такая, что M (A) - это класс, который работает точно так же, как A, но все входы и выходы метода прослеживаются до log4j. AspectJ может это сделать, но на самом языке Java не существует возможности, позволяющего вам.

В этом документе описывается, как аспектно-ориентированное программирование, как в AspectJ, может быть формализовано как монады

В частности, на языке Java нет способа указать тип программным образом (не считая манипуляции с байтовым кодом a la AspectJ). Все типы предварительно задаются при запуске программы.