Haskell'deki saf fonksiyonları ve yan etkileri anlama - putStrLn


10

Son zamanlarda Haskell öğrenmeye başladım çünkü fonksiyonel programlama hakkındaki bilgilerimi genişletmek istedim ve bunu şimdiye kadar gerçekten sevdiğimi söylemeliyim. Şu anda kullandığım kaynak Çoğul Görüş'teki 'Haskell Fundamentals Bölüm 1' dersidir. Ne yazık ki aşağıdaki kod hakkında öğretim elemanının belirli bir alıntı anlamakta biraz zorluk var ve çocuklar konuya biraz ışık tutabileceğini umuyordum.

Refakatçi Kodu

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

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

Alıntı

Bir do-bloğunda aynı GÇ eylemine birden çok kez sahipseniz, eylem birden çok kez çalıştırılır. Bu program, 'Merhaba Dünya' dizesini üç kez yazdırır. Bu örnek putStrLn, bunun yan etkileri olan bir işlev olmadığını göstermeye yardımcı olur . Değişkeni putStrLntanımlamak için işlevi bir kez çağırırız helloWorld. Dizeyi putStrLnyazdırmanın bir yan etkisi olsaydı, yalnızca bir kez yazdırırdı ve helloWorldana do-blokta tekrarlanan değişkenin hiçbir etkisi olmazdı.

Diğer programlama dillerinin çoğunda, böyle bir program 'Merhaba Dünya'yı yalnızca bir kez basar, çünkü putStrLnişlev çağrıldığında yazdırma gerçekleşir . Bu ince ayrım genellikle yeni başlayanları tetikler, bu yüzden bunu biraz düşünün ve bu programın neden 'Merhaba Dünya' putStrLnyı üç kez yazdırdığını ve işlev yazdırma işlemini bir yan etki olarak yapsa neden yalnızca bir kez yazdırdığını anladığınızdan emin olun .

Ne anlamadım

Benim için 'Merhaba Dünya' dizesinin üç kez basılması neredeyse doğal görünüyor. helloWorldDeğişkeni (veya işlevi?) Daha sonra çağrılan bir tür geri arama olarak algılarım . Anlamadığım şey, nasıl putStrLnbir yan etkisi olsaydı, dizenin sadece bir kez basılmasına neden olur. Ya da neden diğer programlama dillerinde sadece bir kez basılacağı.

Diyelim ki C # kodunda, şöyle görüneceğini varsayıyorum:

C # (Keman)

using System;

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

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

Eminim oldukça basit bir şeyi gözden kaçırıyorum ya da terminolojisini yanlış yorumluyorum. Herhangi bir yardım büyük mutluluk duyacağız.

DÜZENLE:

Cevaplarınız için hepinize teşekkürler! Yanıtlarınız bu kavramları daha iyi anlamama yardımcı oldu. Henüz tam olarak tıklandığını sanmıyorum, ancak konuyu gelecekte tekrar ziyaret edeceğim, teşekkür ederim!


2
helloWorldC # 'da bir alan veya değişken gibi sabit olmayı düşünün . Uygulanmakta olan bir parametre yok helloWorld.
Caramiriel

2
putStrLn etmez bir yan etkisi vardır; kaç kez çağırırsanız arayın bağımsız değişken için aynı GÇ eylemini döndürür . "Hello World"putStrLn
chepner

1
Öyle olsaydı, helloworldbaskılar o bir eylem olmaz Hello world; Bu tarafından döndürülen değeri olacaktır putStrLn sonra bu baskılı Hello World(yani, ()).
chepner

2
Bu örneği anlamak için Haskell'de yan etkilerin nasıl çalıştığını zaten anlamanız gerekir. Bu iyi bir örnek değil.
user253751

C # snippet'inizde beğenmezsiniz helloWorld = Console.WriteLine("Hello World");. Sadece içerirler Console.WriteLine("Hello World");içinde HelloWorldişleve yürütülecek her HelloWorldçağrılır. Şimdi düşün helloWorld = putStrLn "Hello World"yapar helloWorld. İçerdiği bir IO monadına atanır (). Bir kez bağladıktan >>=sonra, ancak o zaman faaliyetini gerçekleştirir (bir şeyler yazdırır) ve size ()ciltleme operatörünün sağ tarafında verir.
Redu

Yanıtlar:


8

helloWorldYerel bir değişken olarak tanımlarsak yazarın ne anlama geldiğini anlamak daha kolay olurdu :

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

Bu C # benzeri sahte kodla karşılaştırabilirsiniz:

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

C # 'daki Ie WriteLine, argümanını yazdıran ve hiçbir şey döndürmeyen bir prosedürdür. Haskell'de, putStrLnbir dize alan ve yürütülecek bu dizeyi yazdıracak bir eylem veren bir işlevdir. Yazma arasında kesinlikle hiçbir fark olmadığı anlamına gelir

