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 putStrLn
itu bukan fungsi dengan efek samping. Kami memanggil putStrLn
fungsi satu kali untuk mendefinisikan helloWorld
variabel. Jika putStrLn
ada efek samping dari mencetak string, itu hanya akan mencetak sekali dan helloWorld
variabel 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 putStrLn
fungsinya 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 putStrLn
fungsi melakukan pencetakan sebagai efek samping.
Apa yang tidak saya mengerti
Bagi saya tampaknya hampir alami bahwa string 'Hello World' dicetak tiga kali. Saya menganggap helloWorld
variabel (atau fungsi?) Sebagai semacam panggilan balik yang dipanggil nanti. Yang tidak saya mengerti adalah, bagaimana jika putStrLn
memiliki 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!
helloWorld
menjadi konstan seperti bidang atau variabel dalam C #. Tidak ada parameter yang diterapkanhelloWorld
.putStrLn
tidak memiliki efek samping; itu hanya mengembalikan tindakan IO, tindakan IO yang sama untuk argumen"Hello World"
tidak peduli berapa kali Anda meneleponputStrLn
.helloworld
tidak akan menjadi tindakan yang mencetakHello world
; itu akan menjadi nilai yang dikembalikan olehputStrLn
setelah itu dicetakHello World
(yaitu,()
).helloWorld = Console.WriteLine("Hello World");
. Anda hanya mengandungConsole.WriteLine("Hello World");
dalamHelloWorld
fungsi yang akan dijalankan setiap kaliHelloWorld
dipanggil. Sekarang pikirkan tentang apa yanghelloWorld = putStrLn "Hello World"
membuatnyahelloWorld
. 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.Jawaban:
Mungkin akan lebih mudah untuk memahami apa yang penulis maksudkan jika kita mendefinisikan
helloWorld
sebagai variabel lokal:yang bisa Anda bandingkan dengan pseudocode mirip-C ini:
Yaitu di C #
WriteLine
adalah prosedur yang mencetak argumennya dan tidak mengembalikan apa pun. Di Haskell,putStrLn
adalah fungsi yang mengambil string dan memberi Anda tindakan yang akan mencetak string yang akan dieksekusi. Artinya sama sekali tidak ada perbedaan antara menulisdan
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
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
main
atau utas munculmain
).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)
sumber
Mungkin lebih mudah untuk melihat perbedaan seperti yang dijelaskan jika Anda menggunakan fungsi yang benar-benar melakukan sesuatu, daripada
helloWorld
. Pikirkan hal-hal berikut:Ini akan mencetak "Saya menambahkan 2 dan 3" 3 kali.
Di C #, Anda dapat menulis yang berikut:
Yang akan mencetak hanya sekali.
sumber
Jika evaluasi
putStrLn "Hello World"
memiliki efek samping, maka pesan hanya akan dicetak sekali.Kami dapat memperkirakan skenario itu dengan kode berikut:
unsafePerformIO
mengambilIO
tindakan dan "lupa" ituIO
tindakan, menghapusnya dari urutan yang biasa diberlakukan oleh komposisiIO
tindakan dan membiarkan efek terjadi (atau tidak) sesuai dengan keanehan evaluasi malas.evaluate
mengambil nilai murni dan memastikan bahwa nilai tersebut dievaluasi setiap kaliIO
tindakan yang dihasilkan dievaluasi — yang bagi kita itu akan terjadi, karena itu terletak di jalurmain
. Kami menggunakannya di sini untuk menghubungkan evaluasi beberapa nilai ke pengesahan program.Kode ini hanya mencetak "Hello World" satu kali. Kami memperlakukan
helloWorld
sebagai nilai murni. Tetapi itu berarti akan dibagikan di antara semuaevaluate helloWorld
panggilan. Dan kenapa tidak? Bagaimanapun juga, ini adalah nilai murni, mengapa menghitung ulang dengan sia-sia? Tindakan pertamaevaluate
"memunculkan" efek "tersembunyi" dan tindakan selanjutnya hanya mengevaluasi hasilnya()
, yang tidak menyebabkan efek lebih lanjut.sumber
unsafePerformIO
pada 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 dihasilkanunsafePerformIO
.Ada satu detail yang perlu diperhatikan: Anda memanggil
putStrLn
fungsi hanya sekali, saat mendefinisikanhelloWorld
. Dalammain
fungsi Anda hanya menggunakan nilai balik ituputStrLn "Hello, World"
tiga kali.Dosen mengatakan bahwa
putStrLn
panggilan itu tidak memiliki efek samping dan itu benar. Tapi lihat jenishelloWorld
- itu adalah tindakan IO.putStrLn
hanya menciptakannya untuk Anda. Kemudian, Anda rantai 3 dari mereka dengando
blok 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.
sumber