Допустим, у меня есть логика ниже. Как написать это в функциональном программировании?
public int doSomeCalc(int[] array)
{
int answer = 0;
if(array!=null)
{
for(int e: array)
{
answer += e;
if(answer == 10) break;
if(answer == 150) answer += 100;
}
}
return answer;
}
Примеры в большинстве блогов, статей ... Я вижу, просто объясняет простой случай одной прямой математической функции, скажем, "Сумма". Но у меня есть логика, аналогичная вышеописанной на Java, и я хотел бы перенести ее в функциональный код в Clojure. Если мы не можем сделать это в FP, то в промо-акциях для FP это прямо не указано.
Я знаю, что приведенный выше кодекс абсолютно необходим. Он не был написан с мыслью о переносе его в FP в будущем.
break
иreturn answer
может быть замененаreturn
внутри цикла. В FP вы можете реализовать это раннее возвращение, используя продолжения, см., Например, en.wikipedia.org/wiki/ContinuationtakeWhile
.Ответы:
Ближайшим эквивалентом зацикливания массива в большинстве функциональных языков является
fold
функция, то есть функция, которая вызывает пользовательскую функцию для каждого значения массива, передавая накопленное значение по цепочке. Во многих функциональных языкахfold
дополнен множеством дополнительных функций, которые предоставляют дополнительные функции, в том числе возможность досрочной остановки при возникновении некоторых условий. В ленивых языках (например, Haskell) досрочная остановка может быть достигнута просто без дальнейшей оценки по списку, что приведет к тому, что дополнительные значения никогда не будут генерироваться. Поэтому, переводя ваш пример на Haskell, я бы написал так:Разбивая это построчно, если вы не знакомы с синтаксисом Haskell, это работает так:
Определяет тип функции, принимает список целых и возвращает одно целое.
Основная часть функции: заданный аргумент
values
, returnfoldr1
с аргументамиcombine
(которые мы определим ниже) иvalues
.foldr1
является вариантом примитива сгиба, который начинается с того, что аккумулятор установлен на первое значение списка (отсюда и1
в имени функции), а затем объединяет его, используя функцию, заданную пользователем слева направо (которая обычно называется правым сгибом , следовательноr
в названии функции). Такfoldr1 f [1,2,3]
эквивалентноf 1 (f 2 3)
(илиf(1,f(2,3))
в более обычном C-подобном синтаксисе).Определение
combine
локальной функции: она получает два аргументаv1
иv2
. Когдаv1
10, он просто возвращаетсяv1
. В этом случае v2 никогда не оценивается , поэтому цикл останавливается здесь.Кроме того, когда v1 равен 150, добавляет к нему дополнительные 100 и добавляет v2.
И, если ни одно из этих условий не выполняется, просто добавляет v1 к v2.
Теперь, это решение несколько специфично для Haskell, потому что тот факт, что правильное сгибание заканчивается, если функция объединения не оценивает свой второй аргумент, вызвано ленивой стратегией оценки Haskell. Я не знаю Clojure, но я полагаю, что он использует строгую оценку, поэтому я ожидаю, что
fold
в его стандартной библиотеке будет функция, включающая специальную поддержку для досрочного завершения. Это часто называютfoldWhile
,foldUntil
или аналогичный.Беглый взгляд на документацию библиотеки Clojure показывает, что она немного отличается от большинства функциональных языков в именовании, и это
fold
не то, что вам нужно (это более продвинутый механизм, предназначенный для обеспечения параллельных вычислений), ноreduce
оно более прямое. эквивалент. Досрочное прекращение происходит, еслиreduced
функция вызывается в вашей функции объединения. Я не уверен на 100%, что понимаю синтаксис, но подозреваю, что вы ищете что-то вроде этого:NB: оба перевода, Haskell и Clojure, не совсем подходят для этого конкретного кода; но они передают общую суть этого - см. обсуждение в комментариях ниже для конкретных проблем с этими примерами.
источник
v1 v2
сбивают с толку:v1
это «значение из массива», ноv2
это накопленный результат. и ваш перевод ошибочен, я полагаю, цикл OP завершается, когда накопленное (слева) значение достигает 10, а не некоторый элемент в массиве. То же самое с приращением на 100. Если использовать складки здесь, используйте левую складку с ранним выходом, некоторые вариации наfoldlWhile
здесь .(= v1 150)
использования значения передv2
(aka.e
) Суммируется с ним.Breaking this down line by line in case you're not familiar with Haskell's syntax
-- Ты мой герой. Хаскель для меня загадка.Вы можете легко преобразовать это в рекурсию. И у него есть хороший рекурсивный вызов с оптимизированным хвостом.
Псевдокод:
источник
GOTO
. (Не совсем так плохо, но все же довольно неловко.) Эквивалент петли, как говорит Жюль, является подходящим сгибом.GOTO
. В любом случае, когда вы компилируете хвостовую рекурсию, она в основном сводится кwhile (true)
циклу с телом функции, где ранний возврат - это простоbreak
оператор. Сгиб, хотя вы правы в том, что он является циклом, на самом деле более ограничен, чем общая конструкция цикла; это больше похоже на цикл для каждогоGOTO
заключается в том, что вам необходимо тщательно вести учет того, какие аргументы в каком состоянии передаются рекурсивному вызову, чтобы убедиться, что он действительно ведет себя так, как задумано. В этом нет необходимости в той же степени в прилично написанных императивных циклах (где довольно ясно, что такое переменные с состоянием и как они изменяются в каждой итерации), а также в наивной рекурсии (где обычно мало что делается с аргументами, а вместо этого результат собран довольно интуитивно понятным способом). ...Мне очень нравится ответ Жюля , но я хотел бы дополнительно отметить кое-что, что люди часто упускают из-за ленивого функционального программирования, а именно, что все не должно быть «внутри цикла». Например:
Вы можете видеть, что каждая часть вашей логики может быть вычислена в отдельной функции, а затем составлена вместе. Это учитывает меньшие функции, которые обычно намного легче устранить неисправности. Для вашего игрушечного примера, возможно, это добавляет больше сложности, чем удаляет, но в коде реального мира разделенные функции часто намного проще, чем целое.
источник
stopAt10
это не хороший потребитель. Ваш ответ это лучше , чем тот , который вы процитировать в том , что он правильно изолирует основной производительscanl (+) 0
значений. их потребление должно включать управляющую логику напрямую, хотя лучше реализовать только с двумяspan
s и alast
, явно. это также будет следовать исходной структуре кода и логике, и будет легко поддерживать.Большинство примеров обработки списка вы будете видеть функции использования , как
map
,filter
,sum
т. Д., Которые работают со списком в целом. Но в вашем случае у вас есть условный досрочный выход - довольно необычный шаблон, который не поддерживается обычными операциями со списком. Таким образом, вам нужно сбросить уровень абстракции и использовать рекурсию, что также ближе к тому, как выглядит императивный пример.Это довольно прямой (вероятно, не идиоматический) перевод на Clojure:
Edit: Жюль отмечают, что
reduce
в Clojure сделать поддержку раннего выхода. Используя это более элегантно:В любом случае вы можете делать что-либо на функциональных языках, как на императивных языках, но вам часто приходится несколько менять свое мышление, чтобы найти элегантное решение. В императивном кодировании вы думаете об обработке списка шаг за шагом, в то время как в функциональных языках вы ищете операцию, которую можно применить ко всем элементам в списке.
источник
reduce
операция Clojure поддерживает ранний выход.takeWhile
не «обычная операция»?takeWhile
это обычная операция, она не особенно полезна в этом случае, потому что вам нужны результаты вашего преобразования, прежде чем вы решите, стоит ли останавливаться. На ленивом языке это не имеет значения: вы можете использоватьscan
иtakeWhile
результаты сканирования (см. Ответ Карла Билефельда, который, хотя он и не используется,takeWhile
можно легко переписать для этого), но для строгого языка, такого как clojure, это будет означает обработку всего списка, а затем отбрасывание результатов. Однако функции генератора могут решить эту проблему, и я считаю, что clojure их поддерживает.take-while
в Clojure производит ленивую последовательность (согласно документам). Другой способ справиться с этим - использовать преобразователи (возможно, лучший).Как указано в других ответах, Clojure предлагает
reduced
прекратить сокращение на ранних этапах:Это лучшее решение для вашей конкретной ситуации. Вы также можете получить много пробег от объединения
reduced
сtransduce
, что позволяет использовать датчики сmap
, иfilter
т.д. Однако это далеко не полный ответ на ваш общий вопрос.Продолжения Escape являются обобщенной версией операторов break и return. Они напрямую реализованы в некоторых Schemes (
call-with-escape-continuation
), Common Lisp (block
+return
,catch
+throw
) и даже C (setjmp
+longjmp
). Более общие или неограниченные продолжения, определенные в стандартной Схеме или в виде монад продолжения в Хаскеле и Скале, также могут использоваться в качестве продолжения эвакуации.Например, в Racket вы можете использовать
let/ec
так:Многие другие языки также имеют экранирующие продолжения конструкции в форме обработки исключений. В Haskell вы также можете использовать одну из различных монад ошибок с
foldM
. Поскольку они в первую очередь являются конструкциями обработки ошибок, использующими исключения или монады ошибок для раннего возврата, обычно культурно неприемлемы и, возможно, довольно медленны.Вы также можете перейти от функций высшего порядка к хвостовым вызовам.
При использовании циклов вы автоматически вводите следующую итерацию, когда достигаете конца тела цикла. Вы можете ввести следующую итерацию как можно раньше
continue
или выйти из цикла с помощьюbreak
(илиreturn
). При использовании хвостовых вызовов (илиloop
конструкции Clojure, которая имитирует хвостовую рекурсию), вы всегда должны делать явный вызов для входа в следующую итерацию. Чтобы прекратить зацикливание, вы просто не делаете рекурсивный вызов, а передаете значение напрямую:источник
MonadError
, в основном эквивалентEither
не имеет такого смещения в сторону только обработки ошибок, поэтому может легко использоваться в качестве замены.Запутанная часть - петля. Давайте начнем с этого. Цикл обычно преобразуется в функциональный стиль, выражая итерацию одной функцией. Итерация - это преобразование переменной цикла.
Вот функциональная реализация общего цикла:
Он принимает (начальное значение переменной цикла, функция, которая выражает одну итерацию [в переменной цикла]) (условие для продолжения цикла).
Ваш пример использует цикл в массиве, который также разрывается. Эта возможность на вашем императивном языке встроена в сам язык. В функциональном программировании такая возможность обычно реализуется на уровне библиотеки. Вот возможная реализация
В этом :
Я использую пару ((val, next_pos)), которая содержит видимую снаружи переменную цикла и позицию в массиве, которую скрывает эта функция.
Функция итерации немного сложнее, чем в общем цикле, эта версия позволяет использовать текущий элемент массива. [Это в карри .]
Такие функции обычно называют «сложить».
Я вставил «l» в названии, чтобы указать, что накопление элементов массива осуществляется левоассоциативным образом; имитировать привычку императивных языков программирования перебирать массив от низкого до высокого индекса.
Я вставил «c» в названии, чтобы указать, что эта версия сгиба принимает условие, которое контролирует, когда и когда цикл должен быть остановлен досрочно.
Конечно, такие вспомогательные функции, вероятно, будут легко доступны в базовой библиотеке, поставляемой с используемым функциональным языком программирования. Я написал их здесь для демонстрации.
Теперь, когда у нас есть все инструменты, которые есть в языке в императивном случае, мы можем обратиться к реализации конкретной функциональности вашего примера.
Переменная в вашем цикле - это пара ('answer', логическое значение, которое кодирует, продолжать ли).
Обратите внимание, что я использовал новую переменную new_answer. Это связано с тем, что в функциональном программировании я не могу изменить значение уже инициализированной «переменной». Я не беспокоюсь о производительности, компилятор может повторно использовать память 'answer' для 'new_answer' с помощью анализа времени жизни, если он считает, что это более эффективно.
Включение этого в нашу функцию цикла, разработанную ранее:
«Массив» - это имя модуля, в котором экспортируется функция foldlc.
«кулак», «второй» означают функции, которые возвращают первый, второй компонент своего параметра пары
В этом случае стиль «без точек» повышает удобочитаемость реализации doSomeCalc:
(>>>) - это композиция функций:
(>>>) : (a -> b) -> (b -> c) -> (a -> c)
Это то же самое, что и выше, только параметр "arr" не учитывается с обеих сторон определяющего уравнения.
И последнее: проверка регистра (массив == ноль). В лучше разработанных языках программирования, но даже в плохо разработанных языках с некоторой базовой дисциплиной, один использует дополнительный тип для выражения несуществования. Это не имеет ничего общего с функциональным программированием, о котором в конечном итоге и идет речь, поэтому я не занимаюсь этим.
источник
Во-первых, немного переписать цикл так, чтобы каждая итерация цикла либо рано выходила, либо изменялась
answer
ровно один раз:Должно быть ясно, что поведение этой версии точно такое же, как и раньше, но теперь гораздо проще перейти к рекурсивному стилю. Вот прямой перевод на Haskell:
Теперь он чисто функциональный, но мы можем улучшить его как с точки зрения эффективности, так и читабельности, используя сгиб вместо явной рекурсии:
В этом контексте
Left
ранний выход с его значением иRight
продолжение рекурсии с его значением.Теперь это можно упростить немного больше, например так:
Это лучше, чем финальный код на Haskell, но теперь немного менее понятно, как он отображается на исходную Java.
источник