Как писать полезные Java-программы без использования изменяемых переменных

12

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

(take 25 (squares-of (integers)))

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

Возможно ли достичь этого на Java? Предположим, что вы должны напечатать квадраты первых 15 целых чисел. Можете ли вы написать цикл for или while без использования переменных?

Мод уведомления

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

minusSeven
источник
19
Ваш функциональный пример делает переменные использования на внутренней стороне , но язык делает все это за кулисами. Вы фактически делегировали неприятные детали тому, кто, по вашему мнению, сделал это правильно.
Blrfl
12
@Blrfl: Аргумент «за кулисами» убивает все языковые дебаты, поскольку каждый фрагмент кода в конечном итоге переводится в машинный код x86. Код x86 не является объектно-ориентированным, не процедурным, не функциональным и не чем-то, но эти категории являются ценными тегами для языков программирования. Посмотрите на язык, а не на реализацию.
13
10
@thiton Не согласен. Blrfl говорит, что эти функции, вероятно, используют переменные, написанные на одном языке программирования . Не нужно идти на низком уровне здесь. Фрагмент просто использует библиотечные функции. Вы можете легко написать тот же код на Java, см .: squaresOf(integers()).take(25)(написание этих функций оставлено для читателя в качестве упражнения; сложность заключается в бесконечном наборе integers()значений для , но это проблема для Java из-за ее стремительной оценки, не имеющей ничего общего с переменные)
Андрес Ф.
6
Эта цитата сбивает с толку и вводит в заблуждение, там нет никакой магии, только синтаксический сахар .
Яннис
2
@thiton Я предлагаю вам больше узнать о языках FP, но, тем не менее, фрагмент кода не поддерживает (или отклоняет) утверждение о том, что «переменные» не нужны (под этим я предполагаю, что вы имеете в виду «изменяемые переменные», потому что другой вид обычен в FP). Фрагмент показывает только библиотечные функции, которые могли бы быть реализованы и на Java, за исключением ленивых / энергичных проблем, которые здесь не имеют смысла.
Андрес Ф.

Ответы:

31

Можно ли реализовать такой пример в Java без использования деструктивных обновлений? Да. Однако, как упоминали @Thiton и сама статья, это будет некрасиво (в зависимости от вкуса). Одним из способов является использование рекурсии; Вот пример Haskell, который делает нечто подобное:

unfoldr      :: (b -> Maybe (a, b)) -> b -> [a]
unfoldr f b  =
  case f b of
   Just (a,new_b) -> a : unfoldr f new_b
   Nothing        -> []  

Примечание 1) отсутствие мутации, 2) использование рекурсии и 3) отсутствие петель. Последний пункт очень важен - функциональным языкам не нужны встроенные в язык циклические конструкции, поскольку рекурсия может использоваться для большинства (всех?) Случаев, когда в Java используются циклы. Вот известная серия статей, показывающая, насколько невероятно выразительными могут быть вызовы функций.


Я нашел статью неудовлетворительной и хотел бы сделать пару дополнительных замечаний:

Эта статья очень плохое и запутанное объяснение функционального программирования и его преимуществ. Я настоятельно рекомендую другие источники для изучения функционального программирования.

Самая запутанная часть статьи заключается в том, что в ней не упоминается, что существует два варианта использования операторов присваивания в Java (и большинстве других основных языков):

  1. привязка значения к имени: final int MAX_SIZE = 100;

  2. разрушительное обновление: int a = 3; a += 1; a++;

Функциональное программирование избегает второго, но охватывает первое (примеры: let-выражения, параметры функции, defineвыражения верхнего уровня ) . Это очень важный момент для понимания, потому что в противном случае эта статья просто кажется глупой и может оставить вас интересно, что это take, squares-ofи integersесли не переменные?

Кроме того, пример не имеет смысла. Он не показывает реализации take, squares-ofили integers. Насколько нам известно, они реализованы с использованием изменяемых переменных. Как сказал @Martin, вы можете написать этот пример на Java.

