Apakah menangkap / melempar pengecualian membuat metode yang murni murni?

27

Contoh kode berikut memberikan konteks untuk pertanyaan saya.

Kelas Kamar diinisialisasi dengan delegasi. Dalam implementasi pertama dari kelas Ruangan, tidak ada penjaga terhadap delegasi yang melempar pengecualian. Pengecualian seperti itu akan muncul ke properti North, di mana delegasi dievaluasi (catatan: Main () metode menunjukkan bagaimana instance Kamar digunakan dalam kode klien):

public sealed class Room
{
    private readonly Func<Room> north;

    public Room(Func<Room> north)
    {
        this.north = north;
    }

    public Room North
    {
        get
        {
            return this.north();
        }
    }

    public static void Main(string[] args)
    {
        Func<Room> evilDelegate = () => { throw new Exception(); };

        var kitchen = new Room(north: evilDelegate);

        var room = kitchen.North; //<----this will throw

    }
}

Menjadi bahwa saya lebih suka gagal pada pembuatan objek daripada ketika membaca properti Utara, saya mengubah konstruktor menjadi pribadi, dan memperkenalkan metode pabrik statis bernama Buat (). Metode ini menangkap pengecualian yang dilemparkan oleh delegasi, dan melemparkan pengecualian pembungkus, memiliki pesan pengecualian yang bermakna:

public sealed class Room
{
    private readonly Func<Room> north;

    private Room(Func<Room> north)
    {
        this.north = north;
    }

    public Room North
    {
        get
        {
            return this.north();
        }
    }

    public static Room Create(Func<Room> north)
    {
        try
        {
            north?.Invoke();
        }
        catch (Exception e)
        {
            throw new Exception(
              message: "Initialized with an evil delegate!", innerException: e);
        }

        return new Room(north);
    }

    public static void Main(string[] args)
    {
        Func<Room> evilDelegate = () => { throw new Exception(); };

        var kitchen = Room.Create(north: evilDelegate); //<----this will throw

        var room = kitchen.North;
    }
}

Apakah blok try-catch membuat metode Create () tidak murni?

Rock Anthony Johnson
sumber
1
Apa manfaat dari fungsi Room Create; jika Anda menggunakan delegasi mengapa memanggilnya sebelum klien memanggil 'Utara'?
SH-
1
@ SS Jika delegasi melempar pengecualian, saya ingin mencari tahu saat membuat objek Room. Saya tidak ingin klien menemukan pengecualian pada penggunaan, tetapi lebih pada penciptaan. Metode Buat adalah tempat yang sempurna untuk mengekspos delegasi jahat.
Rock Anthony Johnson
3
@RockAnthonyJohnson, Ya. Apa yang Anda lakukan ketika menjalankan delegasi hanya dapat bekerja pertama kali, atau mengembalikan kamar berbeda pada panggilan kedua? Memanggil delegasi dapat dianggap / menyebabkan efek samping itu sendiri?
SH-
4
Fungsi yang memanggil fungsi tidak murni adalah fungsi tidak murni, jadi jika delegasi tidak murni Createjuga tidak murni, karena ia memanggilnya.
Idan Arye
2
Anda Createfungsi tidak melindungi Anda dari mendapatkan pengecualian ketika mendapatkan properti. Jika delegasi Anda melempar, dalam kehidupan nyata sangat mungkin bahwa itu akan dilemparkan hanya dalam beberapa kondisi. Kemungkinannya adalah kondisi untuk melempar tidak ada selama konstruksi, tetapi mereka ada saat mendapatkan properti.
Bart van Ingen Schenau

Jawaban:

26

Iya nih. Itu secara efektif merupakan fungsi yang tidak murni. Itu menciptakan efek samping: eksekusi program berlanjut di suatu tempat selain dari tempat fungsi diharapkan kembali.

Untuk menjadikannya fungsi murni, returnobjek aktual yang merangkum nilai yang diharapkan dari fungsi dan nilai yang menunjukkan kemungkinan kondisi kesalahan, seperti Maybeobjek atau objek Unit Kerja.

