Apa aturan yang ada tentang fungsi a -> () yang dievaluasi di Haskell?

12

Sama seperti judulnya: jaminan apa yang ada untuk unit pengembalian fungsi Haskell yang akan dievaluasi? Orang akan berpikir bahwa tidak perlu menjalankan evaluasi apa pun dalam kasus seperti itu, kompilator dapat mengganti semua panggilan seperti itu dengan ()nilai langsung kecuali jika ada permintaan eksplisit untuk ketatnya, dalam hal ini kode mungkin harus memutuskan apakah harus kembali ()atau bawah.
Saya telah bereksperimen dengan ini di GHCi, dan sepertinya kebalikannya terjadi, yaitu fungsi seperti itu tampaknya dievaluasi. Contoh yang sangat primitif adalah

f :: a -> ()
f _ = undefined

Mengevaluasi f 1kesalahan melempar karena kehadiran undefined, jadi beberapa evaluasi pasti terjadi. Tidak jelas seberapa dalam evaluasi berjalan; kadang-kadang tampaknya masuk sedalam yang diperlukan untuk mengevaluasi semua panggilan ke fungsi yang kembali (). Contoh:

g :: [a] -> ()
g [] = ()
g (_:xs) = g xs

Kode ini berulang tanpa batas jika disajikan dengan g (let x = 1:x in x). Tapi kemudian

f :: a -> ()
f _ = undefined
h :: a -> ()
h _ = ()

dapat digunakan untuk menunjukkan bahwa h (f 1)pengembalian (), jadi dalam hal ini tidak semua subekspresi bernilai unit dievaluasi. Apa aturan umum di sini?

ETA: tentu saja saya tahu tentang kemalasan. Saya bertanya apa yang mencegah penulis kompiler membuat kasus khusus ini lebih malas dari biasanya.

ETA2: ringkasan dari contoh-contoh: GHC tampaknya memperlakukan ()persis seperti jenis lainnya, yaitu seolah-olah ada pertanyaan tentang nilai reguler yang menghuni jenis tersebut harus dikembalikan dari suatu fungsi. Fakta bahwa hanya ada satu nilai yang tampaknya tidak (ab) digunakan oleh algoritma optimasi.

ETA3: ketika saya mengatakan Haskell, maksud saya Haskell-sebagaimana-didefinisikan-oleh-the-Report, bukan Haskell-the-H-in-GHC. Tampaknya ada asumsi yang tidak dibagikan seluas yang saya bayangkan (yang 'oleh 100% pembaca'), atau saya mungkin bisa merumuskan pertanyaan yang lebih jelas. Meski begitu, saya menyesal mengubah judul pertanyaan, karena awalnya bertanya jaminan apa yang ada untuk fungsi yang dipanggil.

ETA4: sepertinya pertanyaan ini sudah berjalan, dan saya menganggapnya belum terjawab. (Saya sedang mencari fungsi 'pertanyaan dekat' tetapi hanya menemukan 'jawab pertanyaan Anda sendiri' dan karena tidak bisa dijawab, saya tidak turun jalan itu.) Tidak ada yang membawa apa pun dari Laporan yang akan memutuskan dengan cara apa pun , yang saya tergoda untuk menafsirkan sebagai jawaban yang kuat tetapi tidak pasti 'tidak ada jaminan untuk bahasa seperti itu'. Yang kami tahu adalah bahwa implementasi GHC saat ini tidak akan melewatkan evaluasi fungsi tersebut.

Saya mengalami masalah saat porting aplikasi OCaml ke Haskell. Aplikasi asli memiliki struktur saling rekursif dari banyak jenis, dan kode menyatakan sejumlah fungsi yang disebut assert_structureN_is_correctN dalam 1..6 atau 7, masing-masing mengembalikan unit jika struktur memang benar dan melemparkan pengecualian jika tidak . Selain itu, fungsi-fungsi ini saling memanggil karena mereka menguraikan kondisi kebenaran. Di Haskell ini lebih baik ditangani menggunakan Either Stringmonad, jadi saya menyalinnya seperti itu, tetapi pertanyaan sebagai masalah teoritis tetap ada. Terima kasih atas semua masukan dan balasan.

