В функциональном программировании все еще считаются «плохой практикой» локальные изменяемые переменные без побочных эффектов?

23

Имеет ли изменяемые локальные переменные в функции, которые используются только для внутренних целей (например, функция не имеет побочных эффектов, по крайней мере, намеренно), все еще считается «не функциональным»?

например, в проверке стиля курса «Функциональное программирование с помощью Scala» любое varиспользование считается плохим

Мой вопрос, если функция не имеет побочных эффектов, все еще не рекомендуется писать код императивного стиля?

например, вместо использования хвостовой рекурсии с шаблоном аккумулятора, что плохого в том, чтобы делать локальный цикл for и создавать локальный изменяемый объект ListBufferи добавлять к нему, пока ввод не изменяется?

Если ответ «да, они всегда обескураживают, даже если нет побочных эффектов», тогда в чем причина?

Эран Медан
источник
3
Все советы, наставления и т. Д. По теме, которую я когда-либо слышал, относятся к общему изменяемому состоянию как источнику сложности. Этот курс предназначен только для начинающих? Тогда это, вероятно, хорошо продуманное намеренное упрощение.
Килиан Фот
3
@KilianFoth: Совместно изменяемое состояние является проблемой в многопоточных контекстах, но не разделяемое изменяемое состояние может привести к тому, что программы также будут трудно рассуждать.
Майкл Шоу
1
Я думаю, что использование локальной изменяемой переменной не обязательно является плохой практикой, но это не «функциональный стиль»: я думаю, что цель курса Scala (который я проходил осенью прошлого года) - научить вас программированию в функциональном стиле. Как только вы сможете четко различать функциональный и императивный стиль, вы можете решить, когда использовать какой (в случае, если ваш язык программирования позволяет оба варианта). varвсегда не работает. Scala имеет ленивые vals и оптимизацию хвостовой рекурсии, которые позволяют полностью избежать vars.
Джорджио

Ответы:

17

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

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

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

CA Макканн
источник
16

Проблема не в изменчивости как таковой, а в отсутствии ссылочной прозрачности.

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

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

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

Майкл Шоу
источник
4
Ваше замечание о запоминании очень хорошо. Обратите внимание, что Haskell настоятельно подчеркивает ссылочную прозрачность для программирования, но ленивое вычисление, подобное запоминанию, включает в себя ошеломляющее количество мутаций, выполняемых языковой средой исполнения за кулисами.
CA McCann
@CA McCann: Я думаю, что то, что вы говорите, очень важно: в функциональном языке среда выполнения может использовать мутацию для оптимизации вычислений, но в языке нет конструкции, позволяющей программисту использовать мутацию. Другой пример - цикл while с переменной цикла: в Haskell вы можете написать хвостовую рекурсивную функцию, которая может быть реализована с изменяемой переменной (чтобы избежать использования стека), но то, что видит программист, это неизменяемые аргументы функции, которые передаются из одного звоните к следующему.
Джорджио
@ Майкл Шоу: +1 за «Проблема не в изменчивости как таковой, а в отсутствии ссылочной прозрачности». Возможно, вы можете сослаться на чистый язык, на котором у вас есть типы уникальности: они допускают изменчивость, но все же гарантируют прозрачность ссылок.
Джорджио
@ Джорджио: Я действительно ничего не знаю о Чистой, хотя я слышал, что это упоминалось время от времени. Может быть, я должен посмотреть на это.
Майкл Шоу
@ Майкл Шоу: я не очень много знаю о Clean, но я знаю, что он использует уникальные типы для обеспечения ссылочной прозрачности. По сути, вы можете изменить объект данных при условии, что после модификации у вас нет ссылок на старое значение. ИМО, это иллюстрирует вашу точку зрения: ссылочная прозрачность является наиболее важным моментом, а неизменность является лишь одним из возможных способов ее обеспечения.
Джорджио
8

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

Что касается перспективы, я вполне уверен, что CanBuildFromпочти все неизменяемые структуры, предоставляемые scala, делают какие-то мутации внутри себя. Дело в том, что то, что они выставляют, является неизменным. Сохранение максимально возможного количества значений помогает программе легче рассуждать и меньше подвержено ошибкам .

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

Имея это в виду, многие проблемы, которые обычно требуют изменяемых переменных (таких как циклы), могут быть лучше решены с помощью множества функций более высокого порядка, которые предоставляют языки, такие как Scala (map / filter / fold). Будьте в курсе тех.

KChaloux
источник
2
Да, мне почти никогда не нужен цикл for при использовании коллекций Scala. map, filter, foldLeftИ forEach сделать трюк большую часть времени, но когда они не делают, будучи в состоянии чувствовать , что я «OK» , чтобы возвращаться к грубой силы императивного кода хорошо. (до тех пор, пока нет никаких побочных эффектов, конечно)
Эран Медан
3

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

У вас также есть гораздо больше возможностей для функциональной обработки контейнеров, чем с императивными циклами. С императивом у вас, в основном, есть for, whileи незначительные вариации на эти два типа do...whileи foreach.

В функционале у вас есть агрегат, счетчик, фильтр, поиск, flatMap, свертывание, groupBy, lastIndexWhere, map, maxBy, minBy, разделение, сканирование, sortBy, sortWith, span и takeWhile, просто чтобы назвать несколько более распространенных из Scala стандартная библиотека. Когда вы привыкнете к тому, что они доступны, императивные forпетли кажутся слишком простыми в сравнении.

Единственная реальная причина использовать локальную изменчивость очень редко для производительности.

Карл Билефельдт
источник
2

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

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

Как говорится в ссылке:

Если дерево падает в лесу, издает ли оно звук? Если чистая функция изменяет некоторые локальные данные для получения неизменяемого возвращаемого значения, это нормально?

Саймон Бергот
источник
2

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

Я был сожжен такой локальной переменной (не в моем коде, и при этом у меня не было источника), вызывающей повреждение данных с низкой вероятностью. Безопасность потоков не упоминалась так или иначе, не было состояния, которое сохранялось между вызовами, и не было никаких побочных эффектов. Мне не пришло в голову, что это не может быть потокобезопасным, погоня за случайным повреждением данных 1 к 100 000 - это королевская боль.

Лорен Печтель
источник