Comprendre les fonctions pures et les effets secondaires dans Haskell - putStrLn


10

Récemment, j'ai commencé à apprendre Haskell parce que je voulais élargir mes connaissances sur la programmation fonctionnelle et je dois dire que je l'aime vraiment jusqu'à présent. La ressource que j'utilise actuellement est le cours «Haskell Fundamentals Part 1» sur Pluralsight. Malheureusement, j'ai du mal à comprendre une citation particulière du conférencier au sujet du code suivant et j'espérais que vous pourriez éclairer le sujet.

Code d'accompagnement

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

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

La citation

Si vous avez la même action IO plusieurs fois dans un do-block, elle sera exécutée plusieurs fois. Ce programme imprime donc la chaîne «Hello World» trois fois. Cet exemple permet d'illustrer que ce putStrLnn'est pas une fonction avec des effets secondaires. Nous appelons la putStrLnfonction une fois pour définir la helloWorldvariable. Si cela putStrLnavait pour effet secondaire d'imprimer la chaîne, elle ne s'imprimerait qu'une seule fois et la helloWorldvariable répétée dans le do-block principal n'aurait aucun effet.

Dans la plupart des autres langages de programmation, un programme comme celui-ci n'imprimait 'Hello World' qu'une seule fois, car l'impression se produisait lorsque la putStrLnfonction était appelée. Cette distinction subtile ferait souvent trébucher les débutants, alors pensez-y un peu et assurez-vous de comprendre pourquoi ce programme imprime `` Hello World '' trois fois et pourquoi il ne l'imprimerait qu'une seule fois si la putStrLnfonction faisait l'impression comme effet secondaire.

Ce que je ne comprends pas

Pour moi, il semble presque naturel que la chaîne «Hello World» soit imprimée trois fois. Je perçois la helloWorldvariable (ou la fonction?) Comme une sorte de rappel qui est invoqué plus tard. Ce que je ne comprends pas, c'est que si cela putStrLnavait un effet secondaire, cela entraînerait l'impression de la chaîne une seule fois. Ou pourquoi il ne serait imprimé qu'une seule fois dans d'autres langages de programmation.

Disons que dans le code C #, je suppose que cela ressemblerait à ceci:

C # (violon)

using System;

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

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

Je suis sûr que j'oublie quelque chose d'assez simple ou que je interprète mal sa terminologie. Toute aide serait grandement appréciée.

ÉDITER:

Merci à tous pour vos réponses! Vos réponses m'ont aidé à mieux comprendre ces concepts. Je ne pense pas qu'il ait complètement cliqué pour le moment, mais je reviendrai sur le sujet à l'avenir, merci!


2
Pensez à helloWorldêtre constant comme un champ ou une variable en C #. Aucun paramètre n'est appliqué helloWorld.
Caramiriel

2
putStrLn n'a pas d'effet secondaire; il renvoie simplement une action IO, la même action IO pour l'argument, "Hello World"quel que soit le nombre de fois que vous appelez putStrLn.
chepner

1
Si c'était le cas, ce ne helloworldserait pas une action qui s'imprime Hello world; ce serait la valeur renvoyée par putStrLn après son impression Hello World(à savoir, ()).
chepner

2
Je pense que pour comprendre cet exemple, vous devez déjà comprendre comment les effets secondaires fonctionnent dans Haskell. Ce n'est pas un bon exemple.
user253751

Dans votre extrait de code C #, vous n'aimez pas helloWorld = Console.WriteLine("Hello World");. Vous venez de contenir la Console.WriteLine("Hello World");dans la HelloWorldfonction à exécuter chaque fois que vous HelloWorldinvoquez. Pensez maintenant à ce qui helloWorld = putStrLn "Hello World"fait helloWorld. Il est affecté à une monade IO qui contient (). Une fois que vous le lierez, il >>=effectuera seulement son activité (impression de quelque chose) et vous donnera ()sur le côté droit de l'opérateur de liaison.
Redu

Réponses:


8

Il serait probablement plus facile de comprendre ce que l'auteur veut dire si nous définissons helloWorldcomme variable locale:

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

que vous pourriez comparer à ce pseudocode de type C #:

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

Ie en C # WriteLineest une procédure qui imprime son argument et ne renvoie rien. Dans Haskell, putStrLnest une fonction qui prend une chaîne et vous donne une action qui afficherait cette chaîne si elle devait être exécutée. Cela signifie qu'il n'y a absolument aucune différence entre l'écriture