Еще раз, я бы рекомендовал избегать этой статьи и других подобных, если вы действительно хотите узнать о функциональном программировании. Кажется, это написано больше с целью шокировать и оскорблять, а не преподавать концепции и основы. Вместо этого, почему бы не проверить одну из моих самых любимых работ Джона Хьюза? Хьюз пытается решить некоторые из тех же проблем, что и в статье (хотя Хьюз не говорит о параллелизме / распараллеливании); вот тизер:

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

[...]

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


источник
10
+1 за «Я бы порекомендовал избегать этой статьи и других подобных, если вы действительно хотите узнать о функциональном программировании. Кажется, что она написана больше с целью шокировать и оскорблять, а не преподавать концепции и основы»
3
Половина причины, по которой люди не делают FP, заключается в том, что они ничего не слышат / не узнают об этом в универе, а другая половина - потому что, когда они смотрят на это, они находят статьи, которые оставляют их и не информированными, и думают, что это все для какой-то фантастики. играть, а не продуманный аргументированный подход с преимуществами. +1 за предоставление лучших источников информации
Джимми Хоффа
3
Если хотите, поместите свой ответ на вопрос на абсолютной вершине, чтобы он был более прямым к вопросу, и, возможно, этот вопрос останется открытым тогда (с прямым ответом, сфокусированным на вопросе)
Джимми Хоффа
2
Извините, но я не понимаю, почему вы выбрали этот код haskell. Я читал ЛЯ, и твой пример мне не удался. Я также не вижу связи с оригинальным вопросом. Почему вы просто не использовали take 25 (map (^2) [1..])в качестве примера?
Даниэль Каплан
2
@tieTYT хороший вопрос - спасибо за указание на это. Причина, по которой я использовал этот пример, заключается в том, что он показывает, как создать список чисел, используя рекурсию и избегая изменчивых переменных. Я хотел, чтобы OP увидел этот код и подумал о том, как сделать что-то подобное в Java. Что касается вашего кода, что это [1..]? Это классная функция, встроенная в Haskell, но она не иллюстрирует концепцию создания такого списка. Я уверен, что экземпляры Enumкласса (которые требует этот синтаксис) также полезны, но было слишком лениво, чтобы их найти. Таким образом, unfoldr. :)
27

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

Итак, путь Java будет таким:

for (int i = 1; i <= 25; ++i) {
    System.out.println(i*i);
}

и не позволяйте себе быть обманутым, чтобы написать это более сложным способом из-за ненависти к переменным.

thiton
источник
5
"Ненависть к переменным"? Оооооо ... Что вы читали о функциональном программировании? На каких языках вы пробовали? Какие учебники?
Андрес Ф.
8
@AndresF .: Более двух лет курсовых работ на Хаскеле. Я не говорю, что FP это плохо. Однако во многих обсуждениях FP-vs-IP (таких как связанная статья) существует тенденция осуждать использование переназначаемых именованных объектов (переменных AKA) и осуждать без веской причины или данных. Необоснованное осуждение - это ненависть в моей книге. И ненависть делает действительно плохой код.
13
10
«Ненависть к переменным» - это чрезмерное упрощение причин. En.wikipedia.org/wiki/Fallacy_of_the_single_cause есть много преимуществ для программирования без сохранения состояния, которое может быть даже в Java, хотя я согласен с вашим ответом, что в Java стоимость будет слишком высокой по сложности программа и не-идиоматические. Я все равно не стал бы отказываться от идеи о том, что программирование без состояния - это хорошо, а состояние - плохо, потому что какой-то эмоциональный отклик, а не аргументированная, хорошо продуманная позиция, к которой люди пришли из-за опыта.
Джимми Хоффа
2
В соответствии с тем, что говорит @JimmyHoffa, я отошлю вас к Джону Кармаку на тему программирования в функциональном стиле на императивных языках (C ++ в его случае) ( altdevblogaday.com/2012/04/26/functional-programming-in-c ).
Стивен Эверс
5
Необоснованное осуждение не является ненавистью, и избегание изменчивого состояния не является необоснованным.
Майкл Шоу
21

Самое простое, что я могу сделать с рекурсией - это функция с одним параметром. Это не очень Java-esque, но это работает:

public class squares
{
    public static void main(String[] args)
    {
        squares(15);
    }

