Apa arti tanda seru dalam deklarasi Haskell?

262

Saya menemukan definisi berikut ketika saya mencoba mempelajari Haskell menggunakan proyek nyata untuk mengendarainya. Saya tidak mengerti apa arti tanda seru di depan setiap argumen dan sepertinya buku-buku saya tidak menyebutkannya.

data MidiMessage = MidiMessage !Int !MidiMessage
David
sumber
13
Saya menduga bahwa ini mungkin pertanyaan yang sangat umum; Saya ingat dengan jelas bertanya-tanya tentang hal yang persis sama sendiri, jauh ketika.
cjs

Jawaban:

314

Ini deklarasi ketat. Pada dasarnya, itu berarti bahwa itu harus dievaluasi untuk apa yang disebut "bentuk normal kepala lemah" ketika nilai struktur data dibuat. Mari kita lihat sebuah contoh, sehingga kita dapat melihat apa artinya ini:

data Foo = Foo Int Int !Int !(Maybe Int)

f = Foo (2+2) (3+3) (4+4) (Just (5+5))

Fungsi di fatas, ketika dievaluasi, akan mengembalikan "thunk": yaitu kode yang akan dieksekusi untuk mencari nilainya. Pada titik itu, Foo bahkan belum ada, hanya kodenya.

Tetapi pada titik tertentu seseorang mungkin mencoba melihat ke dalamnya, mungkin melalui pencocokan pola:

case f of
     Foo 0 _ _ _ -> "first arg is zero"
     _           -> "first arge is something else"

Ini akan mengeksekusi kode yang cukup untuk melakukan apa yang dibutuhkan, dan tidak lebih. Jadi itu akan membuat Foo dengan empat parameter (karena Anda tidak dapat melihat di dalamnya tanpa itu ada). Yang pertama, sejak kami mengujinya, kami perlu mengevaluasi sampai ke sana 4, di mana kami menyadari itu tidak cocok.

Yang kedua tidak perlu dievaluasi, karena kami tidak mengujinya. Jadi, daripada 6disimpan di lokasi memori itu, kami hanya akan menyimpan kode untuk kemungkinan evaluasi nanti (3+3),. Itu akan berubah menjadi 6 hanya jika seseorang melihatnya.

Namun, parameter ketiga ada !di depannya, jadi dievaluasi secara ketat: (4+4)dijalankan, dan 8disimpan di lokasi memori itu.

Parameter keempat juga dievaluasi secara ketat. Tapi di sinilah agak rumit: kita mengevaluasi tidak sepenuhnya, tetapi hanya untuk bentuk kepala normal yang lemah. Ini berarti bahwa kita mencari tahu apakah itu sesuatu Nothingatau Justsesuatu, dan menyimpannya, tetapi kita tidak melangkah lebih jauh. Itu berarti bahwa kita tidak menyimpan Just 10tetapi sebenarnya Just (5+5), membiarkan bagian dalam tidak dievaluasi. Ini penting untuk diketahui, meskipun saya pikir semua implikasi ini melampaui lingkup pertanyaan ini.

Anda dapat membuat anotasi argumen fungsi dengan cara yang sama, jika Anda mengaktifkan BangPatternsekstensi bahasa:

f x !y = x*y

f (1+1) (2+2)akan mengembalikan thunk (1+1)*4.

