Memahami fungsi murni dan efek samping di Haskell - putStrLn

10

Baru-baru ini, saya mulai belajar Haskell karena saya ingin memperluas pengetahuan saya tentang pemrograman fungsional dan saya harus mengatakan saya sangat menyukainya sejauh ini. Sumber daya yang saya gunakan saat ini adalah kursus 'Haskell Fundamentals Part 1' di Pluralsight. Sayangnya saya mengalami kesulitan memahami satu kutipan khusus dari dosen tentang kode berikut dan berharap kalian bisa menjelaskan topik ini.

Kode Pengiring

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

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

Kutipan

Jika Anda memiliki tindakan IO yang sama beberapa kali dalam do-block, itu akan dijalankan beberapa kali. Jadi program ini mencetak string 'Hello World' tiga kali. Contoh ini membantu menggambarkan bahwa putStrLnitu bukan fungsi dengan efek samping. Kami memanggil putStrLnfungsi satu kali untuk mendefinisikan helloWorldvariabel. Jika putStrLnada efek samping dari mencetak string, itu hanya akan mencetak sekali dan helloWorldvariabel yang diulang dalam do-block utama tidak akan memiliki efek apa pun.

Di sebagian besar bahasa pemrograman lain, program seperti ini hanya akan mencetak 'Hello World' sekali saja, karena pencetakan akan terjadi ketika putStrLnfungsinya dipanggil. Perbedaan yang halus ini sering membuat pemula tersandung, jadi pikirkanlah ini sedikit, dan pastikan Anda memahami mengapa program ini mencetak 'Hello World' tiga kali dan mengapa ia akan mencetaknya hanya sekali jika putStrLnfungsi melakukan pencetakan sebagai efek samping.

Apa yang tidak saya mengerti

Bagi saya tampaknya hampir alami bahwa string 'Hello World' dicetak tiga kali. Saya menganggap helloWorldvariabel (atau fungsi?) Sebagai semacam panggilan balik yang dipanggil nanti. Yang tidak saya mengerti adalah, bagaimana jika putStrLnmemiliki efek samping, itu akan menghasilkan string yang dicetak hanya sekali. Atau mengapa hanya dicetak sekali dalam bahasa pemrograman lain.

Katakanlah dalam kode C #, saya akan menganggapnya akan terlihat seperti ini:

C # (Fiddle)

using System;

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

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

Saya yakin saya mengabaikan sesuatu yang cukup sederhana atau salah menafsirkan terminologinya. Bantuan apa pun akan sangat dihargai.

EDIT:

Terima kasih atas jawaban Anda! Jawaban Anda membantu saya mendapatkan pemahaman yang lebih baik tentang konsep-konsep ini. Saya pikir belum sepenuhnya diklik, tapi saya akan kembali ke topik di masa depan, terima kasih!

Lancar
sumber
2
Pikirkan helloWorldmenjadi konstan seperti bidang atau variabel dalam C #. Tidak ada parameter yang diterapkan helloWorld.
Caramiriel
2
putStrLn tidak memiliki efek samping; itu hanya mengembalikan tindakan IO, tindakan IO yang sama untuk argumen "Hello World"tidak peduli berapa kali Anda menelepon putStrLn.
chepner
1
Jika ya, helloworldtidak akan menjadi tindakan yang mencetak Hello world; itu akan menjadi nilai yang dikembalikan oleh putStrLn setelah itu dicetak Hello World(yaitu, ()).
chepner
2
Saya pikir untuk memahami contoh ini Anda harus memahami bagaimana efek samping bekerja di Haskell. Itu bukan contoh yang baik.
user253751
Dalam cuplikan C # Anda tidak suka helloWorld = Console.WriteLine("Hello World");. Anda hanya mengandung Console.WriteLine("Hello World");dalam HelloWorldfungsi yang akan dijalankan setiap kali HelloWorlddipanggil. Sekarang pikirkan tentang apa yang helloWorld = putStrLn "Hello World"membuatnya helloWorld. Itu akan ditugaskan ke monad IO yang berisi (). Setelah Anda mengikatnya >>=hanya akan melakukan aktivitasnya (mencetak sesuatu) dan akan memberikan Anda ()di sisi kanan operator bind.
Redu

Jawaban:

8

Mungkin akan lebih mudah untuk memahami apa yang penulis maksudkan jika kita mendefinisikan helloWorldsebagai variabel lokal:

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

yang bisa Anda bandingkan dengan pseudocode mirip-C ini:

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

Yaitu di C # WriteLineadalah prosedur yang mencetak argumennya dan tidak mengembalikan apa pun. Di Haskell, putStrLnadalah fungsi yang mengambil string dan memberi Anda tindakan yang akan mencetak string yang akan dieksekusi. Artinya sama sekali tidak ada perbedaan antara menulis

do
  let hello = putStrLn "Hello World"
  hello
  hello

dan

do
  putStrLn "Hello World"
  putStrLn "Hello World"

