Comprensione delle funzioni pure e degli effetti collaterali in Haskell - putStrLn


10

Di recente, ho iniziato a studiare Haskell perché volevo ampliare le mie conoscenze sulla programmazione funzionale e devo dire che finora lo adoro davvero. La risorsa che sto attualmente utilizzando è il corso "Haskell Fundamentals Part 1" su Pluralsight. Sfortunatamente ho qualche difficoltà a capire una particolare citazione del docente sul seguente codice e spero che voi ragazzi possiate far luce sull'argomento.

Codice di accompagnamento

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

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

La citazione

Se hai la stessa azione IO più volte in un do-block, verrà eseguita più volte. Quindi questo programma stampa tre volte la stringa "Hello World". Questo esempio aiuta a dimostrare che putStrLnnon è una funzione con effetti collaterali. Chiamiamo la putStrLnfunzione una volta per definire la helloWorldvariabile. Se putStrLnavesse avuto un effetto collaterale nel stampare la stringa, sarebbe stata stampata una sola volta e la helloWorldvariabile ripetuta nel do-block principale non avrebbe avuto alcun effetto.

Nella maggior parte degli altri linguaggi di programmazione, un programma come questo stampa 'Hello World' una sola volta, poiché la stampa avverrebbe quando putStrLnviene chiamata la funzione. Questa sottile distinzione farebbe spesso inciampare i principianti, quindi pensaci un po 'e assicurati di capire perché questo programma stampa "Hello World" tre volte e perché lo stampa solo una volta se la putStrLnfunzione eseguisse la stampa come effetto collaterale.

Quello che non capisco

Per me sembra quasi naturale che la stringa "Hello World" venga stampata tre volte. Percepisco ilhelloWorld variabile (o la funzione?) Come una sorta di callback che viene invocato in seguito. Quello che non capisco è, come se putStrLnavesse un effetto collaterale, la stringa verrebbe stampata una sola volta. O perché sarebbe stampato solo una volta in altri linguaggi di programmazione.

Diciamo nel codice C #, presumo che sarebbe simile a questo:

C # (violino)

using System;

public class Program
{
    public static void HelloWorld()
    {
        Console.WriteLine("Hello World");
    }

    public static void Main()
    {
        HelloWorld();
        HelloWorld();
        HelloWorld();
    }
}

Sono sicuro di trascurare qualcosa di abbastanza semplice o di interpretare erroneamente la sua terminologia. Qualsiasi aiuto sarebbe molto apprezzato.

MODIFICARE:

Grazie a tutti per le vostre risposte! Le tue risposte mi hanno aiutato a capire meglio questi concetti. Non credo che sia stato ancora completamente cliccato, ma rivisiterò l'argomento in futuro, grazie!


2
Pensa di helloWorldessere costante come un campo o una variabile in C #. Non ci sono parametri a cui applicare helloWorld.
Caramiriel,

2
putStrLn non ha effetti collaterali; restituisce semplicemente un'azione I / O, la stessa azione I / O per l'argomento "Hello World"non importa quante volte si chiama putStrLn.
Chepner,

1
Se lo facesse, helloworldnon sarebbe un'azione che stampa Hello world; sarebbe il valore restituito da putStrLn dopo la stampa Hello World(ovvero, ()).
Chepner,

2
Penso che per capire questo esempio dovresti già capire come funzionano gli effetti collaterali in Haskell. Non è un buon esempio.
user253751

Nel tuo frammento di C # non ti piace helloWorld = Console.WriteLine("Hello World");. Basta contenere l' Console.WriteLine("Hello World");in HelloWorldogni funzione da eseguire HelloWorldviene invocato. Ora pensa a ciò che helloWorld = putStrLn "Hello World"rende helloWorld. Viene assegnato a una monade IO che contiene (). Una volta che lo vincoli, >>=solo allora eseguirà la sua attività (stampa qualcosa) e ti darà ()sul lato destro dell'operatore di rilegatura.
Redu,

Risposte:


8

Probabilmente sarebbe più facile capire cosa significa l'autore se definiamo helloWorlduna variabile locale:

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

che potresti confrontare con questo pseudocodice simile a C #:

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

Vale a dire in C # WriteLineè una procedura che stampa il suo argomento e non restituisce nulla. In Haskell, putStrLnè una funzione che accetta una stringa e ti dà un'azione che stampa quella stringa se dovesse essere eseguita. Significa che non c'è assolutamente alcuna differenza tra la scrittura