do
  let hello = putStrLn "Hello World"
  hello
  hello

et

do
  putStrLn "Hello World"
  putStrLn "Hello World"

Cela étant dit, dans cet exemple, la différence n'est pas particulièrement profonde, donc c'est bien si vous n'obtenez pas tout à fait ce que l'auteur essaie de comprendre dans cette section et que vous passez à autre chose pour l'instant.

cela fonctionne un peu mieux si vous le comparez à python

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

Le point ici étant que les actions d'E / S dans Haskell sont des valeurs «réelles» qui n'ont pas besoin d'être encapsulées dans d'autres «rappels» ou quoi que ce soit du genre pour les empêcher de s'exécuter - plutôt, la seule façon de les amener à s'exécuter est pour les mettre dans un endroit particulier (c'est-à-dire quelque part à l'intérieur mainou un fil engendré main).

Ce n'est pas seulement une astuce de salon non plus, cela finit par avoir des effets intéressants sur la façon dont vous écrivez du code (par exemple, cela fait partie de la raison pour laquelle Haskell n'a pas vraiment besoin des structures de contrôle communes que vous connaissez avec des langages impératifs et peut à la place tout faire en termes de fonctions), mais encore une fois, je ne m'inquiéterais pas trop à ce sujet (des analogies comme celles-ci ne cliquent pas toujours immédiatement)


4

Il pourrait être plus facile de voir la différence comme décrit si vous utilisez une fonction qui fait réellement quelque chose, plutôt que helloWorld. Pensez à ce qui suit:

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

Cela imprimera "j'ajoute 2 et 3" 3 fois.

En C #, vous pouvez écrire ce qui suit:

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

Qui n'imprimerait qu'une seule fois.


3

Si l'évaluation a putStrLn "Hello World"eu des effets secondaires, alors le message ne sera imprimé qu'une seule fois.

Nous pouvons approximer ce scénario avec le code suivant:

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

unsafePerformIOprend une IOaction et «oublie» que c'est une IOaction, la détache de l'enchaînement habituel imposé par la composition des IOactions et laisse l'effet se produire (ou non) selon les caprices de l'évaluation paresseuse.

evaluateprend une valeur pure et garantit que la valeur est évaluée chaque fois que l' IOaction résultante est évaluée - ce qui pour nous, ce sera, car elle se situe dans le chemin de main. Nous l'utilisons ici pour relier l'évaluation de certaines valeurs à l'exection du programme.

Ce code imprime une seule fois "Hello World". Nous traitons helloWorldcomme une valeur pure. Mais cela signifie qu'il sera partagé entre tous les evaluate helloWorldappels. Et pourquoi pas? C'est une pure valeur après tout, pourquoi la recalculer inutilement? La première evaluateaction "fait apparaître" l'effet "caché" et les actions ultérieures évaluent simplement le résultat (), ce qui ne provoque aucun autre effet.


1
Il convient de noter que vous ne devez absolument pas utiliser unsafePerformIOà ce stade de l'apprentissage de Haskell. Il a «dangereux» dans le nom pour une raison, et vous ne devriez pas l'utiliser à moins que vous puissiez (et avez) soigneusement considérer les implications de son utilisation dans le contexte. Le code que danidiaz a mis dans la réponse capture parfaitement le type de comportement non intuitif qui peut en résulter unsafePerformIO.
Andrew Ray

1

Il y a un détail à noter: vous n'appelez putStrLnfonction qu'une seule fois, tout en définissant helloWorld. En mainfonction, vous utilisez simplement la valeur de retour de cela putStrLn "Hello, World"trois fois.

Le conférencier dit que l' putStrLnappel n'a pas d'effets secondaires et c'est vrai. Mais regardez le type de helloWorld- c'est une action d'E / S. putStrLnle crée juste pour vous. Plus tard, vous enchaînez 3 d'entre eux avec le dobloc pour créer une autre action IO - main. Plus tard, lorsque vous exécuterez votre programme, cette action sera exécutée, c'est là que se situent les effets secondaires.

Le mécanisme qui se trouve à la base de cela - les monades . Ce concept puissant vous permet d'utiliser certains effets secondaires comme l'impression dans un langage qui ne prend pas directement en charge les effets secondaires. Vous enchaînez juste quelques actions, et cette chaîne sera exécutée au démarrage de votre programme. Vous devrez comprendre ce concept en profondeur si vous souhaitez utiliser Haskell sérieusement.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.