Розуміння чистих функцій та побічних ефектів у Haskell - putStrLn


10

Нещодавно я почав вивчати Haskell, тому що хотів розширити свої знання з функціонального програмування, і мушу сказати, що дуже люблю його до цих пір. Зараз я використовую курс "Основи Haskell, частина 1" з питань плюралізму. На жаль, у мене є труднощі з розумінням однієї конкретної цитати лектора про наступний код і сподівався, що ви, хлопці, зможете пролити трохи світла на цю тему.

Супровідний кодекс

helloWorld :: IO ()
helloWorld = putStrLn "Hello World"

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

Цитата

Якщо у вас є одна і та ж дія IO кілька разів у do-блоці, вона буде виконуватися кілька разів. Тож ця програма виводить рядок "Hello World" тричі. Цей приклад допомагає проілюструвати, що putStrLnце не функція з побічними ефектами. Ми викликаємо putStrLnфункцію один раз для визначення helloWorldзмінної. Якби putStrLnбув побічний ефект друку рядка, він надрукував би лише один раз, і helloWorldзмінна, повторена в основному блоці виконання, не мала би жодного ефекту.

У більшості інших мов програмування така програма буде надрукувати "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();
    }
}

Я впевнений, що я пропускаю щось зовсім просте або неправильно трактую його термінологію. Будь-яка допомога буде дуже вдячна.

Редагувати:

Дякую всім за відповіді! Ваші відповіді допомогли мені краще зрозуміти ці поняття. Я не думаю, що його ще повністю не натиснули, але я перегляну цю тему в майбутньому, дякую!


2
Подумайте про helloWorldпостійність, наприклад, поле чи змінну в C #. Немає жодного параметра, до якого застосовується helloWorld.
Caramiriel

2
putStrLn не має побічного ефекту; він просто повертає IO-дію, та сама дія IO для аргументу "Hello World"незалежно від того, скільки разів ви викликаєте putStrLn.
чепнер

1
Якби це було, helloworldце не була б дія, яка друкує Hello world; це було б значення, повернене putStrLn після його друку Hello World(а саме ()).
чепнер

2
Я думаю, щоб зрозуміти цей приклад, ви вже повинні зрозуміти, як працюють побічні ефекти у Haskell. Це не гарний приклад.
користувач253751

У вашому фрагменті 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"

Зважаючи на це, в цьому прикладі різниця не особливо глибока, тому добре, якщо ви не зовсім зрозуміли те, що автор намагається досягти в цьому розділі, а просто продовжуєте рухатися зараз.

він працює трохи краще, якщо порівнювати його з python

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;
    }
}

Що було б надруковано лише один раз


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дія "вискакує" ефект "прихованого", а пізніші дії просто оцінюють результат (), що не спричиняє подальших наслідків.


1
Варто зауважити, що ви абсолютно не повинні використовувати unsafePerformIOна цьому етапі навчання Haskell. У назві він є "небезпечним" з причини, і ви не повинні використовувати його, якщо ви не зможете (і зробили) уважно розглядати наслідки його використання в контексті. Код, який Данідіаз вводив у відповідь, прекрасно фіксує тип неінтуїтивної поведінки, який може бути наслідком unsafePerformIO.
Ендрю Рей

1

Потрібно помітити одну деталь: ви викликаєте putStrLnфункцію лише один раз, визначаючи helloWorld. У mainфункції ви просто використовуєте повернене значення цього putStrLn "Hello, World"три рази.

Лектор каже, що putStrLnдзвінок не має побічних ефектів, і це правда. Але подивіться на тип helloWorld- це дія вводу-виводу. putStrLnпросто створює це для вас. Пізніше ви пов’язуєте 3 з них doблоком, щоб створити ще одну дію вводу-виводу - main. Пізніше, коли ви виконаєте свою програму, ця дія буде запущена, саме тут криються побічні ефекти.

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.