Kryptozoon
sumber
1
Ini kemalasan di tempat kerja. Kecuali jika hasil dari suatu fungsi diminta (misalnya dengan pencocokan pola dengan konstruktor), tubuh fungsi tidak dievaluasi. Untuk mengamati perbedaannya, cobalah membandingkan h1::()->() ; h1 () = ()dan h2::()->() ; h2 _ = (). Jalankan keduanya h1 (f 1)dan h2 (f 1), dan perhatikan bahwa hanya yang pertama yang dituntut (f 1).
chi
1
"Kemalasan tampaknya akan mendikte bahwa ia akan diganti dengan () tanpa terjadi evaluasi apa pun." Apa artinya? f 1"diganti" oleh undefineddalam semua kasus.
oisdk
3
Suatu fungsi ... -> ()dapat 1) mengakhiri dan mengembalikan (), 2) mengakhiri dengan pengecualian / kesalahan runtime dan gagal mengembalikan apa pun, atau 3) menyimpang (rekursi tak terbatas). GHC tidak mengoptimalkan kode dengan asumsi hanya 1) dapat terjadi: jika f 1diminta, ia tidak melewati evaluasi dan kembali (). Semantik Haskell adalah untuk mengevaluasi dan melihat apa yang terjadi di antara 1,2,3.
chi
2
Benar-benar tidak ada yang istimewa tentang ()(baik tipe atau nilainya) dalam pertanyaan ini. Semua pengamatan yang sama terjadi jika Anda mengganti () :: ()dengan, katakanlah, di 0 :: Intmana-mana. Ini semua hanya konsekuensi lama yang membosankan dari evaluasi malas.
Daniel Wagner
2
tidak, "menghindari" dll. bukan semantik Haskell. dan ada dua kemungkinan nilai ()tipe, ()dan undefined.
Will Ness

Jawaban:

10

Anda tampaknya berasal dari asumsi bahwa tipe ()hanya memiliki satu nilai yang mungkin (), dan dengan demikian berharap bahwa setiap panggilan fungsi yang mengembalikan nilai tipe ()harus secara otomatis diasumsikan memang menghasilkan nilai ().

Ini bukan cara Haskell bekerja. Setiap jenis memiliki satu nilai lagi di Haskell, yaitu tidak ada nilai, kesalahan, atau disebut "bawah", disandikan oleh undefined. Jadi evaluasi sebenarnya terjadi:

main = print (f 1)

setara dengan bahasa Core

main = _Case (f 1) _Of x -> print x   -- pseudocode illustration

atau bahkan (*)

main = _Case (f 1) _Of x -> putStr "()"

dan Core _Caseadalah memaksa :

"Mengevaluasi %case[ekspresi] memaksa evaluasi ekspresi yang diuji (" scrutinee "). Nilai scrutinee terikat pada variabel yang mengikuti %ofkata kunci, ...".

Nilai dipaksa untuk bentuk kepala normal lemah. Ini adalah bagian dari definisi bahasa.

Haskell adalah tidak sebuah deklaratif bahasa pemrograman.


(*) print x = putStr (show x) dan show () = "()", sehingga showpanggilan dapat dikompilasi sama sekali.

Nilai tersebut memang dikenal di muka sebagai (), dan bahkan nilai show ()tersebut dikenal di muka sebagai "()". Namun semantik Haskell yang diterima menuntut bahwa nilai (f 1)dipaksa ke bentuk kepala normal sebelum melanjutkan untuk mencetak yang dikenal di string terlebih dahulu "()",.


sunting: Pertimbangkan concat (repeat []). Haruskah itu [], atau haruskah itu loop tak terbatas?