Robert Harvey
sumber
16
Ingat, "murni" hanyalah sebuah definisi. Jika Anda membutuhkan fungsi yang melempar, tetapi dengan segala cara itu bersifat referensial transparan, maka itulah yang Anda tulis.
Robert Harvey
1
Dalam haskell setidaknya fungsi apa pun bisa melempar tetapi Anda harus berada di IO untuk menangkap, fungsi murni yang terkadang melempar biasanya disebut parsial
jk.
4
Saya memiliki pencerahan karena komentar Anda. Sementara saya secara intelektual tertarik dengan kemurnian, apa yang sebenarnya saya butuhkan dalam kepraktisan adalah determinisme dan transparansi yang terhormat. Jika saya mencapai kemurnian, itu hanya bonus tambahan. Terima kasih. FYI, hal yang mendorong saya ke jalur ini adalah fakta bahwa Microsoft IntelliTest hanya efektif terhadap kode yang bersifat deterministik.
Rock Anthony Johnson
4
@RockAnthonyJohnson: Ya, semua kode harus bersifat deterministik, atau setidak-tidaknya set deterministik mungkin. Transparansi referensial hanyalah salah satu cara untuk mendapatkan determinisme itu. Beberapa teknik, seperti utas, memiliki kecenderungan untuk bekerja melawan determinisme itu, sehingga ada teknik lain seperti Tugas yang meningkatkan kecenderungan non-deterministik model threading. Hal-hal seperti generator bilangan acak dan simulator Monte-Carlo juga bersifat deterministik, tetapi dengan cara yang berbeda berdasarkan probabilitas.
Robert Harvey
3
@zzzzBov: Saya tidak menganggap definisi "murni" yang sebenarnya, sangat penting. Lihat komentar kedua, di atas.
Robert Harvey
12

Ya, ya ... dan tidak.

Fungsi murni harus memiliki Transparansi Referensial - yaitu, Anda harus dapat mengganti panggilan yang mungkin ke fungsi murni dengan nilai yang dikembalikan, tanpa mengubah perilaku program. * Fungsi Anda dijamin untuk selalu melemparkan argumen tertentu, jadi tidak ada nilai balik untuk mengganti panggilan fungsi dengan, jadi alih-alih mari kita abaikan saja. Pertimbangkan kode ini:

{
    var ignoreThis = func(arg);
}

Jika funcmurni, pengoptimal dapat memutuskan yang func(arg)dapat diganti dengan hasilnya. Belum tahu apa hasilnya, tetapi dapat mengatakan bahwa itu tidak digunakan - jadi hanya dapat menyimpulkan pernyataan ini tidak memiliki efek dan menghapusnya.

Tetapi jika func(arg)kebetulan melempar, pernyataan ini memang melakukan sesuatu - itu melempar pengecualian! Jadi pengoptimal tidak dapat menghapusnya - tidak masalah jika fungsi dipanggil atau tidak.

Tapi...

Dalam praktiknya, ini sangat kecil artinya. Pengecualian - setidaknya dalam C # - adalah sesuatu yang luar biasa. Anda tidak seharusnya menggunakannya sebagai bagian dari aliran kontrol reguler Anda - Anda seharusnya mencoba dan menangkapnya, dan jika Anda menangkap sesuatu menangani kesalahan untuk mengembalikan apa yang Anda lakukan atau entah bagaimana masih menyelesaikannya. Jika program Anda tidak berfungsi sebagaimana mestinya karena kode yang gagal gagal telah dioptimalkan, Anda menggunakan pengecualian yang salah (kecuali jika itu adalah kode pengujian, dan ketika Anda membuat pengecualian untuk pengujian tidak boleh dioptimalkan).

Yang telah dibilang...

Jangan membuang pengecualian dari fungsi murni dengan maksud bahwa mereka akan ditangkap - ada alasan bagus mengapa bahasa fungsional lebih memilih untuk menggunakan monad daripada pengecualian stack-unwinding-exception.