    private static void squares(int x)
    {
        if (x>0)
        {
            System.out.println(x*x);
            squares(x-1);
        }
    }
}
FrustratedWithFormsDesigner
источник
3
+1 за попытку ответить на вопрос на примере Java.
KChaloux,
Я бы понизил это для представления кода в стиле гольфа (см. Уведомление о моде ), но не могу заставить себя нажать стрелку вниз, потому что этот код идеально соответствует утверждениям, сделанным в моем любимом ответе : «1) отсутствие мутации, 2) использование рекурсия и 3) отсутствие петель »
комнат
3
@gnat: Этот ответ был опубликован до уведомления мода. Я не стремился к великолепному стилю, я шел к простоте и удовлетворял первоначальный вопрос ОП; чтобы показать , что это можно делать такие вещи в Java.
FrustratedWithFormsDesigner
@FrustratedWithFormsDesigner уверен; это не остановит меня от DVing (так как вы должны быть в состоянии редактировать, чтобы соответствовать) - это поразительно идеальное совпадение, которое сделало волшебство. Хорошо сделано, действительно хорошо сделано, довольно познавательно - спасибо
комнат
16

В вашем функциональном примере мы не видим , как squares-ofи takeфункции реализованы. Я не эксперт по Java, но я почти уверен, что мы могли бы написать эти функции, чтобы включить подобное утверждение ...

squares_of(integers).take(25);

что не так сильно отличается.

Мартин
источник
6
Nitpick: squares-ofне является допустимым именем в Java ( squares_ofхотя). Но в остальном, хороший момент, который показывает, что пример статьи плохой.
Я подозреваю, что статья integerлениво генерирует целые числа, и takeфункция выбирает 25 из squared-ofчисел integer. Короче говоря, у вас должна быть integerфункция лениво производить целые числа до бесконечности.
OnesimusUnbound
Вызывать что-то похожее (integer)на функцию - это немного безумие - функция все еще является тем, что отображает аргумент в значение. Оказывается, это (integer)не функция, а просто значение. Можно даже пойти так далеко, чтобы сказать, что integerэто переменная, которая связана с бесконечным потоком чисел.
Инго
6

В Java вы можете сделать это (особенно часть бесконечного списка) с итераторами. В следующем примере кода число, предоставляемое Takeконструктору, может быть произвольно высоким.

class Example {
    public static void main(String[] a) {
        Numbers test = new Take(25, new SquaresOf(new Integers()));
        while (test.hasNext())
            System.out.println(test.next());
    }
}

Или с помощью цепных заводских методов:

class Example {
    public static void main(String[] a) {
        Numbers test = Numbers.integers().squares().take(23);
        while (test.hasNext())
            System.out.println(test.next());
    }
}

Где SquaresOf, Takeи IntegersпродлитьNumbers

abstract class Numbers implements Iterator<Integer> {
    public static Numbers integers() {
        return new Integers();
    }

    public Numbers squares() {
        return new SquaresOf(this);
    }

    public Numbers take(int c) {
        return new Take(c, this);
    }
    public void remove() {}
}
artistoex
источник
1
Это показывает превосходство ОО-парадигмы над функциональной; при правильном дизайне OO вы можете имитировать функциональную парадигму, но вы не можете имитировать OO парадигму в функциональном стиле.
m3th0dman
3
@ m3th0dman: При правильном дизайне ОО вы, возможно, наполовину имитируете FP, как любой язык, имеющий строки, списки и / или словари, может наполовину имитировать ОО. Эквивалентность по Тьюрингу языков общего назначения означает, что при достаточных усилиях любой язык может имитировать свойства любого другого.
cHao
Обратите внимание, что итераторы в стиле Java, такие как in while (test.hasNext()) System.out.println(test.next()), в FP будут запрещены; итераторы по своей природе изменчивы.
Чао
1
@ cHao Я вряд ли верю, что истинную инкапсуляцию или полиморфизм можно имитировать; также Java (в этом примере) не может действительно имитировать функциональный язык из-за строгой нетерпеливой оценки. Я также считаю, что итераторы могут быть написаны рекурсивно.
m3th0dman
@ m3th0dman: Полиморфизм совсем не сложно подражать; даже C и ассемблер могут сделать это. Просто сделайте метод полем в объекте или дескрипторе класса / vtable. И инкапсуляция в смысле сокрытия данных не является строго необходимой; половина языков не обеспечивает его, когда ваш объект неизменен, не имеет значения, смогут ли люди увидеть его внутренности в любом случае. все, что нужно, - это перенос данных , который легко могли бы обеспечить вышеупомянутые поля метода.
cHao
6

