Я читал статью о функциональном программировании, где автор пишет
(take 25 (squares-of (integers)))
Обратите внимание, что в нем нет переменных. Действительно, он имеет не более трех функций и одну константу. Попробуйте написать квадраты целых чисел в Java без использования переменной. О, вероятно, есть способ сделать это, но это, конечно, не естественно, и это не будет читать так же хорошо, как моя программа выше.
Возможно ли достичь этого на Java? Предположим, что вы должны напечатать квадраты первых 15 целых чисел. Можете ли вы написать цикл for или while без использования переменных?
Мод уведомления
Этот вопрос не является кодом соревнования по гольфу. Мы ищем ответы, которые объясняют используемые концепции (в идеале, без повторения более ранних ответов), а не просто для еще одного куска кода.
источник
squaresOf(integers()).take(25)
(написание этих функций оставлено для читателя в качестве упражнения; сложность заключается в бесконечном набореintegers()
значений для , но это проблема для Java из-за ее стремительной оценки, не имеющей ничего общего с переменные)Ответы:
Можно ли реализовать такой пример в Java без использования деструктивных обновлений? Да. Однако, как упоминали @Thiton и сама статья, это будет некрасиво (в зависимости от вкуса). Одним из способов является использование рекурсии; Вот пример Haskell, который делает нечто подобное:
Примечание 1) отсутствие мутации, 2) использование рекурсии и 3) отсутствие петель. Последний пункт очень важен - функциональным языкам не нужны встроенные в язык циклические конструкции, поскольку рекурсия может использоваться для большинства (всех?) Случаев, когда в Java используются циклы. Вот известная серия статей, показывающая, насколько невероятно выразительными могут быть вызовы функций.
Я нашел статью неудовлетворительной и хотел бы сделать пару дополнительных замечаний:
Эта статья очень плохое и запутанное объяснение функционального программирования и его преимуществ. Я настоятельно рекомендую другие источники для изучения функционального программирования.
Самая запутанная часть статьи заключается в том, что в ней не упоминается, что существует два варианта использования операторов присваивания в Java (и большинстве других основных языков):
привязка значения к имени:
final int MAX_SIZE = 100;
разрушительное обновление:
int a = 3; a += 1; a++;
Функциональное программирование избегает второго, но охватывает первое (примеры:
let
-выражения, параметры функции,define
выражения верхнего уровня ) . Это очень важный момент для понимания, потому что в противном случае эта статья просто кажется глупой и может оставить вас интересно, что этоtake
,squares-of
иintegers
если не переменные?Кроме того, пример не имеет смысла. Он не показывает реализации
take
,squares-of
илиintegers
. Насколько нам известно, они реализованы с использованием изменяемых переменных. Как сказал @Martin, вы можете написать этот пример на Java.Еще раз, я бы рекомендовал избегать этой статьи и других подобных, если вы действительно хотите узнать о функциональном программировании. Кажется, это написано больше с целью шокировать и оскорблять, а не преподавать концепции и основы. Вместо этого, почему бы не проверить одну из моих самых любимых работ Джона Хьюза? Хьюз пытается решить некоторые из тех же проблем, что и в статье (хотя Хьюз не говорит о параллелизме / распараллеливании); вот тизер:
источник
take 25 (map (^2) [1..])
в качестве примера?[1..]
? Это классная функция, встроенная в Haskell, но она не иллюстрирует концепцию создания такого списка. Я уверен, что экземплярыEnum
класса (которые требует этот синтаксис) также полезны, но было слишком лениво, чтобы их найти. Таким образом,unfoldr
. :)Вы бы не. Переменные лежат в основе императивного программирования, и если вы пытаетесь программировать императивно без использования переменных, вы просто причиняете всем боль в заднице. В разных парадигмах программирования стили различны, и различные концепции составляют основу. Переменная в Java, когда она хорошо используется с небольшой областью действия, не является злом. Запрашивать Java-программу без переменных - это все равно, что запрашивать программу на Haskell без функций, поэтому вы не просите об этом и не позволяете себе обмануть себя, рассматривая императивное программирование как неполноценное, поскольку оно использует переменные.
Итак, путь Java будет таким:
и не позволяйте себе быть обманутым, чтобы написать это более сложным способом из-за ненависти к переменным.
источник
Самое простое, что я могу сделать с рекурсией - это функция с одним параметром. Это не очень Java-esque, но это работает:
источник
В вашем функциональном примере мы не видим , как
squares-of
иtake
функции реализованы. Я не эксперт по Java, но я почти уверен, что мы могли бы написать эти функции, чтобы включить подобное утверждение ...что не так сильно отличается.
источник
squares-of
не является допустимым именем в Java (squares_of
хотя). Но в остальном, хороший момент, который показывает, что пример статьи плохой.integer
лениво генерирует целые числа, иtake
функция выбирает 25 изsquared-of
чиселinteger
. Короче говоря, у вас должна бытьinteger
функция лениво производить целые числа до бесконечности.(integer)
на функцию - это немного безумие - функция все еще является тем, что отображает аргумент в значение. Оказывается, это(integer)
не функция, а просто значение. Можно даже пойти так далеко, чтобы сказать, чтоinteger
это переменная, которая связана с бесконечным потоком чисел.В Java вы можете сделать это (особенно часть бесконечного списка) с итераторами. В следующем примере кода число, предоставляемое
Take
конструктору, может быть произвольно высоким.Или с помощью цепных заводских методов:
Где
SquaresOf
,Take
иIntegers
продлитьNumbers
источник
while (test.hasNext()) System.out.println(test.next())
, в FP будут запрещены; итераторы по своей природе изменчивы.Укороченная версия:
Для того чтобы стиль единого назначения работал надежно в Java, вам понадобится (1) какая-то неизменяемая инфраструктура и (2) поддержка на уровне компилятора или среды выполнения для устранения хвостовых вызовов.
Мы можем написать большую часть инфраструктуры, и мы можем организовать вещи, чтобы избежать заполнения стека. Но до тех пор, пока каждый вызов занимает кадр стека, будет ограничено количество рекурсии, которое вы можете выполнить. Держите ваши итераторы маленькими и / или ленивыми, и у вас не должно быть серьезных проблем. По крайней мере, большинство проблем, с которыми вы столкнетесь, не требуют одновременного возврата миллиона результатов. :)
Также обратите внимание: поскольку для того, чтобы быть достойным запуска, программа должна фактически вносить видимые изменения, вы не можете сделать все неизменным. Однако вы можете оставить неизменным подавляющее большинство ваших собственных вещей, используя небольшое подмножество необходимых изменяемых файлов (например, потоков) только в определенных ключевых точках, где альтернативы будут слишком обременительными.
Длинная версия:
Проще говоря, Java-программа не может полностью избежать переменных, если она хочет сделать что-то стоящее. Вы можете содержать их и, таким образом, в значительной степени ограничивать изменчивость, но сам дизайн языка и API - наряду с необходимостью в конечном итоге изменять базовую систему - делают невозможным полную неизменность.
Java был разработан с самого начала как императивный , объектно-ориентированный язык.
while (true)
иfor (;;)
! - полностью зависят от переменной, где-то переходящей от итерации к итерации.Конечный результат этих дизайнерских решений заключается в том, что без изменяемых переменных у Java нет возможности изменить состояние чего-либо - даже такого простого, как печать «Hello world!» к экрану относится выходной поток, который включает в себя вставку байтов в изменяемый буфер.
Таким образом, для практических целей мы ограничены исключением переменных из нашего собственного кода. ОК, мы можем сделать это. Почти. По сути, нам нужно заменить почти всю итерацию рекурсией, а все мутации - рекурсивными вызовами, возвращающими измененное значение. вот так...
По сути, мы строим связанный список, где каждый узел сам по себе является списком. Каждый список имеет «голову» (текущее значение) и «хвост» (оставшийся подсписок). Большинство функциональных языков делают что-то похожее на это, потому что это очень поддается эффективной неизменности. «Следующая» операция просто возвращает хвост, который обычно передается на следующий уровень в стеке рекурсивных вызовов.
Теперь это чрезвычайно упрощенная версия этого материала. Но этого достаточно, чтобы продемонстрировать серьезную проблему с этим подходом в Java. Рассмотрим этот код:
Хотя нам нужно только 25 дюймов для результата,
squares_of
не знает этого. Это собирается вернуть квадрат каждого числа вintegers
. Рекурсия глубиной в 20 миллионов вызывает большие проблемы в Java.Видите ли, функциональные языки, в которых вы обычно совершаете дурацкие поступки, имеют функцию, называемую «устранение хвостовых вызовов». Это означает, что когда компилятор видит, что последним действием кода является сам вызов (и возвращает результат, если функция не является пустым), он использует кадр стека текущего вызова вместо установки нового и вместо этого выполняет «переход» "вызова" (поэтому используемое пространство стека остается постоянным). Короче говоря, до 90% пути превращения хвостовой рекурсии в итерацию. Он может справиться с этими миллиардами целых без переполнения стека. (В конце концов, все равно не хватило бы памяти, но при составлении списка из миллиарда целых в любом случае это может испортить вам память в 32-битной системе.)
В большинстве случаев Java этого не делает. (Это зависит от компилятора и времени выполнения, но реализация Oracle этого не делает.) Каждый вызов рекурсивной функции пожирает объем памяти стека. Используйте слишком много, и вы получите переполнение стека. Переполнение стека почти гарантирует смерть программы. Поэтому мы должны быть уверены, что не будем этого делать.
Один полуобход ... ленивая оценка. У нас все еще есть ограничения стека, но они могут быть связаны с факторами, которые мы можем контролировать больше. Нам не нужно рассчитывать миллион целых, чтобы вернуть 25. :)
Итак, давайте создадим нам инфраструктуру для ленивых вычислений. (Этот код был протестирован некоторое время назад, но с тех пор я немного его изменил; прочитайте идею, а не синтаксические ошибки. :))
(Имейте в виду, что если бы это было действительно жизнеспособно в Java, код, по крайней мере, в некоторой степени похожий на приведенный выше, уже был бы частью API.)
Теперь, имея инфраструктуру, довольно просто писать код, который не требует изменяемых переменных и, по крайней мере, стабилен при меньших объемах ввода.
Это в основном работает, но все еще несколько подвержено переполнению стека. Попробуйте
take
2 миллиарда целых и сделайте с ними какие-то действия. : P Это в конечном итоге вызовет исключение, по крайней мере, до тех пор, пока 64+ ГБ ОЗУ не станут стандартными. Проблема в том, что объем памяти программы, который зарезервирован для ее стека, не так велик. Обычно это от 1 до 8 МиБ. (Вы можете попросить больше, но это не имеет значения , все , что много , сколько вы просите - вы звонитеtake(1000000000, someInfiniteSequence)
, вы будете получать исключение.) Контроля К счастью, с ленивыми вычислениями, слабое место в области , мы можем лучше , Мы просто должны быть осторожны с тем, сколько мыtake()
.У него все еще будет много проблем с масштабированием, потому что использование нашего стека увеличивается линейно. Каждый вызов обрабатывает один элемент, а остальные передает другому вызову. Теперь, когда я думаю об этом, есть один трюк, который мы можем применить, который может получить нам немного больше запаса: превратить цепочку вызовов в дерево вызовов. Рассмотрим что-то более похожее на это:
workWith
в основном разбивает работу на две половины и назначает каждую половину другому вызову для себя. Поскольку каждый вызов уменьшает размер рабочего списка вдвое, а не на единицу, он должен масштабироваться логарифмически, а не линейно.Проблема в том, что эта функция хочет ввода - и со связанным списком, получение длины требует обхода всего списка. Это легко решается, хотя; просто не важно сколько там записей. :) Приведенный выше код будет работать с чем-то вроде
Integer.MAX_VALUE
счетчика, так как null в любом случае останавливает обработку. Количество в основном там, поэтому у нас есть солидный базовый случай. Если вы ожидаете, чтоInteger.MAX_VALUE
в списке будет больше записей, вы можете проверитьworkWith
возвращаемое значение - в конце оно должно быть нулевым. В противном случае, рекурсировать.Имейте в виду, это затрагивает столько элементов, сколько вы говорите. Это не лень; это делает свое дело немедленно. Вы хотите делать это только для действий - то есть для вещей, единственная цель которых - применить себя к каждому элементу в списке. Поскольку я обдумываю это прямо сейчас, мне кажется, что последовательности были бы намного менее сложными, если бы они были линейными; не должно быть проблемой, так как последовательности в любом случае не вызывают себя - они просто создают объекты, которые вызывают их снова.
источник
Ранее я пытался создать интерпретатор для lisp-подобного языка в Java (несколько лет назад и весь код был потерян, как это было в CVS в sourceforge), и итераторы утилит Java немного многословны для функционального программирования. в списках.
Вот что-то, основанное на интерфейсе последовательности, в котором есть только две операции, необходимые для получения текущего значения и получения последовательности, начиная со следующего элемента. Они названы головой и хвостом после функций в схеме.
Важно использовать что-то вроде интерфейсов
Seq
or, такIterator
как это означает, что список создается лениво.Iterator
Интерфейс не может быть непреложным объектом, поэтому меньше подходит для функционального программирования - если вы не можете сказать , если значение вы передаете в функцию было изменены ею, вы теряете один из ключевых преимуществ функционального программирования.Очевидно,
integers
должен быть список всех целых чисел, поэтому я начал с нуля и поочередно возвращал положительные и отрицательные.Существует две версии квадратов - одна создает собственную последовательность, другая использует
map
функцию, которая принимает «функцию» - Java 7 не имеет лямбда-выражений, поэтому я использовал интерфейс - и применяю ее к каждому элементу последовательности по очереди.Смысл
square ( int x )
функции состоит только в том, чтобы устранить необходимость вызоваhead()
дважды - обычно я сделал бы это, поместив значение в конечную переменную, но добавление этой функции означает, что в программе нет переменных, только параметры функции.Многословие Java для такого рода программирования побудило меня написать вторую версию моего интерпретатора вместо C99.
источник
Теоретически вы можете реализовать практически все в Java, используя только рекурсию и не изменяемые переменные.
На практике:
Язык Java не предназначен для этого. Многие конструкции предназначены для мутации, и их трудно использовать без нее. (Например, вы не можете инициализировать массив Java переменной длины без мутации.)
То же самое для библиотек. И если вы ограничиваете себя библиотечными классами, которые не используют мутации под прикрытием, это еще сложнее. (Вы даже не можете использовать String ... посмотрите, как
hashcode
это реализовано.)Основные реализации Java не поддерживают оптимизацию хвостового вызова. Это означает, что рекурсивные версии алгоритмов имеют тенденцию быть «голодными» в пространстве стека. А поскольку стеки Java-потоков не растут, вам нужно предварительно выделить большие стеки ... или риск
StackOverflowError
.Объедините эти три вещи, и Java на самом деле не является жизнеспособным вариантом для написания полезных (то есть нетривиальных) программ без изменяемых переменных.
(Но, эй, все в порядке. Для JVM доступны другие языки программирования, некоторые из которых поддерживают функциональное программирование.)
источник
Поскольку мы ищем пример концепций, я бы сказал, что давайте отложим Java и поищем другой, но знакомый параметр, в котором можно найти знакомую версию концепций. Каналы UNIX очень похожи на цепочку ленивых функций.
В Linux это означает, что дайте мне байты, каждый из которых состоит из ложных, а не истинных битов, пока я не потеряю свой аппетит; измените каждый из этих байтов на символ новой строки; нумерация каждой строки, созданной таким образом; и произвести квадрат этого числа. Кроме того, у меня аппетит на 25 строк и не более.
Я утверждаю, что программисту не советуют так писать конвейер Linux. Это относительно нормальные сценарии оболочки Linux.
Я утверждаю, что программисту не рекомендуется пытаться писать то же самое аналогично в Java. Причина заключается в обслуживании программного обеспечения как основного фактора стоимости программных проектов в течение всей жизни. Мы не хотим вводить в заблуждение следующего программиста, представляя то, что якобы является программой Java, но на самом деле написано на собственном одноразовом языке, продуманно дублируя функциональность, которая уже существует на платформе Java.
С другой стороны, я утверждаю, что следующий программист мог бы быть более восприимчивым, если некоторые из наших пакетов «Java» на самом деле являются пакетами виртуальной машины Java, написанными на одном из функциональных или объектно-функциональных языков, таких как Clojure и Scala. Они предназначены для кодирования путем объединения функций и вызова из Java обычным способом вызовов методов Java.
С другой стороны, для программиста на Java все еще может быть хорошая идея черпать вдохновение из функционального программирования в некоторых местах.
Недавно мой любимый метод [заключался в том, чтобы] использовать неизменяемую неинициализированную возвращаемую переменную и один выход, чтобы, как это делают некоторые компиляторы функционального языка, 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;
.Самый простой способ выяснить это - передать следующее компилятору Frege и посмотреть на сгенерированный код Java:
источник