cjs
sumber
16
Ini sangat membantu. Saya tidak bisa berhenti bertanya-tanya apakah terminologi Haskell membuat hal-hal lebih rumit bagi orang untuk memahami dengan istilah-istilah seperti "bentuk kepala normal yang lemah", "ketat" dan sebagainya. Jika saya mengerti Anda dengan benar, itu terdengar seperti! operator berarti menyimpan nilai yang dievaluasi dari ekspresi daripada menyimpan blok anonim untuk mengevaluasi nanti. Apakah itu interpretasi yang masuk akal atau ada sesuatu yang lebih dari itu?
David
71
@ David Pertanyaannya adalah seberapa jauh untuk mengevaluasi nilai. Bentuk normal lemah kepala berarti: mengevaluasi sampai Anda mencapai konstruktor terluar. Bentuk normal berarti mengevaluasi seluruh nilai sampai tidak ada komponen yang tidak dievaluasi yang tersisa. Karena Haskell memungkinkan untuk semua tingkat kedalaman evaluasi, ia memiliki terminologi yang kaya untuk menggambarkan hal ini. Anda tidak cenderung menemukan perbedaan ini dalam bahasa yang hanya mendukung semantik nilai-panggilan.
Don Stewart
8
@ David: Saya menulis penjelasan yang lebih mendalam di sini: Haskell: Apa itu Bentuk Normal Kepala Lemah? . Meskipun saya tidak menyebutkan pola bang atau anotasi ketat, mereka setara dengan menggunakan seq.
hammar
1
Hanya untuk memastikan saya mengerti: apakah kemalasan itu hanya terkait dengan yang tidak dikenal pada argumen waktu kompilasi? Yaitu jika kode sumber benar-benar memiliki pernyataan (2 + 2) , apakah masih akan dioptimalkan ke 4 daripada memiliki kode untuk menambahkan angka yang dikenal?
Hi-Angel
1
Hai-Malaikat, itu benar-benar sampai sejauh mana kompiler ingin mengoptimalkan. Meskipun kami menggunakan poni untuk mengatakan kami ingin memaksa hal-hal tertentu pada bentuk kepala normal yang lemah, kami tidak memiliki (dan juga tidak perlu atau menginginkan) cara untuk mengatakan "tolong pastikan Anda belum mengevaluasi yang satu ini melangkah lebih jauh." Dari sudut pandang semantik, diberi ketukan "n = 2 + 2", Anda bahkan tidak tahu apakah ada referensi tertentu ke n sedang dievaluasi sekarang atau sebelumnya dievaluasi.
cjs
92

Cara sederhana untuk melihat perbedaan antara argumen konstruktor yang ketat dan tidak ketat adalah bagaimana mereka berperilaku ketika mereka tidak terdefinisi. Diberikan

data Foo = Foo Int !Int

first (Foo x _) = x
second (Foo _ y) = y

Karena argumen non-ketat tidak dievaluasi oleh second, menyampaikan undefinedtidak menyebabkan masalah:

> second (Foo undefined 1)
1

Tetapi argumen yang ketat tidak bisa undefined, bahkan jika kita tidak menggunakan nilainya:

> first (Foo 1 undefined)
*** Exception: Prelude.undefined
Chris Conway
sumber
2
Ini lebih baik daripada jawaban Curt Sampson karena ini sebenarnya menjelaskan efek yang dapat diamati pengguna yang dimiliki !simbol, daripada menggali detail implementasi internal.
David Grayson
26

Saya percaya ini adalah penjelasan ketat.

Haskell adalah bahasa fungsional murni dan malas , tetapi kadang-kadang overhead dari kemalasan bisa terlalu banyak atau boros. Jadi untuk mengatasinya, Anda dapat meminta kompiler untuk mengevaluasi argumen sepenuhnya ke fungsi alih-alih menguraikan pemicu.

Ada informasi lebih lanjut di halaman ini: Kinerja / Ketat .

Chris Vest
sumber
Hmm, contoh pada halaman yang Anda referensikan sepertinya berbicara tentang penggunaan! saat menjalankan fungsi. Tapi itu tampaknya berbeda dari memasukkannya ke dalam tipe deklarasi. Apa yang saya lewatkan?
David
Membuat instance dari suatu tipe juga merupakan ekspresi. Anda dapat menganggap konstruktor tipe sebagai fungsi yang mengembalikan instance baru dari tipe yang ditentukan, mengingat argumen yang disediakan.
Chris Vest
4
Bahkan, untuk semua maksud dan tujuan, konstruktor tipe adalah fungsi; Anda dapat menerapkannya sebagian, meneruskannya ke fungsi lain (misalnya, map Just [1,2,3]untuk mendapatkan [Hanya 1, Hanya 2, Hanya 3]) dan seterusnya. Saya merasa terbantu untuk memikirkan kemampuan untuk menyesuaikan pola dengan mereka serta fasilitas yang sama sekali tidak terkait.
cjs