Укороченная версия:

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

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

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


Длинная версия:

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

Java был разработан с самого начала как императивный , объектно-ориентированный язык.

  • Императивные языки почти всегда зависят от изменяемых переменных. Например, они предпочитают итерацию, а не рекурсию, и почти всем итерационным конструкциям - даже while (true)и for (;;)! - полностью зависят от переменной, где-то переходящей от итерации к итерации.
  • Объектно-ориентированные языки в значительной степени представляют каждую программу в виде графа объектов, отправляющих сообщения друг другу, и почти во всех случаях, отвечающих на эти сообщения путем мутации чего-либо.

Конечный результат этих дизайнерских решений заключается в том, что без изменяемых переменных у Java нет возможности изменить состояние чего-либо - даже такого простого, как печать «Hello world!» к экрану относится выходной поток, который включает в себя вставку байтов в изменяемый буфер.

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

class Ints {
     final int value;
     final Ints tail;

     public Ints(int value, Ints rest) {
         this.value = value;
         this.tail = rest;
     }
     public Ints next() { return this.tail; }
     public int value() { return this.value; }
}

public Ints take(int count, Ints input) {
    if (count == 0 || input == null) return null;
    return new Ints(input.value(), take(count - 1, input.next()));
}    

public Ints squares_of(Ints input) {
    if (input == null) return null;
    int i = input.value();
    return new Ints(i * i, squares_of(input.next()));
}

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

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

public function doStuff() {
    final Ints integers = ...somehow assemble list of 20 million ints...;
    final Ints result = take(25, squares_of(integers));
    ...
}

Хотя нам нужно только 25 дюймов для результата, squares_ofне знает этого. Это собирается вернуть квадрат каждого числа в integers. Рекурсия глубиной в 20 миллионов вызывает большие проблемы в Java.

Видите ли, функциональные языки, в которых вы обычно совершаете дурацкие поступки, имеют функцию, называемую «устранение хвостовых вызовов». Это означает, что когда компилятор видит, что последним действием кода является сам вызов (и возвращает результат, если функция не является пустым), он использует кадр стека текущего вызова вместо установки нового и вместо этого выполняет «переход» "вызова" (поэтому используемое пространство стека остается постоянным). Короче говоря, до 90% пути превращения хвостовой рекурсии в итерацию. Он может справиться с этими миллиардами целых без переполнения стека. (В конце концов, все равно не хватило бы памяти, но при составлении списка из миллиарда целых в любом случае это может испортить вам память в 32-битной системе.)

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

Один полуобход ... ленивая оценка. У нас все еще есть ограничения стека, но они могут быть связаны с факторами, которые мы можем контролировать больше. Нам не нужно рассчитывать миллион целых, чтобы вернуть 25. :)

Итак, давайте создадим нам инфраструктуру для ленивых вычислений. (Этот код был протестирован некоторое время назад, но с тех пор я немного его изменил; прочитайте идею, а не синтаксические ошибки. :))

// Represents something that can give us instances of OutType.
// We can basically treat this class like a list.
interface Source<OutType> {
     public Source<OutType> next();
     public OutType value();
}

// Represents an operation that turns an InType into an OutType.
// Note, these can be the same type.  We're just flexible like that.
interface Transform<InType, OutType> {
    public OutType appliedTo(InType input);
}

// Represents an action (as opposed to a function) that can run on
// every element of a sequence.
abstract class Action<InType> {
    abstract void doWith(final InType input);
    public void doWithEach(final Source<InType> input) {
        if (input == null) return;
        doWith(input.value());
        doWithEach(input.next());
    }
}

// A list of Integers.
class Ints implements Source<Integer> {
     final Integer value;
     final Ints tail;
     public Ints(Integer value, Ints rest) {
         this.value = value;
         this.tail = rest;
     }
     public Ints(Source<Integer> input) {
         this.value = input.value();
         this.tail = new Ints(input.next());
     }
     public Source<Integer> next() { return this.tail; }
     public Integer value() { return this.value; }
     public static Ints fromArray(Integer[] input) {
         return fromArray(input, 0, input.length);
     }
     public static Ints fromArray(Integer[] input, int start, int end) {
         if (end == start || input == null) return null;
         return new Ints(input[start], fromArray(input, start + 1, end));
     }
}