do
  let hello = putStrLn "Hello World"
  hello
  hello

ve

do
  putStrLn "Hello World"
  putStrLn "Hello World"

Bununla birlikte, bu örnekte fark özellikle derin değildir, bu yüzden yazarın bu bölümde almaya çalıştığı şeyi tam olarak almazsanız ve şimdilik devam ederseniz sorun olmaz.

python ile karşılaştırırsanız biraz daha iyi çalışır

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

Nokta burada Haskell IO eylemler yürütülmesini engellemek için daha fazla “geri aramaları” veya benzeri olaylardan sarılmış olması gerekmez “gerçek” değerleri olduğuna olmak - daha doğrusu, tek yol için yapmak çalışması için gereken onları belirli bir yere koymak (örneğin bir yerde mainveya ortaya çıkan bir iplik main).

Bu sadece bir salon hilesi değil, kod yazma şekliniz üzerinde bazı ilginç etkilere neden oluyor (örneğin, Haskell'in neden bildiğiniz ortak kontrol yapılarından herhangi birine gerçekten ihtiyaç duymamasının bir nedeni zorunlu dillerden ve işlevler açısından her şeyi yapmaktan kurtulabilir), ama yine de bu konuda çok fazla endişelenmem (bunlar gibi analojiler her zaman hemen tıklanmaz)


4

Aslında bir şey yapan bir işlev kullanırsanız, farkı açıklandığı gibi görmek daha kolay olabilir helloWorld. Aşağıdakileri düşünün:

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

Bu, 3 kez "2 ve 3 ekliyorum" yazdıracaktır.

C # 'da aşağıdakileri yazabilirsiniz:

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

Bu sadece bir kez basar.


3

Değerlendirmenin putStrLn "Hello World"yan etkileri olsaydı, o zaman mesaj sadece bir kez yazdırılır.

Bu senaryoyu aşağıdaki kodla tahmin edebiliriz:

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

unsafePerformIObir IOeylemde bulunur ve IOeylemin bileşimi tarafından dayatılan olağan sekanslamadan ayrılması ve IOtembel değerlendirmenin değişkenlerine göre etkinin gerçekleşmesine (veya olmamasına) izin veren bir eylemi "unutur" .

evaluatesaf bir değer alır ve sonuçta ortaya çıkan IOeylem değerlendirildiğinde değerin değerlendirilmesini sağlar - bu bizim için olacaktır, çünkü yolunda yatar main. Burada bazı değerlerin değerlendirilmesini programın uygulanmasına bağlamak için kullanıyoruz.

Bu kod yalnızca bir kez "Merhaba Dünya" yı yazdırır. helloWorldSaf bir değer olarak davranırız . Ancak bu, tüm evaluate helloWorldçağrılar arasında paylaşılacağı anlamına gelir . Ve neden olmasın? Sonuçta saf bir değer, neden gereksiz yere yeniden hesaplıyoruz? İlk evaluateeylem "gizli" efekti "açar" ve sonraki eylemler sadece sonucu değerlendirir (), bu da başka etkilere neden olmaz.


1
unsafePerformIOHaskell'i öğrenmenin bu aşamasında kesinlikle kullanmamanız gerektiğini belirtmek gerekir . Bir nedenle adında "güvensiz" vardır ve kullanımının sonuçlarını bağlam içinde dikkatlice düşünemedikçe (ve yapmadıkça) kullanmamalısınız. Danidiaz'ın yanıta koyduğu kod, kaynaklanabilecek sezgisel olmayan davranışları mükemmel bir şekilde yakalar unsafePerformIO.
Andrew Ray

1

Dikkat edilmesi gereken bir ayrıntı vardır: putStrLntanımlarken işlevi yalnızca bir kez çağırırsınız helloWorld. In mainfonksiyonu sadece o dönüş değeri kullanın putStrLn "Hello, World"üç kez.

Öğretim üyesi putStrLnçağrının yan etkisi olmadığını ve doğru olduğunu söylüyor. Ancak türüne bakın helloWorld- bu bir IO eylemidir. putStrLnsadece sizin için yaratır. Daha sonra, dobaşka bir IO eylemi oluşturmak için 3 tanesini blokla zincirlersiniz main. Daha sonra, programınızı yürüttüğünüzde, bu eylem yürütülecektir, bu yan etkilerin yattığı yerdir.

Bunun temelinde yatan mekanizma - monadlar . Bu güçlü konsept, yan etkileri doğrudan desteklemeyen bir dilde yazdırma gibi bazı yan efektleri kullanmanızı sağlar. Sadece bazı eylemleri zincirlersiniz ve bu zincir programınızın başlangıcında çalıştırılır. Haskell'i ciddiye almak istiyorsanız bu kavramı derinlemesine anlamanız gerekir.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.