Jika C # memiliki Errorkelas seperti Java (dan banyak bahasa lainnya), saya akan menyarankan untuk melempar Errorbukan Exception. Ini menunjukkan bahwa pengguna fungsi melakukan sesuatu yang salah (melewati fungsi yang melempar), dan hal-hal seperti itu diperbolehkan dalam fungsi murni. Tetapi C # tidak memiliki Errorkelas, dan pengecualian kesalahan penggunaan tampaknya berasal dari Exception. Saran saya adalah melempar ArgumentException, memperjelas bahwa fungsi itu dipanggil dengan argumen yang buruk.


* Berbicara secara komputasi. Fungsi Fibonacci yang diimplementasikan menggunakan rekursi naif akan membutuhkan waktu lama untuk jumlah besar, dan dapat menghabiskan sumber daya mesin, tetapi ini karena dengan waktu dan memori tanpa batas fungsi akan selalu mengembalikan nilai yang sama dan tidak akan memiliki efek samping (selain mengalokasikan memori dan mengubah memori itu) - itu masih dianggap murni.

Idan Arye
sumber
1
+1 Untuk penggunaan ArgumentException yang Anda sarankan, dan agar rekomendasi Anda tidak "membuang pengecualian dari fungsi murni dengan maksud bahwa mereka akan ditangkap".
Rock Anthony Johnson
Argumen pertama Anda (sampai "Tapi ...") tampaknya tidak sehat bagi saya. Pengecualian adalah bagian dari kontrak fungsi. Anda dapat, secara konseptual, menulis ulang fungsi apa pun sebagai (value, exception) = func(arg)dan menghapus kemungkinan melempar pengecualian (dalam beberapa bahasa dan untuk beberapa jenis pengecualian Anda benar-benar perlu mencantumkannya dengan definisi, sama seperti argumen atau nilai pengembalian). Sekarang akan sama murni seperti sebelumnya (asalkan selalu kembali alias melempar pengecualian yang diberikan argumen yang sama). Melempar pengecualian bukanlah efek samping, jika Anda menggunakan interpretasi ini.
AnoE
@AnoE Jika Anda akan menulis funcseperti itu, blok yang saya berikan bisa saja dioptimalkan, karena alih-alih membuka tumpukan, pengecualian yang dilempar akan dikodekan ignoreThis, yang kita abaikan. Tidak seperti pengecualian yang melepaskan tumpukan, pengecualian yang dikodekan dalam jenis kembali tidak melanggar kemurnian - dan itulah sebabnya banyak bahasa fungsional lebih suka menggunakannya.
Idan Arye
Tepat ... Anda tidak akan mengabaikan pengecualian untuk transformasi yang dibayangkan ini. Saya kira itu semua sedikit masalah semantik / definisi, saya hanya ingin menunjukkan bahwa keberadaan atau kejadian pengecualian tidak serta merta membatalkan semua konsep kemurnian.
AnoE
1
@AnoE Tapi kode yang saya kirimkan akan mengabaikan pengecualian untuk transformasi yang dibayangkan ini. func(arg)akan mengembalikan tuple (<junk>, TheException()), dan juga ignoreThisakan berisi tuple (<junk>, TheException()), tetapi karena kita tidak pernah menggunakan ignoreThispengecualian tidak akan berpengaruh pada apa pun.
Idan Arye
1

Salah satu pertimbangannya adalah bahwa blok try-catch bukanlah masalahnya. (Berdasarkan komentar saya untuk pertanyaan di atas).

Masalah utama adalah bahwa properti Utara adalah panggilan I / O .

Pada titik itu dalam eksekusi kode, program perlu memeriksa I / O yang disediakan oleh kode klien. (Tidak akan relevan bahwa input tersebut dalam bentuk delegasi, atau bahwa inputnya, secara nominal, sudah masuk).