// An example of the spiff we get by splitting the "iterator" interface
// off.  These ints are effectively generated on the fly, as opposed to
// us having to build a huge list.  This saves huge amounts of memory
// and CPU time, for the rather common case where the whole sequence
// isn't needed.
class Range implements Source<Integer> {
    final int start, end;
    public Range(int start, int end) {
        this.start = start;
        this.end = end;
    }
    public Integer value() { return start; }
    public Source<Integer> next() {
        if (start >= end) return null;
        return new Range(start + 1, end);
    }
}

// This takes each InType of a sequence and turns it into an OutType.
// This *takes* a Transform, rather than just *implementing* Transform,
// because the transforms applied are likely to be specified inline.
// If we just let people override `value()`, we wouldn't easily know what type
// to return, and returning our own type would lose the transform method.
static class Mapper<InType, OutType> implements Source<OutType> {
    private final Source<InType> input;
    private final Transform<InType, OutType> transform;

    public Mapper(Transform<InType, OutType> transform, Source<InType> input) {
        this.transform = transform;
        this.input = input;
    }

    public Source<OutType> next() {
         return new Mapper<InType, OutType>(transform, input.next());
    }
    public OutType value() {
         return transform.appliedTo(input.value());
    }
}

// ...

public <T> Source<T> take(int count, Source<T> input) {
    if (count <= 0 || input == null) return null;
    return new Source<T>() {
        public T value() { return input.value(); }
        public Source<T> next() { return take(count - 1, input.next()); }
    };
}

(Имейте в виду, что если бы это было действительно жизнеспособно в Java, код, по крайней мере, в некоторой степени похожий на приведенный выше, уже был бы частью API.)

Теперь, имея инфраструктуру, довольно просто писать код, который не требует изменяемых переменных и, по крайней мере, стабилен при меньших объемах ввода.

public Source<Integer> squares_of(Source<Integer> input) {
     final Transform<Integer, Integer> square = new Transform<Integer, Integer>() {
         public Integer appliedTo(final Integer i) { return i * i; }
     };
     return new Mapper<>(square, input);
}


public void example() {
    final Source<Integer> integers = new Range(0, 1000000000);

    // and, as for the author's "bet you can't do this"...
    final Source<Integer> squares = take(25, squares_of(integers));

    // Just to make sure we got it right :P
    final Action<Integer> printAction = new Action<Integer>() {
        public void doWith(Integer input) { System.out.println(input); }
    };
    printAction.doWithEach(squares);
}

Это в основном работает, но все еще несколько подвержено переполнению стека. Попробуйте take2 миллиарда целых и сделайте с ними какие-то действия. : P Это в конечном итоге вызовет исключение, по крайней мере, до тех пор, пока 64+ ГБ ОЗУ не станут стандартными. Проблема в том, что объем памяти программы, который зарезервирован для ее стека, не так велик. Обычно это от 1 до 8 МиБ. (Вы можете попросить больше, но это не имеет значения , все , что много , сколько вы просите - вы звоните take(1000000000, someInfiniteSequence), вы будете получать исключение.) Контроля К счастью, с ленивыми вычислениями, слабое место в области , мы можем лучше , Мы просто должны быть осторожны с тем, сколько мы take().

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

public <T> void doSomethingWith(T input) { /* magic happens here */ }
public <T> Source<T> workWith(Source<T> input, int count) {
    if (count < 0 || input == null) return null;
    if (count == 0) return input;
    if (count == 1) {
        doSomethingWith(input.value());
        return input.next();
    }
    return (workWith(workWith(input, count/2), count - count/2);
}

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

Проблема в том, что эта функция хочет ввода - и со связанным списком, получение длины требует обхода всего списка. Это легко решается, хотя; просто не важно сколько там записей. :) Приведенный выше код будет работать с чем-то вроде Integer.MAX_VALUEсчетчика, так как null в любом случае останавливает обработку. Количество в основном там, поэтому у нас есть солидный базовый случай. Если вы ожидаете, что Integer.MAX_VALUEв списке будет больше записей, вы можете проверить workWithвозвращаемое значение - в конце оно должно быть нулевым. В противном случае, рекурсировать.

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

