Смогу ли я развивать хорошие/плохие привычки из-за ленивой оценки?

Я хочу изучить функциональное программирование на Haskell или F #.

Существуют ли какие-либо привычки программирования (хорошие или плохие), которые могут сформироваться в результате ленивой оценки Haskell? Мне нравится идея чистоты функционального программирования на Haskell в целях понимания функционального программирования. Я просто немного беспокоюсь о двух вещах:

  1. Я могу неверно истолковать функции, основанные на ленивой оценке, как часть "функциональной парадигмы".
  2. Я могу разработать шаблоны мышления, которые работают в ленивом мире, но не в нормальном мире порядка/стремления к оценке.

Ответ 1

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

f x y = if x > y then .. a .. b .. else c
  where
    a = expensive
    b = expensive 
    c = expensive

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

Другим примером, который приходит на ум, является "нумерация вещей":

pairs = zip xs [1..]

здесь мы просто хотим связать каждый элемент в списке с его индексом, а zipping с бесконечным списком [1..] - естественный способ сделать это в Haskell. Как вы пишете это без бесконечного списка? Ну, складки не слишком читаемы

pairs = foldr (\x xs -> \n -> (x,n) : xs (n+1)) (const []) xs 1

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

Я уверен, что есть еще много. Лень на удивление полезна, когда вы привыкаете к ней.

Ответ 2

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

Я могу разработать шаблоны мышления, которые работают в ленивом мире, но не в нормальном состоянии/нетерпении мира оценки.

Right. Вы будете более округлым программистом. Абстракции, которые обеспечивают механизмы "задержки", довольно распространены сейчас, поэтому вы были бы хуже программистом, чтобы не знать их.

Ответ 3

  • Я могу неправильно интерпретировать функции, основанные на ленивой оценке, как часть "функциональной парадигмы".

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

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

(Отказ от ответственности: нет опыта Haskell или F #)

Ответ 4

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

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

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

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

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

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

Ответ 5

Я ожидаю вредных привычек.

Я видел, как один из моих коллег попытался использовать (ручную) ленивую оценку в нашем .NET-проекте. К сожалению, последствия ленивой оценки спрятали ошибку, когда она пыталась удалять вызовы до начала основного выполнения и, таким образом, вне try/catch обрабатывала дело "Эй, я не могу подключиться к интернету".

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

Ответ 6

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

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

Зная, что не-ленивые списки на Python могут быть дорогими, если они имеют тенденцию становиться большими, я подумал о следующем закрытом решении Python. Для меня было использовать генератор для шага создания ценности.

Код Python стал намного лучше благодаря моему ленивому (каламбурному) мышлению.

Ответ 7

Хорошо, попробуйте подумать о чем-то, что могло бы работать, если бы лениво оценивалось, что не было бы, если бы он был оценен с нетерпением. Наиболее распространенной категорией их будет ленивая оценка логического оператора, используемая для скрытия "побочного эффекта". Я буду использовать язык С# -ish, чтобы объяснить, но функциональные языки будут иметь аналогичные аналоги.

Возьмите простую С# лямбда:

(a,b) => a==0 || ++b < 20

В ленивом языке, если a == 0, выражение ++ b < 20 не оценивается (поскольку все выражение равно true в любом случае), что означает, что b не увеличивается. Как в императивном, так и в функциональном языках это поведение (и подобное поведение оператора И) может быть использовано для "скрытия" логики, содержащей побочные эффекты, которые не должны выполняться:

(a,b) => a==0 && save(b)

"a" в этом случае может быть числом ошибок проверки. Если были ошибки проверки, первая половина не удалась, а вторая половина не была оценена. Если ошибок проверки не было, будет оценена вторая половина (которая будет включать побочный эффект попытки сохранения b), и результат (по-видимому, true или false) возвращается для оценки. Если любая из сторон оценивает значение false, лямбда возвращает false, указывая, что b не был успешно сохранен. Если бы это было оценено "с нетерпением", мы попытались бы сохранить независимо от значения "a" , что, вероятно, было бы плохо, если бы ненулевое "a" указывало, что мы не должны.

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