Setelah Anda kehilangan kendali atas input, Anda tidak dapat memastikan fungsinya murni. (Apalagi jika fungsinya bisa melempar).


Saya tidak jelas mengapa Anda tidak ingin memeriksa panggilan untuk Pindah Kamar [Periksa]? Sesuai komentar saya untuk pertanyaan:

Iya nih. Apa yang Anda lakukan ketika menjalankan delegasi hanya dapat bekerja pertama kali, atau mengembalikan kamar berbeda pada panggilan kedua? Memanggil delegasi dapat dianggap / menyebabkan efek samping itu sendiri?

Seperti yang dikatakan Bart van Ingen Schenau di atas,

Fungsi Buat Anda tidak melindungi Anda dari mendapatkan pengecualian saat mendapatkan properti. Jika delegasi Anda melempar, dalam kehidupan nyata sangat mungkin bahwa itu akan dilemparkan hanya dalam beberapa kondisi. Kemungkinannya adalah kondisi untuk melempar tidak ada selama konstruksi, tetapi mereka ada saat mendapatkan properti.

Secara umum, segala jenis pemuatan malas secara implisit akan mempertahankan kesalahan hingga saat itu.


Saya akan menyarankan menggunakan metode Pindah [Periksa] Kamar. Ini akan memungkinkan Anda untuk memisahkan aspek I / O yang tidak murni menjadi satu tempat.

Mirip dengan jawaban Robert Harvey :

Untuk menjadikannya fungsi murni, kembalikan objek aktual yang merangkum nilai yang diharapkan dari fungsi dan nilai yang mengindikasikan kemungkinan kondisi kesalahan, seperti objek Mungkin atau objek Unit Kerja.

Akan tergantung pada penulis kode untuk menentukan bagaimana menangani pengecualian (mungkin) dari input. Kemudian metode ini dapat mengembalikan objek Kamar, atau objek Ruang Null, atau mungkin mengeluarkan pengecualian.

Pada titik ini tergantung pada:

  • Apakah domain Kamar memperlakukan Pengecualian Kamar sebagai Null atau Sesuatu Lebih Buruk.
  • Bagaimana cara memberi tahu kode klien yang memanggil Utara di Ruang Null / Exception. (Jaminan / Variabel Status / Status Global / Kembalikan Monad / Apa Pun; Beberapa lebih murni daripada yang lain :)).
SH-
sumber
-1

Hmm ... Saya merasa tidak benar tentang ini. Saya pikir apa yang Anda coba lakukan adalah memastikan orang lain melakukan pekerjaan mereka dengan benar, tanpa mengetahui apa yang mereka lakukan.

Karena fungsi ini dilewatkan oleh konsumen, yang dapat ditulis oleh Anda atau oleh orang lain.

Bagaimana jika fungsi pass in tidak valid untuk dijalankan pada saat objek Room dibuat?

Dari kode itu, saya tidak bisa memastikan apa yang dilakukan Utara.

Katakanlah, jika fungsinya untuk memesan kamar, dan itu membutuhkan waktu dan periode pemesanan, maka Anda akan mendapatkan pengecualian jika Anda belum memiliki informasi yang siap.

Bagaimana jika itu adalah fungsi yang berjalan lama? Anda tidak akan ingin memblokir program Anda saat membuat objek Kamar, bagaimana jika Anda perlu membuat objek 1000 Kamar?

Jika Anda adalah orang yang akan menulis konsumen yang mengkonsumsi kelas ini, Anda akan memastikan fungsi yang diteruskan ditulis dengan benar, bukan?

Jika ada orang lain yang menulis konsumen, dia mungkin tidak tahu kondisi "khusus" membuat objek Kamar, yang dapat menyebabkan eksekusi dan mereka akan menggaruk-garuk kepala mereka mencoba mencari tahu mengapa.

