Недавно я начал изучать Haskell, потому что хотел расширить свои знания по функциональному программированию, и я должен сказать, что я действительно люблю это до сих пор. В настоящее время я использую ресурс «Основы Haskell, часть 1» по Pluralsight. К сожалению, мне сложно понять одну конкретную цитату лектора о следующем коде, и я надеялся, что вы, ребята, сможете пролить свет на эту тему.
Сопровождающий код
helloWorld :: IO ()
helloWorld = putStrLn "Hello World"
main :: IO ()
main = do
helloWorld
helloWorld
helloWorld
Цитата
Если в do-блоке выполняется одно и то же действие ввода-вывода несколько раз, оно будет выполняться несколько раз. Таким образом, эта программа печатает строку «Hello World» три раза. Этот пример помогает проиллюстрировать, что putStrLn
это не функция с побочными эффектами. Мы вызываем putStrLn
функцию один раз, чтобы определить helloWorld
переменную. Если putStrLn
бы был побочный эффект печати строки, он напечатал бы только один раз, и helloWorld
переменная, повторенная в основном блоке do, не имела бы никакого эффекта.
В большинстве других языков программирования такая программа выводит «Hello World» только один раз, поскольку печать происходит при putStrLn
вызове функции. Это тонкое различие часто сбивает с толку новичков, поэтому подумайте об этом немного и убедитесь, что вы понимаете, почему эта программа печатает «Hello World» три раза и почему она печатает его только один раз, если putStrLn
функция выполняла печать как побочный эффект.
Что я не понимаю
Для меня кажется почти естественным, что строка «Hello World» печатается три раза. Я воспринимаю helloWorld
переменную (или функцию?) Как своего рода обратный вызов, который вызывается позже. Что я не понимаю, так это то, что если бы у putStrLn
него был побочный эффект, это привело бы к печати строки только один раз. Или почему он будет напечатан только один раз на других языках программирования.
Скажем, в коде C # я бы предположил, что это будет выглядеть так:
C # (скрипка)
using System;
public class Program
{
public static void HelloWorld()
{
Console.WriteLine("Hello World");
}
public static void Main()
{
HelloWorld();
HelloWorld();
HelloWorld();
}
}
Я уверен, что пропускаю что-то довольно простое или неправильно истолковываю его терминологию. Любая помощь будет принята с благодарностью.
РЕДАКТИРОВАТЬ:
Спасибо всем за ваши ответы! Ваши ответы помогли мне лучше понять эти понятия. Я не думаю, что это полностью щелкнуло еще, но я вернусь к теме в будущем, спасибо!
helloWorld
чтобы быть константой, такой как поле или переменная в C #. Там нет параметра, который применяется кhelloWorld
.putStrLn
не имеет побочных эффектов; он просто возвращает действие ввода-вывода, то же действие ввода-вывода для аргумента,"Hello World"
независимо от того, сколько раз вы вызываетеputStrLn
.helloworld
не было бы действия, которое печатаетHello world
; это будет значение, возвращаемоеputStrLn
после его печатиHello World
(а именно()
).helloWorld = Console.WriteLine("Hello World");
. Вы просто содержитеConsole.WriteLine("Hello World");
вHelloWorld
функции, которая будет выполняться каждый раз, когдаHelloWorld
вызывается. Теперь подумайте, чтоhelloWorld = putStrLn "Hello World"
делаетhelloWorld
. Он присваивается монаде IO, которая содержит()
. Как только вы свяжете его,>>=
он только тогда выполнит свою деятельность (что-то напечатает) и даст вам()
правую часть оператора связывания.Ответы:
Вероятно, было бы легче понять, что имеет в виду автор, если бы мы определили его
helloWorld
как локальную переменную:который вы можете сравнить с этим C # -подобным псевдокодом:
Т.е. в C #
WriteLine
это процедура, которая печатает свой аргумент и ничего не возвращает. В HaskellputStrLn
- это функция, которая принимает строку и дает вам действие, которое будет печатать эту строку, если она будет выполнена. Это означает, что нет абсолютно никакой разницы между написаниема также
Тем не менее, в этом примере разница не особенно значительна, поэтому хорошо, если вы не совсем понимаете, чего пытается достичь автор в этом разделе, и просто идете дальше.
это работает немного лучше, если сравнить его с питоном
Дело в том , что IO действия в Haskell являются «реальные» значения , которые не должны быть завернуты в дальнейшем «обратных вызовов» или что - нибудь в этом роде , чтобы предотвратить их от выполнения - вернее, единственный способ сделать их получить запустить , поместить их в определенном месте (то есть где-нибудь внутри
main
или порожденная нитьmain
).Это не просто уловка, но в конечном итоге это приводит к некоторым интересным эффектам написания кода (например, это одна из причин того, почему Haskell на самом деле не нужна какая-либо из общих структур управления, которые вам знакомы с императивными языками и может сойти с рук, делая все с точки зрения функций вместо этого), но, опять же, я бы не слишком беспокоился об этом (подобные аналогии не всегда сразу щелкают)
источник
Может быть легче увидеть разницу, как описано, если вы используете функцию, которая на самом деле что-то делает, а не
helloWorld
. Подумайте о следующем:Это напечатает «Я добавляю 2 и 3» 3 раза.
В C # вы можете написать следующее:
Который будет печатать только один раз.
источник
Если при оценке
putStrLn "Hello World"
возникли побочные эффекты, то сообщение будет напечатано только один раз.Мы можем приблизить этот сценарий с помощью следующего кода:
unsafePerformIO
принимаетIO
действие и «забывает», что этоIO
действие, освобождая его от обычной последовательности, навязанной композициейIO
действий, и позволяя эффекту иметь место (или нет) в соответствии с капризами ленивой оценки.evaluate
принимает чистое значение и гарантирует, что значение оценивается всякий раз, когда оценивается результирующееIO
действие - что для нас это будет, потому что оно лежит на путиmain
. Мы используем его здесь, чтобы связать оценку некоторых значений с выполнением программы.Этот код печатает «Hello World» только один раз. Мы рассматриваем
helloWorld
как чистую ценность. Но это означает, что он будет распределен между всемиevaluate helloWorld
вызовами. И почему бы нет? В конце концов, это чистая ценность, зачем ее пересчитывать без необходимости? Первоеevaluate
действие «выталкивает» «скрытый» эффект, а последующие действия просто оценивают результат()
, который не вызывает никаких дополнительных эффектов.источник
unsafePerformIO
на данном этапе изучения Haskell. У него есть «небезопасный» в названии по причине, и вы не должны использовать его, если вы не можете (и не сделали) тщательно рассмотреть последствия его использования в контексте. Код, который danidiaz вставил в ответ, прекрасно отражает неинтуитивное поведение, которое может возникнуть в результатеunsafePerformIO
.Обратите внимание на одну деталь: вы вызываете
putStrLn
функцию только один раз при определенииhelloWorld
. Вmain
функции вы просто используете возвращаемое значение этогоputStrLn "Hello, World"
три раза.Лектор говорит, что у
putStrLn
звонка нет побочных эффектов, и это правда. Но посмотрите на типhelloWorld
- это действие ввода-вывода.putStrLn
просто создает это для вас. Позже вы соедините 3 из них сdo
блоком, чтобы создать другое действие ввода-вывода -main
. Позже, когда вы запустите свою программу, это действие будет выполнено, вот где лежат побочные эффекты.Механизм, лежащий в основе этого - монады . Эта мощная концепция позволяет вам использовать некоторые побочные эффекты, такие как печать на языке, который не поддерживает побочные эффекты напрямую. Вы просто соединяете некоторые действия, и эта цепочка запускается при запуске вашей программы. Вам нужно будет глубоко понять эту концепцию, если вы хотите серьезно использовать Haskell.
источник