Yang sedang berkata, dalam contoh ini perbedaannya tidak terlalu mendalam, jadi tidak apa-apa jika Anda tidak mendapatkan apa yang penulis coba untuk dapatkan di bagian ini dan lanjutkan untuk sekarang.

ini bekerja sedikit lebih baik jika Anda membandingkannya dengan python

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

Intinya di sini adalah bahwa tindakan IO di Haskell adalah “nyata” nilai-nilai yang tidak perlu dibungkus lanjut “callback” atau hal semacam itu untuk mencegah mereka dari mengeksekusi - bukan, satu-satunya cara untuk melakukan mendapatkan mereka untuk mengeksekusi yaitu untuk meletakkannya di tempat tertentu (yaitu suatu tempat di dalam mainatau utas muncul main).

Ini bukan hanya trik salon juga, ini akhirnya memiliki beberapa efek menarik pada bagaimana Anda menulis kode (misalnya, itu bagian dari alasan mengapa Haskell tidak benar-benar membutuhkan struktur kontrol umum yang Anda kenal dengan dari bahasa imperatif dan dapat melakukan apa saja dalam hal fungsi sebagai gantinya), tapi sekali lagi saya tidak akan terlalu khawatir tentang hal ini (analogi seperti ini tidak selalu langsung klik)

Kubik
sumber
4

Mungkin lebih mudah untuk melihat perbedaan seperti yang dijelaskan jika Anda menggunakan fungsi yang benar-benar melakukan sesuatu, daripada helloWorld. Pikirkan hal-hal berikut:

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

Ini akan mencetak "Saya menambahkan 2 dan 3" 3 kali.

Di C #, Anda dapat menulis yang berikut:

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

Yang akan mencetak hanya sekali.

oisdk
sumber
3

Jika evaluasi putStrLn "Hello World"memiliki efek samping, maka pesan hanya akan dicetak sekali.

Kami dapat memperkirakan skenario itu dengan kode berikut:

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

unsafePerformIOmengambil IOtindakan dan "lupa" itu IOtindakan, menghapusnya dari urutan yang biasa diberlakukan oleh komposisi IOtindakan dan membiarkan efek terjadi (atau tidak) sesuai dengan keanehan evaluasi malas.

evaluatemengambil nilai murni dan memastikan bahwa nilai tersebut dievaluasi setiap kali IOtindakan yang dihasilkan dievaluasi — yang bagi kita itu akan terjadi, karena itu terletak di jalur main. Kami menggunakannya di sini untuk menghubungkan evaluasi beberapa nilai ke pengesahan program.

Kode ini hanya mencetak "Hello World" satu kali. Kami memperlakukan helloWorldsebagai nilai murni. Tetapi itu berarti akan dibagikan di antara semua evaluate helloWorldpanggilan. Dan kenapa tidak? Bagaimanapun juga, ini adalah nilai murni, mengapa menghitung ulang dengan sia-sia? Tindakan pertama evaluate"memunculkan" efek "tersembunyi" dan tindakan selanjutnya hanya mengevaluasi hasilnya (), yang tidak menyebabkan efek lebih lanjut.

danidiaz
sumber
1
Perlu dicatat bahwa Anda sama sekali tidak boleh menggunakan unsafePerformIOpada tahap pembelajaran Haskell ini. Itu memiliki "tidak aman" dalam nama karena suatu alasan, dan Anda tidak boleh menggunakannya kecuali Anda dapat (dan memang) mempertimbangkan dengan cermat implikasi penggunaannya dalam konteks. Kode yang dimasukkan danidiaz dalam jawaban dengan sempurna menangkap jenis perilaku tidak intuitif yang dapat dihasilkan unsafePerformIO.
Andrew Ray
1

Ada satu detail yang perlu diperhatikan: Anda memanggil putStrLnfungsi hanya sekali, saat mendefinisikan helloWorld. Dalam mainfungsi Anda hanya menggunakan nilai balik itu putStrLn "Hello, World"tiga kali.

Dosen mengatakan bahwa putStrLnpanggilan itu tidak memiliki efek samping dan itu benar. Tapi lihat jenis helloWorld- itu adalah tindakan IO. putStrLnhanya menciptakannya untuk Anda. Kemudian, Anda rantai 3 dari mereka dengan doblok untuk membuat tindakan IO lain - main. Kemudian, ketika Anda menjalankan program Anda, tindakan itu akan dijalankan, di situlah letak efek sampingnya.

Mekanisme yang ada di dasar ini - monad . Konsep yang kuat ini memungkinkan Anda untuk menggunakan beberapa efek samping seperti mencetak dalam bahasa yang tidak mendukung efek samping secara langsung. Anda hanya rantai beberapa tindakan, dan rantai itu akan dijalankan pada awal program Anda. Anda perlu memahami konsep itu secara mendalam jika Anda ingin menggunakan Haskell dengan serius.

Yuri Kovalenko
sumber