Chao
источник
3

Ранее я пытался создать интерпретатор для lisp-подобного языка в Java (несколько лет назад и весь код был потерян, как это было в CVS в sourceforge), и итераторы утилит Java немного многословны для функционального программирования. в списках.

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

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

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

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

Смысл square ( int x )функции состоит только в том, чтобы устранить необходимость вызова head()дважды - обычно я сделал бы это, поместив значение в конечную переменную, но добавление этой функции означает, что в программе нет переменных, только параметры функции.

Многословие Java для такого рода программирования побудило меня написать вторую версию моего интерпретатора вместо C99.

public class Squares {
    interface Seq<T> {
        T head();
        Seq<T> tail();
    }

    public static void main (String...args) {
        print ( take (25, integers ) );
        print ( take (25, squaresOf ( integers ) ) );
        print ( take (25, squaresOfUsingMap ( integers ) ) );
    }

    static Seq<Integer> CreateIntSeq ( final int n) {
        return new Seq<Integer> () {
            public Integer head () {
                return n;
            }
            public Seq<Integer> tail () {
                return n > 0 ? CreateIntSeq ( -n ) : CreateIntSeq ( 1 - n );
            }
        };
    }

    public static final Seq<Integer> integers = CreateIntSeq(0);

    public static Seq<Integer> squaresOf ( final Seq<Integer> source ) {
        return new Seq<Integer> () {
            public Integer head () {
                return square ( source.head() );
            }
            public Seq<Integer> tail () {
                return squaresOf ( source.tail() );
            }
        };
    }

    // mapping a function over a list rather than implementing squaring of each element
    interface Fun<T> {
        T apply ( T value );
    }

    public static Seq<Integer> squaresOfUsingMap ( final Seq<Integer> source ) {
        return map ( new Fun<Integer> () {
            public Integer apply ( final Integer value ) {
                return square ( value );
            }
        }, source );
    }

    public static <T> Seq<T> map ( final Fun<T> fun, final Seq<T> source ) {
        return new Seq<T> () {
            public T head () {
                return fun.apply ( source.head() );
            }
            public Seq<T> tail () {
                return map ( fun, source.tail() );
            }
        };
    }

    public static Seq<Integer> take ( final int count,  final Seq<Integer> source ) {
        return new Seq<Integer> () {
            public Integer head () {
                return source.head();
            }
            public Seq<Integer> tail () {
                return count > 0 ? take ( count - 1, source.tail() ) : nil;
            }
        };
    }

    public static int square ( final int x ) {
        return x * x;
    }

    public static final Seq<Integer> nil = new Seq<Integer> () {
        public Integer head () {
            throw new RuntimeException();
        }
        public Seq<Integer> tail () {
            return this;
        }
    };

    public static <T> void print ( final Seq<T> seq ) {
        printPartSeq ( "[", seq.head(), seq.tail() );
    }

    private static <T> void printPartSeq ( final String prefix, final T value, final Seq<T> seq ) {
        if ( seq == nil) {
            System.out.println("]");
        } else {
            System.out.print(prefix);
            System.out.print(value);
            printPartSeq ( ",", seq.head(), seq.tail() );
        }
    }
}
Пит Киркхэм
источник
3

Как писать полезные Java-программы без использования изменяемых переменных.

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

На практике:

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

  • То же самое для библиотек. И если вы ограничиваете себя библиотечными классами, которые не используют мутации под прикрытием, это еще сложнее. (Вы даже не можете использовать String ... посмотрите, как hashcodeэто реализовано.)

  • Основные реализации Java не поддерживают оптимизацию хвостового вызова. Это означает, что рекурсивные версии алгоритмов имеют тенденцию быть «голодными» в пространстве стека. А поскольку стеки Java-потоков не растут, вам нужно предварительно выделить большие стеки ... или риск StackOverflowError.

Объедините эти три вещи, и Java на самом деле не является жизнеспособным вариантом для написания полезных (то есть нетривиальных) программ без изменяемых переменных.