Hal lain adalah, tidak selalu merupakan hal yang baik untuk melemparkan pengecualian baru. Alasannya adalah tidak akan berisi informasi pengecualian asli. Anda tahu Anda mendapatkan kesalahan (mesaage ramah untuk pengecualian umum), tetapi Anda tidak akan tahu apa kesalahannya (referensi nol, indeks di luar batas, bagi dengan nol dll). Informasi ini sangat penting ketika kita perlu melakukan penyelidikan.

Anda harus menangani pengecualian hanya ketika Anda tahu apa dan bagaimana menanganinya.

Saya pikir kode asli Anda cukup baik, satu-satunya hal yang Anda mungkin ingin menangani pengecualian pada "Main" (atau lapisan apa pun yang tahu cara menanganinya).

pengguna251630
sumber
1
Lihat kembali contoh kode ke-2; Saya menginisialisasi pengecualian baru dengan pengecualian bawah. Seluruh jejak tumpukan dipertahankan.
Rock Anthony Johnson
Ups. Kesalahanku.
user251630
-1

Saya akan tidak setuju dengan jawaban lain dan mengatakan TIDAK . Pengecualian tidak menyebabkan fungsi murni menjadi tidak murni.

Setiap penggunaan pengecualian dapat ditulis ulang untuk menggunakan pemeriksaan eksplisit untuk hasil kesalahan. Pengecualian hanya bisa dianggap sebagai gula sintaksis di atas ini. Gula sintaksis yang mudah digunakan tidak membuat kode murni tidak murni.

JacquesB
sumber
1
Fakta bahwa Anda dapat menulis ulang pengotoran tidak membuat fungsi menjadi murni. Haskell adalah bukti bahwa penggunaan fungsi-fungsi tidak murni dapat ditulis ulang dengan fungsi-fungsi murni - ia menggunakan monad untuk menyandikan perilaku tidak murni apapun dalam tipe-tipe murni yang kembali fungsi murni. Keberadaan IO monads tidak menyiratkan bahwa IO reguler adalah murni - mengapa keberadaan pengecualian monads menyiratkan bahwa pengecualian reguler adalah murni?
Idan Arye
@IdanArye: Saya berpendapat tidak ada kenajisan di tempat pertama. Contoh penulisan ulang hanya untuk menunjukkan hal itu. IO reguler tidak murni karena menyebabkan efek samping yang dapat diamati, atau dapat mengembalikan nilai yang berbeda dengan input yang sama karena beberapa input eksternal. Melempar pengecualian tidak melakukan hal-hal itu.
JacquesB
Kode bebas efek samping tidak cukup. Transparansi Referensial juga merupakan persyaratan untuk kemurnian fungsi.
Idan Arye
1
Determinisme tidak cukup untuk transparansi referensial - efek samping juga dapat bersifat deterministik. Transparansi referensial berarti bahwa ekspresi dapat diganti dengan hasilnya - jadi tidak peduli berapa kali Anda mengevaluasi ekspresi transparan referensial, dalam urutan apa, dan apakah Anda mengevaluasinya sama sekali - hasilnya harus sama. Jika pengecualian dilemparkan secara deterministik, kami baik dengan kriteria "tidak peduli berapa kali", tetapi tidak dengan yang lainnya. Saya telah berdebat tentang kriteria "apakah atau tidak" dalam jawaban saya sendiri, (lanjutan ...)
Idan Arye
1
(... lanjutan) dan untuk "dalam urutan apa" - jika fdan gkeduanya merupakan fungsi murni, maka f(x) + g(x)harus memiliki hasil yang sama terlepas dari urutan yang Anda evaluasi f(x)dan g(x). Tetapi jika f(x)melempar Fdan g(x)melempar G, maka pengecualian yang dilemparkan dari ungkapan ini adalah Fjika f(x)dievaluasi terlebih dahulu dan Gjika g(x)dievaluasi dulu - ini bukan bagaimana fungsi murni seharusnya berperilaku! Ketika pengecualian dikodekan dalam tipe kembali, hasil itu akan berakhir seperti sesuatu yang Error(F) + Error(G)terlepas dari urutan evaluasi.
Idan Arye