Понимание чистых функций и побочных эффектов в Haskell - putStrLn

10

Недавно я начал изучать 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();
    }
}

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

РЕДАКТИРОВАТЬ:

Спасибо всем за ваши ответы! Ваши ответы помогли мне лучше понять эти понятия. Я не думаю, что это полностью щелкнуло еще, но я вернусь к теме в будущем, спасибо!

Fluous
источник
2
Думайте о том, helloWorldчтобы быть константой, такой как поле или переменная в C #. Там нет параметра, который применяется к helloWorld.
Карамириэль
2
putStrLn не имеет побочных эффектов; он просто возвращает действие ввода-вывода, то же действие ввода-вывода для аргумента, "Hello World"независимо от того, сколько раз вы вызываете putStrLn.
Chepner
1
Если бы это произошло, helloworldне было бы действия, которое печатает Hello world; это будет значение, возвращаемое putStrLn после его печати Hello World(а именно ()).
chepner
2
Я думаю, чтобы понять этот пример, вы уже должны понимать, как побочные эффекты работают в Haskell. Это не хороший пример.
user253751
В вашем C # фрагменте вам не нравится helloWorld = Console.WriteLine("Hello World");. Вы просто содержите Console.WriteLine("Hello World");в HelloWorldфункции, которая будет выполняться каждый раз, когда HelloWorldвызывается. Теперь подумайте, что helloWorld = putStrLn "Hello World"делает helloWorld. Он присваивается монаде IO, которая содержит (). Как только вы свяжете его, >>=он только тогда выполнит свою деятельность (что-то напечатает) и даст вам ()правую часть оператора связывания.
Redu

Ответы:

8

Вероятно, было бы легче понять, что имеет в виду автор, если бы мы определили его helloWorldкак локальную переменную:

main :: IO ()
main = do
  let helloWorld = putStrLn "Hello World!"
  helloWorld
  helloWorld
  helloWorld

который вы можете сравнить с этим C # -подобным псевдокодом:

void Main() {
  var helloWorld = {
    WriteLine("Hello World!")
  }
  helloWorld;
  helloWorld;
  helloWorld;
}

Т.е. в C # WriteLineэто процедура, которая печатает свой аргумент и ничего не возвращает. В Haskell putStrLn- это функция, которая принимает строку и дает вам действие, которое будет печатать эту строку, если она будет выполнена. Это означает, что нет абсолютно никакой разницы между написанием

do
  let hello = putStrLn "Hello World"
  hello
  hello

а также

do
  putStrLn "Hello World"
  putStrLn "Hello World"

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

это работает немного лучше, если сравнить его с питоном

hello_world = print('hello world')
hello_world
hello_world
hello_world

Дело в том , что IO действия в Haskell являются «реальные» значения , которые не должны быть завернуты в дальнейшем «обратных вызовов» или что - нибудь в этом роде , чтобы предотвратить их от выполнения - вернее, единственный способ сделать их получить запустить , поместить их в определенном месте (то есть где-нибудь внутри mainили порожденная нить main).

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

кубический
источник
4

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

add :: Int -> Int -> IO Int
add x y = do
  putStrLn ("I am adding " ++ show x ++ " and " ++ show y)
  return (x + y)

plus23 :: IO Int
plus23 = add 2 3

main :: IO ()
main = do
  _ <- plus23
  _ <- plus23
  _ <- plus23
  return ()

Это напечатает «Я добавляю 2 и 3» 3 раза.

В C # вы можете написать следующее:

using System;

public class Program
{
    public static int add(int x, int y)
    {
        Console.WriteLine("I am adding {0} and {1}", x, y);
        return x + y;
    }

    public static void Main()
    {
        int x;
        int plus23 = add(2, 3);
        x = plus23;
        x = plus23;
        x = plus23;
        return;
    }
}

Который будет печатать только один раз.

oisdk
источник
3

Если при оценке putStrLn "Hello World"возникли побочные эффекты, то сообщение будет напечатано только один раз.

Мы можем приблизить этот сценарий с помощью следующего кода:

import System.IO.Unsafe (unsafePerformIO)
import Control.Exception (evaluate)

helloWorld :: ()
helloWorld = unsafePerformIO $ putStrLn "Hello World"

main :: IO ()
main = do
    evaluate helloWorld
    evaluate helloWorld
    evaluate helloWorld

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

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

Этот код печатает «Hello World» только один раз. Мы рассматриваем helloWorldкак чистую ценность. Но это означает, что он будет распределен между всеми evaluate helloWorldвызовами. И почему бы нет? В конце концов, это чистая ценность, зачем ее пересчитывать без необходимости? Первое evaluateдействие «выталкивает» «скрытый» эффект, а последующие действия просто оценивают результат (), который не вызывает никаких дополнительных эффектов.

danidiaz
источник
1
Стоит отметить, что вы абсолютно не должны использовать unsafePerformIOна данном этапе изучения Haskell. У него есть «небезопасный» в названии по причине, и вы не должны использовать его, если вы не можете (и не сделали) тщательно рассмотреть последствия его использования в контексте. Код, который danidiaz вставил в ответ, прекрасно отражает неинтуитивное поведение, которое может возникнуть в результате unsafePerformIO.
Эндрю Рэй
1

Обратите внимание на одну деталь: вы вызываете putStrLnфункцию только один раз при определении helloWorld. В mainфункции вы просто используете возвращаемое значение этого putStrLn "Hello, World"три раза.

Лектор говорит, что у putStrLnзвонка нет побочных эффектов, и это правда. Но посмотрите на тип helloWorld- это действие ввода-вывода. putStrLnпросто создает это для вас. Позже вы соедините 3 из них с doблоком, чтобы создать другое действие ввода-вывода - main. Позже, когда вы запустите свою программу, это действие будет выполнено, вот где лежат побочные эффекты.

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

Юрий Коваленко
источник