(Но, эй, все в порядке. Для JVM доступны другие языки программирования, некоторые из которых поддерживают функциональное программирование.)

Стивен С
источник
2

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

cat /dev/zero | tr '\0' '\n' | cat -n | awk '{ print $0 * $0 }' | head 25

В Linux это означает, что дайте мне байты, каждый из которых состоит из ложных, а не истинных битов, пока я не потеряю свой аппетит; измените каждый из этих байтов на символ новой строки; нумерация каждой строки, созданной таким образом; и произвести квадрат этого числа. Кроме того, у меня аппетит на 25 строк и не более.

Я утверждаю, что программисту не советуют так писать конвейер Linux. Это относительно нормальные сценарии оболочки Linux.

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

С другой стороны, я утверждаю, что следующий программист мог бы быть более восприимчивым, если некоторые из наших пакетов «Java» на самом деле являются пакетами виртуальной машины Java, написанными на одном из функциональных или объектно-функциональных языков, таких как Clojure и Scala. Они предназначены для кодирования путем объединения функций и вызова из Java обычным способом вызовов методов Java.

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

Недавно мой любимый метод [заключался в том, чтобы] использовать неизменяемую неинициализированную возвращаемую переменную и один выход, чтобы, как это делают некоторые компиляторы функционального языка, Java проверял, что независимо от того, что происходит в теле функции, я всегда предоставляю один и только один возвращаемое значение Пример:

int f(final int n) {
    final int result; // not initialized here!
    if (n < 0) {
        result = -n;
    } else if (n < 1) {
        result = 0;
    } else {
        result = n - 1;
    }
    // If I would leave off the "else" clause,
    // Java would fail to compile complaining that
    // "result" is possibly uninitialized.
    return result;
}

minopret
источник
Я на 70% уверен, что в любом случае Java уже проверяет возвращаемое значение. Вы должны получить ошибку об «пропущенном операторе возврата», если управление может выпасть из конца не пустой функции.
Чао
Моя точка зрения: если вы кодируете его так, как int result = -n; if (n < 1) { result = 0 } return result;он хорошо компилируется, и компилятор понятия не имеет, намеревались ли вы сделать его эквивалентным функции в моем примере. Может быть, этот пример слишком прост, чтобы техника выглядела полезной, но в функции с большим количеством ветвей я чувствую, что приятно пояснить, что результат присваивается ровно один раз, независимо от того, по какому пути следуют.
минопрет
Однако, если вы скажете if (n < 1) return 0; else return -n;, у вас не возникнет проблем ... и, кроме того, все будет проще. :) Мне кажется , что в этом случае «один возвращение» правило на самом деле помогает причине вопроса , не зная , когда ваше возвращаемое значение было установлено; в противном случае вы можете просто вернуть его, и Java сможет лучше определить, когда другие пути могут не возвращать значение, потому что вы больше не отделяете вычисление значения от фактического его возвращения.
Чао
Или, например, для вашего ответа if (n < 0) return -n; else if (n == 0) return 0; else return n - 1;.
Чао
Я просто решил, что больше не хочу тратить моменты своей жизни на защиту правила OnlyOneReturn в Java. Выходит. Когда и если я подумаю о практике программирования на Java, которая, как мне кажется, требует защиты, на которую влияют методы функционального программирования, я заменю этот пример. До тех пор ни одного примера.
минопрет
0

Самый простой способ выяснить это - передать следующее компилятору Frege и посмотреть на сгенерированный код Java:

module Main where

result = take 25 (map sqr [1..]) where sqr x = x*x
Инго
источник
Через несколько дней мои мысли вернулись к этому ответу. Ведь частью моего предложения было реализовать функциональные части программирования в Scala. Если мы рассмотрим применение Scala в тех местах, где мы действительно думаем о Haskell (и я думаю, что я не единственный blog.zlemma.com/2013/02/20/… ), не должны ли мы хотя бы рассмотреть Фреге?
минопрет
@minopret Это действительно ниша, которую Фреге торгует - люди, которые узнали и любят Haskell и все же нуждаются в JVM. Я уверен, что однажды Фреге станет достаточно зрелым, чтобы хотя бы серьезно задуматься.
Инго