Как вы определяете анонимную функцию в терминах лямбда-исчисления (или как я могу сказать, что некоторые языки поддерживают анонимные функции)? - PullRequest
5 голосов
/ 19 ноября 2010

Поддерживает ли Java в текущей версии 6 лямбда-выражений или "анонимных функций"?Есть что-то, чего я не могу сделать в Java, чего не смог бы сделать с языком программирования, поддерживающим лямбда-выражения?Я понимаю, что java завершен, поэтому вы можете делать в нем "что угодно".

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

Что такое анонимная функция и как вы можете сказать, что какой-то язык поддерживает анонимные функции?

Ответы [ 2 ]

17 голосов
/ 20 ноября 2010

Как я уже намекал на мой комментарий выше, вопрос действительно зависит от того, как именно вы определяете «поддержку».В своем вопросе вы уже упоминали, что Java завершена по Тьюрингу, и поэтому Java «поддерживает» (для некоторого определения «поддерживает») все, что поддерживает любой другой язык программирования.

Java делает поддержка анонимных функций: просто напишите интерпретатор для λ-исчисления в Java и передайте анонимную функцию в виде строки.

Однако я считаю, что слишком много работы для использования анонимной функции.Поэтому для меня интересен не столько вопрос о том, поддерживает ли Java анонимные функции, но о том, поддерживает ли Java 1007 * me , когда я хочу использовать анонимные функции.IOW: облегчает ли Java использование анонимных функций, помогает ли это мне, помогает ли это мне?

Давайте проведем простой эксперимент: реализуем функцию map и используем ее для приращения каждого элемента списка[1, 2, 3, 4, 5] от 1.

Haskell

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

morph _ []     = []
morph f (x:xs) = f x : morph f xs

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

Как видите, написание функции, которая принимает функцию в качестве аргумента, очень просто, очень легко.

Предполагая, что у нас есть список l:

l = [1, 2, 3, 4, 5]

Теперь мы можем вызывать morph следующим образом:

morph (\x -> 1 + x) l

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

И это выглядит почти как λ-исчисление.Фактически, если вы используете IDE Haskell, текстовый редактор с режимом Haskell или симпатичный принтер Haskell, он будет отображаться так:

morph (λx → 1 + x) l

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

morph (1+) l

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

morph succ l

Хотя это, конечно, не анонимная функция, она именованная.

Scala

В Scala она выглядит очень похоже.Основное отличие состоит в том, что система типов Scala более сложна, чем система Haskell, и поэтому требует больше аннотаций типов:

def morph[A, B](l: List[A])(f: A => B): List[B] = l match {
  case Nil     => Nil
  case x :: xs => f(x) :: morph(xs)(f)
}

Она все еще очень легкая.По сути, все, что нам нужно было сделать - это объявить параметр f типа A => B (то есть функцию от типа A до типа B), что на самом деле является синтаксическим сахаром для Function1[A, B].

Теперь нам просто нужен наш список:

val l = List(1, 2, 3, 4, 5)

И трансформируем его:

morph(l) {_ + 1}

Это снова использует преимущества синтаксического сахара Scala.В анонимных функциях вы можете оставить список параметров;если использовать каждый параметр ровно один раз и в порядке их определения, вы можете просто ссылаться на них как _.

Но даже полная форма не намного тяжелее:

morph(l) {(e) => e + 1}

Если бы я столкнулся с проблемой создания morph метода экземпляра некоторого класса и определил неявное преобразование из List в этот класс согласно шаблону Pimp My Library , я мог бы даже написать что-токак

l morph {_ + 1}

Схема

Схема, конечно, не должна иметь проблем с анонимными функциями и функциями более высокого порядка.Вот morph:

(define (morph f l)
    (if (null? l)
        null
        (cons
            (f (first l))
            (morph f (rest l)))))

Вот наш список:

(define l '(1 2 3 4 5))

И наше использование анонимной функции:

(morph (lambda (e) (+ e 1)) '(1 2 3 4 5))

Ruby

module Enumerable
  def morph
    [].tap {|r| each {|e| r << yield(e) }}
  end
end

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

l = [1, 2, 3, 4, 5]

Вызов его почти так же, каклегкий как скала

l.morph {|e| e + 1 }

Я могу сортировать репликации разделов оператора из примера на Haskell, взяв ссылку на метод + 1:

l.morph(&1.method(:+))

В Ruby также есть предопределенныйsucc метод для целых чисел, который мы можем передать с помощью трюка Symbol#to_proc:

l.morph(&:succ)

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

module Enumerable
  def morph(f)
    [].tap &-> r { each &-> e { r << f.(e) }}
  end
end

l = [1, 2, 3, 4, 5]

l.morph -> e { e + 1 }
l.morph(1.method(:+))

ECMAScript (до 2015 года)

ECMAScript является прямым потомком Scheme, поэтому неудивительно, что он может обрабатывать нашипроблема в том, что с некоторой суммой синтаксического беспорядка:

Array.prototype.morph = function (f) {
    var r = [];
    this.forEach(function (e) { r.push(f(e)); });

    return r;
}

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

Давайте построим нашlist (ну, массив):

var l = [1, 2, 3, 4, 5];

И вызвать функцию morph (на самом деле это метод в данном случае), передав в качестве аргумента анонимную функцию:

l.morph(function (e) { return e + 1; });

ECMAScript(после 2015 года)

ECMAScript 2015 представил литералы анонимной функции "жирная стрелка" :

Array.prototype.morph = f => {
    const r = [];
    this.forEach(e => r.push(f(e)));

    return r;
}

Давайте построим наш список (ну, массив):

const l = [1, 2, 3, 4, 5];

И вызываем функцию morph (на самом деле это метод в данном случае), передавая анонимную функцию в качестве аргумента:

l.morph(e => e + 1);

C #

Теперь мыприближаясь к нашей конечной целевой язык.Вот C #:

public static IEnumerable<B> Morph<A, B>(this IEnumerable<A> l, Func<A, B> f)
{
    IList<B> r = new List<B>();
    foreach (var e in l) r.Add(f(e));

    return r;
}

Не так уж плохо.Обратите внимание на тип функции: Func<A, B>.Это предопределенный тип, который является частью базовой библиотеки, как Function1[A, B] в Scala или a → b в Haskell.(Это важное отличие от Java.)

Благодаря выводу типов и инициализаторам коллекций создание списка не так уж и сложно:

var l = new List<int> { 1, 2, 3, 4, 5 };

И передача лямбды, состоящей только изодного выражения в основном так же легко, как Ruby, Scala, Scheme или Haskell, и даже более легко, чем ECMAScript, потому что вам не нужны ключевые слова function или return:

l.Morph(e => e + 1);

Но дажеиспользование «полного» синтаксиса не так уж и плохо:

l.Morph((e) => { return e + 1; });

(Вы заметите, что я сделал Morph метод расширения, что означает, что я могу назвать его как l.Morph(f) в дополнение к Morph(l, f).)

Java (до 8)

static <A, B> List<B> morph(List<A> l, Function1<A, B> f) {
    List<B> r = new ArrayList<>();
    for (A e: l) r.add(f.apply(e));

    return r;
}

На первый взгляд, это не так уж и плохо на самом деле.На самом деле, это выглядит почти так же, как версия C #.Но почему я не могу написать f(e)?Почему я должен написать f.apply(e)?Во всех других языках я мог использовать тот же (или в случае с Ruby, почти такой же) синтаксис для вызова функции, которая была передана в качестве аргумента, как и для вызова любой другой функции, процедуры или метода.

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

Вот наш список:

List<Integer> l = Arrays.asList(1, 2, 3, 4, 5);

И вот как мы называем наши morph:

morph(l, new Function1<Integer, Integer>() {
    @Override public Integer apply(Integer n) {
        return n + 1;
    }
});

Это довольно тяжелые вещи.Я имею в виду, все, что я делаю, это вызываю метод и передаю два аргумента.Почему это взрывается в четыре строки?На всех других языках это была просто однострочная.Конечно, я мог бы удалить все разрывы строк, и это все равно будет действительным:

morph(l, new Function1<Integer, Integer>() { @OVerride public Integer apply(Integer n) { return n + 1; }});

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

Обратите внимание, что в некоторых других языках я фактически использовал анонимные функции внутри определение функции morph, например, в Ruby и ECMAScript, и это не составило большого труда.Если бы я делал это на Java, это привело бы к еще большему беспорядку и беспорядку и взрыву линий.

Таким образом, даже на этом этапе мы видим, что работа с анонимными функциями более высокого порядка в Java способ более громоздка, чем во многих других основных (и не очень распространенных) языках.

Но мы даже не дошли до действительно ужасной части: что это за тип Function1<A, B>?Откуда это взялось?

Ну, на самом деле мне пришлось написать такого типа себя !

interface Function1<A, B> {
    B apply(A a);
}

Это, конечно,так называемый интерфейс SAM , то есть интерфейс или абстрактный класс с Single Abstract Method .Что ближе всего к типу функции Java.В некотором смысле функция - это просто объект с одним методом, так что это прекрасно.Тот факт, что типы функций представлены через интерфейсы SAM, также не является проблемой.Фактически, именно так они представлены в Scala (в Scala f(a) - это просто синтаксический сахар для f.apply(a), поэтому любой объект с методом apply по сути является функцией), Ruby(в Ruby f.(a) является просто синтаксическим сахаром для f.call(a), поэтому каждый объект с методом call по сути является функцией), и аналогично в C #.

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

Мало того, что я должен был написать сам, мне пришлось придумать имя для этого. И Мне пришлось придумать название для метода.Ничего из того, что я имел отношение к другим языкам здесь.Ну, вообще-то, я просто украл имена у Scala, так что фактическая часть «придумывать имена» не была такой сложной.

Что действительно важно, так это последствия необходимостипридумать имя.У Java есть номинальная система типов , то есть система типов, основанная на names .И поэтому тот факт, что мне пришлось придумывать имя самому, означает, что все остальные также должны придумывать имена.И поскольку их имена отличаются от моих (а если нет, то это будет ошибка компиляции), это означает, что я не могу передать одну и ту же функцию двум разным библиотекам.Скажем, например, я хочу передать одну и ту же функцию фильтрации в мой gridview и в мой ORM.Но gridview ожидает, скажем, javax.swing.Predicate<T> с одним методом apply(T el), тогда как мой ORM ожидает org.sleepy.Predicate<T> с одним методом apply(T el).

Обратите внимание, что эти два типа действительно абсолютно одинаковыПросто у них разные имена, и поэтому я не могу передать одну и ту же функцию обеим библиотекам.Это не гипотетический пример.Во время недавних обсуждений Project Lambda для Java кто-то подсчитал, сколько повторяющихся экземпляров типа Predicate уже было в Java SE 6, и в IIRC это число было двузначным.

Это вполне возможнорешить эту проблему в системе номинального типа.В конце концов, не существует десятков несовместимых копий типа List, просто потому, что Sun поместила одну одну одну в библиотеку, и это используют все.Они могли бы сделать то же самое с Function, но они этого не сделали, что приводит к распространению идентичных, но взаимно несовместимых типов не только в сторонних библиотеках, но даже в JRE.(Например, Runnable, возможно, является типом функции, как и Comparator. Но почему они должны быть в специальном корпусе?) В .NET это работает просто отлично, потому что Microsoft поместила один единственный тип во время выполнения.(Ну, на самом деле, не совсем один единственный тип, но достаточно близко.)

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

Это также одна из причин, почему я так разочарован некоторыми итерациями Project Lambda.Они продолжают отказываться от введения типов функций из проекта, хотя отсутствие типов функций является ИМХО одной из самых больших проблем.Уродливый синтаксис можно исправить с помощью трюков IDE, отсутствие стандартных типов функций - нет.(Не говоря уже обо всех тех людях, которые используют JVM и JRE, но не используют Java. Они не получают никакой выгоды от добавления синтаксического сахара для анонимных внутренних классов SAM в язык Java, просто потому, что они не делаютиспользуйте язык Java. Им нужны типы функций и обновленная библиотека коллекций, которая использует типы функций.)

Итак, у нас сейчас четыре проблемы:

  1. синтаксические издержки , потому что вы должны использовать такие вещи, как f.apply(a) и new Function1<A, A>() { public A apply(A a) { return a; }} (Кстати, это функция идентификации, то есть функция, которая абсолютно ничего не делает, и она занимает58 (!) Символов),
  2. накладные расходы на моделирование , поскольку вы должны сами определять собственные типы функций, в дополнение к тем типам доменов, которые вам действительно нужны,
  3. ограниченная полезность потому что после того, как вы создали свои лямбды, на самом деле не так уж много методов, которые принимают лямбды в качестве аргументов и
  4. ограниченное повторное использование , потому чтодаже если вы найдете метод, который принимает лямбду, он не займет вашу лямбду, поскольку типы не совпадают.

Икогда я говорю о «накладных расходах на моделирование», я говорю не только о типе one Function1.Введите примитивные типы ...

Вы заметили, как я использовал Integer с, а не int с в моем коде выше?Да, именно так, крупнейшая ошибка в истории языков программирования, Original Sin Java, проклятие существования каждого Java-программиста, снова пришла нам в задницу: примитивные типы.

Youвидите, в Scala существует ровно один класс, представляющий функцию с n аргументами.Это называется FunctionN[T₁, T₂, …, Tn, R].Итак, существует ровно один класс Function0[R] для функций без каких-либо аргументов, один класс Function1[T, R] для функций с одним аргументом, один класс Function3[A, B, C, R] для функций с тремя аргументами и т. Д., Вплоть до примерно 20,Я верю.

В C # точно есть два класса, которые представляют функцию с n аргументами: Func<T₁, T₂, …, Tn, R> и Action<T₁, T₂, …, Tn>.Это потому, что нет типа, который представляет «нет типа».Таким образом, вы не можете объявить функцию, которая ничего не возвращает, используя C # (void является модификатором, а не типом), и поэтому вам необходим отдельный тип (Action) для представления функций, которые ничего не возвращают.Итак, у вас есть два класса Func<R> и Action, которые представляют функции, которые не принимают никаких аргументов, два класса Func<T, R> и Action<T>, которые представляют функции одного аргумента и т. Д., Опять же, примерно до 20.(В Scala функция, которая ничего не возвращает, просто имеет тип возвращаемого значения Unit, так что вы можете иметь, например, Function2[Int, Int, Unit].)

В Java, однако, вам нужно 10 ×9 n типов для представления функции из n аргументов.Позвольте мне продемонстрировать, что с помощью всего лишь одного аргумента:

interface Action1_T                 { void    apply(T       a); }
interface Action1_byte              { void    apply(byte    a); }
interface Action1_short             { void    apply(short   a); }
interface Action1_int               { void    apply(int     a); }
interface Action1_long              { void    apply(long    a); }
interface Action1_float             { void    apply(float   a); }
interface Action1_double            { void    apply(double  a); }
interface Action1_boolean           { void    apply(boolean a); }
interface Action1_char              { void    apply(char    a); }
interface Function1_T_R             { R       apply(T       a); }
interface Function1_T_byte          { byte    apply(T       a); }
interface Function1_T_short         { short   apply(T       a); }
interface Function1_T_int           { int     apply(T       a); }
interface Function1_T_long          { long    apply(T       a); }
interface Function1_T_float         { float   apply(T       a); }
interface Function1_T_double        { double  apply(T       a); }
interface Function1_T_boolean       { boolean apply(T       a); }
interface Function1_T_char          { char    apply(T       a); }
interface Function1_byte_R          { R       apply(byte    a); }
interface Function1_byte_byte       { byte    apply(byte    a); }
interface Function1_byte_short      { short   apply(byte    a); }
interface Function1_byte_int        { int     apply(byte    a); }
interface Function1_byte_long       { long    apply(byte    a); }
interface Function1_byte_float      { float   apply(byte    a); }
interface Function1_byte_double     { double  apply(byte    a); }
interface Function1_byte_boolean    { boolean apply(byte    a); }
interface Function1_byte_char       { char    apply(byte    a); }
interface Function1_short_R         { R       apply(short   a); }
interface Function1_short_byte      { byte    apply(short   a); }
interface Function1_short_short     { short   apply(short   a); }
interface Function1_short_int       { int     apply(short   a); }
interface Function1_short_long      { long    apply(short   a); }
interface Function1_short_float     { float   apply(short   a); }
interface Function1_short_double    { double  apply(short   a); }
interface Function1_short_boolean   { boolean apply(short   a); }
interface Function1_short_char      { char    apply(short   a); }
interface Function1_int_R           { R       apply(int     a); }
interface Function1_int_byte        { byte    apply(int     a); }
interface Function1_int_short       { short   apply(int     a); }
interface Function1_int_int         { int     apply(int     a); }
interface Function1_int_long        { long    apply(int     a); }
interface Function1_int_float       { float   apply(int     a); }
interface Function1_int_double      { double  apply(int     a); }
interface Function1_int_boolean     { boolean apply(int     a); }
interface Function1_int_char        { char    apply(int     a); }
interface Function1_long_R          { R       apply(long    a); }
interface Function1_long_byte       { byte    apply(long    a); }
interface Function1_long_short      { short   apply(long    a); }
interface Function1_long_int        { int     apply(long    a); }
interface Function1_long_long       { long    apply(long    a); }
interface Function1_long_float      { float   apply(long    a); }
interface Function1_long_double     { double  apply(long    a); }
interface Function1_long_boolean    { boolean apply(long    a); }
interface Function1_long_char       { char    apply(long    a); }
interface Function1_float_R         { R       apply(float   a); }
interface Function1_float_byte      { byte    apply(float   a); }
interface Function1_float_short     { short   apply(float   a); }
interface Function1_float_int       { int     apply(float   a); }
interface Function1_float_long      { long    apply(float   a); }
interface Function1_float_float     { float   apply(float   a); }
interface Function1_float_double    { double  apply(float   a); }
interface Function1_float_boolean   { boolean apply(float   a); }
interface Function1_float_char      { char    apply(float   a); }
interface Function1_double_R        { R       apply(double  a); }
interface Function1_double_byte     { byte    apply(double  a); }
interface Function1_double_short    { short   apply(double  a); }
interface Function1_double_int      { int     apply(double  a); }
interface Function1_double_long     { long    apply(double  a); }
interface Function1_double_float    { float   apply(double  a); }
interface Function1_double_double   { double  apply(double  a); }
interface Function1_double_boolean  { boolean apply(double  a); }
interface Function1_double_char     { char    apply(double  a); }
interface Function1_boolean_R       { R       apply(boolean a); }
interface Function1_boolean_byte    { byte    apply(boolean a); }
interface Function1_boolean_short   { short   apply(boolean a); }
interface Function1_boolean_int     { int     apply(boolean a); }
interface Function1_boolean_long    { long    apply(boolean a); }
interface Function1_boolean_float   { float   apply(boolean a); }
interface Function1_boolean_double  { double  apply(boolean a); }
interface Function1_boolean_boolean { boolean apply(boolean a); }
interface Function1_boolean_char    { char    apply(boolean a); }
interface Function1_char_R          { R       apply(char    a); }
interface Function1_char_byte       { byte    apply(char    a); }
interface Function1_char_short      { short   apply(char    a); }
interface Function1_char_int        { int     apply(char    a); }
interface Function1_char_long       { long    apply(char    a); }
interface Function1_char_float      { float   apply(char    a); }
interface Function1_char_double     { double  apply(char    a); }
interface Function1_char_boolean    { boolean apply(char    a); }
interface Function1_char_char       { char    apply(char    a); }

Это 90 (!) Различных типов просто для представления понятия "что-то, что принимает один аргумент".

И, конечно, если я хочу написать что-то, что принимает функцию в качестве аргумента, мне нужно также иметь соответствующее количество перегрузок, поэтому, если я хочу написать метод, который фильтрует некоторые значения на основе предиката, янужно 9 перегрузок этой функции, которые принимают Function1_T_boolean, Function1_byte_boolean, Function1_short_boolean, Function1_int_boolean, Function1_long_boolean, Function1_float_boolean, Function1_double_boolean, Function1_boolean_boolean и Function1_char_boolean.

(Между прочим: это все еще игнорирует проверенные исключения. Технически, нам также нужно 2 n копий каждого из этих 90 интерфейсов, где n - количество различных типов проверенных исключений, которые существуют в Java.)

Итак, вот причины номер 5 и 6: массовый взрыв числа типов и, соответственно, количества методов.

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

Но это еще не все!

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

В заключение я бы сказал, что нет, Java не поддерживает анонимные функции.

Java (после 11)

Java 8 представила целевые лямбда-литералы целевого типа в языке, а также понятие функционального интерфейса и некоторые стандартные типы функциональных интерфейсов библиотеки, а также новый API коллекции основанный на потоках, который интенсивно использует функциональные интерфейсы. Java 9, 10 и 11 добавили дополнительные типы функциональных интерфейсов стандартной библиотеки и расширили Streams. Java 11 добавила вывод типа локальной переменной .

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

Если бы они просто ввели набор новых типов функций, например

interface Function1_int_int {
    int apply(int x)
}

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

(int x) -> x + 1

эквивалентно

new Function1_int_int {
    @Override public int apply(int x) {
        return x + 1;
    }
}

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

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

(int x) -> x + 1

может быть передано в любом месте ожидается следующий тип:

interface * {
    int *(int *)
}

или

class * {
    int *(int *) // abstract!
}

И автоматически реализует класс или интерфейс с правильным именем и автоматически реализует метод с правильным именем.

И не только это основано на структурной типизации, в отличие от всей остальной системы типов Java, которая является номинальной , оно также основано на контексте, который используется в и это то, что подразумевается под «типизированной целью»: тип лямбды зависит не от самой лямбды, а от того, что является ее целью , то есть где она используется.

Вернемся к нашему примеру:

static <A, B> List<B> morph(List<A> l, Function<A, B> f) {
    List<B> r = new ArrayList<>();
    for (var e: l) r.add(f.apply(e));

    return r;
}

Единственное существенное отличие от Java pre-8 здесь заключается в том, что я использую предопределенный функциональный интерфейс java.util.function.Function<T, R>, что означает, что мне не нужно писать свой собственный.

Вот снова наш список (тот же код, что и раньше):

List<Integer> l = Arrays.asList(1, 2, 3, 4, 5);

Но вот, как мы называем наше morph, это главное отличие. Вместо этого:

morph(l, new Function<Integer, Integer>() {
    @Override public Integer apply(Integer n) {
        return n + 1;
    }
});

Теперь у нас есть это:

morph(l, n -> n + 1);

О, я почти забыл: где-то был другой вопрос:

Что на самом деле является анонимной функцией

Это легко. Функция без имени.

а как можно сказать, что какой-то язык поддерживает анонимные функции?

Если с ними легко работать.

Java pre-8 не делает: есть разница между поддержкой анонимных функций и возможностью эмулировать подмножество функций анонимных функций путем кодирования их в анонимные внутренние классы SAM.Java post-8 намного приятнее.

0 голосов
/ 19 ноября 2010

Одно важное различие между (скажем) лямбда-выражениями в C # и анонимными внутренними классами в Java заключается в том, что в Java любые локальные переменные, на которые ссылаются внутренние классы, фиксируются значением - поэтому переменнаядолжен быть окончательным.Значение этой переменной копируется во внутренний класс при создании.

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

Вы можете эмулировать это в Java, обернув исходную переменную в изменяемую оболочку - например, массив из одного элемента - и сделав переменную wrapper final.Хотя это довольно неприятный хак.

Самая большая проблема с использованием анонимных внутренних классов IMO - это простое многословие - указание класса, который вы расширяете, затем метод, который вы переопределяете и т.д.

Подробнее о замыканиях и, в частности, сравнении Java и C # в этом отношении см. мою статью на тему .

...