Jawaban "bahasa deklaratif" kemungkinan besar adalah untuk ini []. Jawaban Haskell adalah, infinite loop .

Kemalasan adalah "pemrograman deklaratif orang miskin", tetapi itu masih bukan hal yang nyata .

sunting2 : print $ h (f 1) == _Case (h (f 1)) _Of () -> print ()dan hanya hdipaksa, tidak f; dan untuk menghasilkan yang jawabannya htidak harus memaksakan apapun, menurut definisi h _ = ().

kata perpisahan: Kemalasan mungkin memiliki raison d'etre tapi itu bukan definisi. Kemalasan adalah apa adanya. Hal ini didefinisikan sebagai semua nilai yang awalnya merupakan thunks yang dipaksa untuk WHNF sesuai dengan permintaan yang datang dari main. Jika itu membantu menghindari bottom dalam kasus spesifik tertentu sesuai dengan keadaan spesifiknya, maka itu membantu. Jika tidak, tidak. Itu semuanya.

Ini membantu untuk mengimplementasikannya sendiri, dalam bahasa favorit Anda, untuk merasakannya. Tetapi kita juga dapat melacak evaluasi ekspresi apa pun dengan menyebutkan semua nilai sementara dengan hati-hati .


Mengikuti Laporan , kami punya

f :: a -> ()
f = \_ -> (undefined :: ())

kemudian

print (f 1)
 = print ((\ _ ->  undefined :: ()) 1)
 = print          (undefined :: ())
 = putStrLn (show (undefined :: ()))

dan dengan

instance Show () where
    show :: () -> String
    show x = case x of () -> "()"

itu berlanjut

 = putStrLn (case (undefined :: ()) of () -> "()")

Sekarang, bagian 3.17.3 Kata Semantik Resmi dari Pola yang Cocok dari Laporan mengatakan

Semantik caseungkapan [diberikan] pada Gambar 3.1-3.3. Setiap implementasi harus berperilaku sehingga identitas-identitas ini berlaku [...].

dan kasus (r)pada Gambar 3.2 menyatakan

(r)     case  of { K x1  xn -> e; _ -> e } =  
        where K is a data constructor of arity n 

() adalah konstruktor data arity 0, jadi itu sama dengan

(r)     case  of { () -> e; _ -> e } =  

dan hasil keseluruhan dari evaluasi adalah demikian .

Will Ness
sumber
2
Saya suka penjelasan Anda. Itu jelas dan sederhana.
panah
@DanielWagner sebenarnya ada dalam pikiran saya casedari Core, dan mengabaikan lubang menganga. :) Saya sudah mengedit untuk menyebutkan Core.
Will Ness
1
Tidakkah pemaksaan dilakukan showoleh print? Sesuatu sepertishow x = case x of () -> "()"
user253751
1
Saya merujuk casepada Core, bukan di Haskell itu sendiri. Haskell diterjemahkan ke dalam Core, yang memiliki pemaksaan case, AFAIK. Anda benar bahwa casedi Haskell tidak memaksa dengan sendirinya. Saya bisa menulis sesuatu dalam Skema atau ML (jika saya bisa menulis ML itu), atau pseudocode.
Will Ness
1
Jawaban otoritatif untuk semua ini mungkin ada di suatu tempat dalam Laporan. Yang saya tahu adalah tidak ada "optimasi" yang terjadi di sini, dan "nilai reguler" bukanlah istilah yang bermakna dalam konteks ini. Apa pun yang dipaksakan, dipaksakan. printPasukan sebanyak yang dibutuhkan untuk mencetak. itu tidak melihat jenisnya, jenisnya hilang, terhapus, pada saat program berjalan, subrutin pencetakan yang benar sudah dipilih dan dikompilasi, sesuai dengan jenisnya, pada waktu kompilasi; bahwa subrutin masih akan memaksakan nilai inputnya ke WHNF pada saat run time, dan jika itu tidak didefinisikan, itu akan menyebabkan kesalahan.
Will Ness