do
  let hello = putStrLn "Hello World"
  hello
  hello

e

do
  putStrLn "Hello World"
  putStrLn "Hello World"

Detto questo, in questo esempio la differenza non è particolarmente profonda, quindi va bene se non ottieni esattamente ciò che l'autore sta cercando di ottenere in questa sezione e vai avanti per ora.

funziona un po 'meglio se lo confronti con Python

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

Il punto qui è che le azioni IO a Haskell sono valori “veri” che non hanno bisogno di essere avvolto in ulteriori “callback” o qualcosa del genere per impedire loro di esecuzione - anzi, l'unico modo per fare ottenere loro da eseguire è metterli in un posto particolare (cioè da qualche parte all'interno maino un filo generatomain ).

Questo non è solo un trucco da parlatore, finisce per avere alcuni effetti interessanti su come si scrive il codice (ad esempio, fa parte del motivo per cui Haskell non ha davvero bisogno di nessuna delle strutture di controllo comuni che si avrebbero familiarità con da linguaggi imperativi e può cavarsela facendo invece tutto in termini di funzioni), ma ancora una volta non mi preoccuperei troppo di questo (analogie come queste non sempre fanno clic immediatamente)


4

Potrebbe essere più facile vedere la differenza come descritto se si utilizza una funzione che fa effettivamente qualcosa, piuttosto che helloWorld. Pensa a quanto segue:

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 ()

Questo stamperà "Sto aggiungendo 2 e 3" 3 volte.

In C #, potresti scrivere quanto segue:

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

Che sarebbe stampato solo una volta.


3

Se la valutazione di putStrLn "Hello World" avesse effetti collaterali, allora il messaggio verrebbe stampato solo una volta.

Possiamo approssimare quello scenario con il seguente codice:

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

unsafePerformIOprende IOun'azione e "dimentica" è IOun'azione, smontandola dal solito sequenziamento imposto dalla composizione delle IOazioni e lasciando che l'effetto abbia luogo (o meno) secondo i capricci della valutazione pigra.

evaluateassume un valore puro e garantisce che il valore venga valutato ogni volta che viene valutata l' IOazione risultante , cosa che per noi sarà, perché si trova sul percorso di main. Lo stiamo usando qui per collegare la valutazione di alcuni valori all'uscita del programma.

Questo codice stampa "Hello World" solo una volta. Trattiamo helloWorldcome un valore puro. Ciò significa che verrà condiviso tra tutte le evaluate helloWorldchiamate. E perchè no? Dopotutto è un valore puro, perché ricalcolarlo inutilmente? La prima evaluateazione "apre" l'effetto "nascosto" e le azioni successive valutano semplicemente il risultato (), il che non causa ulteriori effetti.


1
Vale la pena notare che non si dovrebbe assolutamente usare unsafePerformIOin questa fase dell'apprendimento di Haskell. Ha un nome "non sicuro" per un motivo e non dovresti usarlo a meno che tu non possa (e abbia fatto) attentamente considerare le implicazioni del suo utilizzo nel contesto. Il codice che Danidiaz inserisce nella risposta cattura perfettamente il tipo di comportamento non intuitivo che può derivare unsafePerformIO.
Andrew Ray,

1

C'è un dettaglio da notare: si chiama putStrLnfunzione una sola volta, durante la definizione helloWorld. In mainfunzione, basta usare il valore restituito per putStrLn "Hello, World"tre volte.

Il docente afferma che la putStrLnchiamata non ha effetti collaterali ed è vera. Ma guarda il tipo di helloWorld- è un'azione IO. putStrLnlo crea solo per te. Più tardi, ne fai una catena 3 con il doblocco per creare un'altra azione IO - main. Successivamente, quando esegui il tuo programma, quell'azione verrà eseguita, ecco dove si trovano gli effetti collaterali.

Il meccanismo che sta alla base di questo - monadi . Questo potente concetto ti consente di utilizzare alcuni effetti collaterali come la stampa in un linguaggio che non supporta direttamente gli effetti collaterali. Basta concatenare alcune azioni e quella catena verrà eseguita all'avvio del programma. Dovrai comprendere a fondo questo concetto se vuoi usare seriamente Haskell.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.