Penanganan pengecualian gaya fungsional

24

Saya telah diberitahu bahwa dalam pemrograman fungsional seseorang tidak seharusnya melempar dan / atau mengamati pengecualian. Sebaliknya perhitungan yang salah harus dievaluasi sebagai nilai dasar. Dalam Python (atau bahasa lain yang tidak sepenuhnya mendorong pemrograman fungsional) seseorang dapat kembali None(atau alternatif lain yang diperlakukan sebagai nilai dasar, meskipun Nonetidak sepenuhnya mematuhi definisi) setiap kali ada yang salah dengan "tetap murni", tetapi untuk melakukan jadi kita harus mengamati kesalahan di tempat pertama, yaitu

def fn(*args):
    try:
        ... do something
    except SomeException:
        return None

Apakah ini melanggar kemurnian? Dan jika demikian, apakah ini berarti, bahwa tidak mungkin untuk menangani kesalahan murni dengan Python?

Memperbarui

Dalam komentarnya Eric Lippert mengingatkan saya pada cara lain untuk memperlakukan pengecualian di FP. Meskipun saya belum pernah melihat itu dilakukan dalam Python dalam praktek, saya bermain dengannya ketika saya belajar FP setahun yang lalu. Di sini, optionalfungsi -decorated mengembalikan Optionalnilai, yang bisa kosong, untuk output normal serta untuk daftar pengecualian yang ditentukan (pengecualian yang tidak ditentukan masih dapat menghentikan eksekusi). Carrymembuat evaluasi tertunda, di mana setiap langkah (panggilan fungsi tertunda) baik mendapatkan Optionaloutput kosong dari langkah sebelumnya dan hanya meneruskannya, atau sebaliknya mengevaluasi sendiri melewati baru Optional. Pada akhirnya nilai akhirnya adalah normal atau Empty. Di sini, try/exceptblok disembunyikan di belakang dekorator, sehingga pengecualian yang ditentukan dapat dianggap sebagai bagian dari tanda tangan jenis pengembalian.

class Empty:
    def __repr__(self):
        return "Empty"


class Optional:
    def __init__(self, value=Empty):
        self._value = value

    @property
    def value(self):
        return Empty if self.isempty else self._value

    @property
    def isempty(self):
        return isinstance(self._value, BaseException) or self._value is Empty

    def __bool__(self):
        raise TypeError("Optional has no boolean value")


def optional(*exception_types):
    def build_wrapper(func):
        def wrapper(*args, **kwargs):
            try:
                return Optional(func(*args, **kwargs))
            except exception_types as e:
                return Optional(e)
        wrapper.__isoptional__ = True
        return wrapper
    return build_wrapper


class Carry:
    """
    >>> from functools import partial
    >>> @optional(ArithmeticError)
    ... def rdiv(a, b):
    ...     return b // a
    >>> (Carry() >> (rdiv, 0) >> (rdiv, 0) >> partial(rdiv, 1))(1)
    1
    >>> (Carry() >> (rdiv, 0) >> (rdiv, 1))(1)
    1
    >>> (Carry() >> rdiv >> rdiv)(0, 1) is Empty
    True
    """
    def __init__(self, steps=None):
        self._steps = tuple(steps) if steps is not None else ()

    def _add_step(self, step):
        fn, *step_args = step if isinstance(step, Sequence) else (step, )
        return type(self)(steps=self._steps + ((fn, step_args), ))

    def __rshift__(self, step) -> "Carry":
        return self._add_step(step)

    def _evaluate(self, *args) -> Optional:
        def caller(carried: Optional, step):
            fn, step_args = step
            return fn(*(*step_args, *args)) if carried.isempty else carried
        return reduce(caller, self._steps, Optional())

    def __call__(self, *args):
        return self._evaluate(*args).value
Eli Korvigo
sumber
1
Pertanyaan Anda telah dijawab, jadi hanya komentar: apakah Anda mengerti mengapa fungsi Anda melempar pengecualian tidak disukai dalam pemrograman fungsional? Ini bukan kemauan yang sewenang-wenang :)
Andres F.
6
Ada alternatif lain untuk mengembalikan nilai yang menunjukkan kesalahan. Ingat, penanganan pengecualian adalah aliran kontrol dan kami memiliki mekanisme fungsional untuk menentukan aliran kontrol. Anda bisa meniru penanganan pengecualian dalam bahasa fungsional dengan menulis metode Anda untuk mengambil dua fungsi: kelanjutan keberhasilan dan kelanjutan kesalahan . Hal terakhir yang fungsi Anda lakukan adalah memanggil kelanjutan sukses, melewati "hasil", atau kelanjutan kesalahan, melewati "pengecualian". Sisi buruknya adalah Anda harus menulis program dengan gaya luar-dalam.
Eric Lippert
3
Tidak, bagaimana keadaannya dalam kasus ini? Ada beberapa masalah, tetapi ada beberapa: 1- ada satu kemungkinan aliran yang tidak dikodekan dalam jenisnya, yaitu Anda tidak dapat mengetahui apakah suatu fungsi akan melempar pengecualian hanya dengan melihat jenisnya (kecuali jika Anda baru saja memeriksa pengecualian, tentu saja, tapi saya tidak tahu bahasa apa pun yang hanya memiliki mereka). Anda secara efektif bekerja "di luar" sistem jenis, 2- programmer fungsional berusaha untuk menulis fungsi "total" bila memungkinkan, yaitu fungsi yang mengembalikan nilai untuk setiap input (kecuali non-terminasi). Pengecualian berlaku untuk ini.
Andres F.
3
3 - saat Anda bekerja dengan fungsi total, Anda dapat menyusunnya dengan fungsi lain dan menggunakannya dalam fungsi tingkat tinggi tanpa khawatir tentang hasil kesalahan "tidak dikodekan dalam tipe", yaitu pengecualian.
Andres F.
1
@EliKorvigo Mengatur variabel memperkenalkan status, menurut definisi, berhenti penuh. Seluruh tujuan dari variabel adalah mereka memiliki status. Itu tidak ada hubungannya dengan pengecualian. Mengamati nilai kembalinya suatu fungsi dan menetapkan nilai suatu variabel berdasarkan pengamatan akan memperkenalkan keadaan ke dalam evaluasi, bukan?
user253751

Jawaban:

20

Pertama-tama, mari kita selesaikan beberapa kesalahpahaman. Tidak ada "nilai bawah". Tipe bawah didefinisikan sebagai tipe yang merupakan subtipe dari setiap tipe lain dalam bahasa. Dari ini, orang dapat membuktikan (dalam sistem tipe apa pun yang menarik setidaknya), bahwa tipe bawah tidak memiliki nilai - itu kosong . Jadi tidak ada yang namanya nilai dasar.

Mengapa tipe dasar berguna? Nah, mengetahui bahwa itu kosong, mari kita membuat beberapa pengurangan pada perilaku program. Misalnya, jika kita memiliki fungsi:

def do_thing(a: int) -> Bottom: ...

kita tahu bahwa do_thingtidak pernah bisa kembali, karena itu harus mengembalikan nilai tipeBottom . Dengan demikian, hanya ada dua kemungkinan:

  1. do_thing tidak berhenti
  2. do_thing melempar pengecualian (dalam bahasa dengan mekanisme pengecualian)

Perhatikan bahwa saya membuat jenis Bottomyang sebenarnya tidak ada dalam bahasa Python. Noneadalah keliru; itu sebenarnya nilai unit , satu-satunya nilai dari tipe unit , yang disebut NoneTypedengan Python (lakukan type(None)untuk mengonfirmasi sendiri).

Sekarang, kesalahpahaman lain adalah bahwa bahasa fungsional tidak memiliki pengecualian. Ini juga tidak benar. SML misalnya memiliki mekanisme pengecualian yang sangat bagus. Namun, pengecualian digunakan lebih hemat di SML daripada di misalnya Python. Seperti yang telah Anda katakan, cara umum untuk menunjukkan beberapa jenis kegagalan dalam bahasa fungsional adalah dengan mengembalikan suatu Optionjenis. Misalnya, kami akan membuat fungsi pembagian aman sebagai berikut:

def safe_div(num: int, den: int) -> Option[int]:
  return Some(num/den) if den != 0 else None

Sayangnya, karena Python sebenarnya tidak memiliki tipe penjumlahan, ini bukan pendekatan yang layak. Anda dapat kembali Nonesebagai tipe opsi orang miskin untuk menunjukkan kegagalan, tetapi ini benar-benar tidak lebih baik daripada kembali Null. Tidak ada keamanan jenis.

Jadi saya akan menyarankan mengikuti konvensi bahasa dalam kasus ini. Python menggunakan pengecualian secara idiomatis untuk menangani aliran kontrol (yang merupakan desain yang buruk, IMO, tetapi tetap standar), jadi kecuali Anda hanya bekerja dengan kode yang Anda tulis sendiri, saya akan merekomendasikan mengikuti praktik standar. Apakah ini "murni" atau tidak tidak relevan.

kepala kebun
sumber
Dengan "nilai bawah" Sebenarnya saya maksud "tipe bawah", itu sebabnya saya menulis yang Nonetidak sesuai dengan definisi. Lagi pula, terima kasih sudah mengoreksi saya. Tidakkah Anda berpikir bahwa menggunakan pengecualian hanya untuk menghentikan eksekusi sepenuhnya atau mengembalikan nilai opsional tidak masalah dengan prinsip Python? Maksudku, mengapa tidak bisa menggunakan pengecualian untuk kontrol yang rumit?
Eli Korvigo
@EliKorvigo Itulah yang kurang lebih seperti yang saya katakan, kan? Pengecualian adalah Python idiomatik.
Gardenhead
1
Sebagai contoh, saya mengecilkan hati mahasiswa sarjana saya untuk menggunakan try/except/finallyseperti alternatif lain if/else, yaitu try: var = expession1; except ...: var = expression 2; except ...: var = expression 3..., meskipun itu adalah hal yang umum untuk dilakukan dalam bahasa imperatif (btw, saya sangat tidak menyarankan menggunakan if/elseblok untuk ini juga). Apakah maksud Anda, bahwa saya tidak masuk akal dan harus membiarkan pola seperti itu karena "ini adalah Python"?
Eli Korvigo
@EliKorvigo Saya setuju dengan Anda secara umum (btw, Anda seorang profesor?). try... catch...seharusnya tidak digunakan untuk aliran kontrol. Entah mengapa, komunitas Python memutuskan untuk melakukan banyak hal. Sebagai contoh, safe_divfungsi yang saya tulis di atas biasanya akan ditulis try: result = num / div: except ArithmeticError: result = None. Jadi jika Anda mengajari mereka prinsip-prinsip rekayasa perangkat lunak umum, Anda harus mencegahnya. if ... else...juga bau kode, tapi itu terlalu lama untuk masuk ke sini.
Gardenhead
2
Ada "nilai dasar" (itu digunakan dalam berbicara tentang semantik Haskell, misalnya), dan itu tidak ada hubungannya dengan tipe bawah. Jadi itu bukan kesalahpahaman tentang OP, hanya berbicara tentang hal yang berbeda.
Ben
11

Karena ada begitu banyak minat pada kemurnian selama beberapa hari terakhir, mengapa kita tidak memeriksa seperti apa fungsi murni itu.

Fungsi murni:

  • Apakah transparan referensial; yaitu, untuk input yang diberikan, ia akan selalu menghasilkan output yang sama.

  • Tidak menghasilkan efek samping; itu tidak mengubah input, output, atau apa pun di lingkungan eksternal. Itu hanya menghasilkan nilai kembali.

Jadi tanyakan pada diri sendiri. Apakah fungsi Anda melakukan sesuatu selain menerima input dan mengembalikan output?

Robert Harvey
sumber
3
Jadi, tidak masalah, seberapa jelekkah itu tertulis di dalamnya asalkan berperilaku secara fungsional?
Eli Korvigo
8
Benar, jika Anda hanya tertarik pada kemurnian. Mungkin, tentu saja, ada hal-hal lain yang perlu dipertimbangkan.
Robert Harvey
3
Untuk berperan sebagai pendukung setan, saya berpendapat bahwa melempar pengecualian hanyalah bentuk output dan fungsi yang melempar adalah murni. Apakah itu masalah?
Bergi
1
@Bergi Saya tidak tahu bahwa Anda berperan sebagai advokat iblis, karena itulah yang dimaksud oleh jawaban ini :) Masalahnya adalah bahwa ada pertimbangan lain selain kesucian. Jika Anda mengizinkan pengecualian yang tidak dicentang (yang menurut definisi bukan bagian dari tanda tangan fungsi) maka jenis pengembalian setiap fungsi secara efektif menjadi T + { Exception }(di mana Tadalah jenis pengembalian yang dinyatakan secara eksplisit), yang bermasalah. Anda tidak dapat mengetahui apakah suatu fungsi akan melempar pengecualian tanpa melihat kode sumbernya, yang membuat penulisan fungsi urutan yang lebih tinggi juga bermasalah.
Andres F.
2
@Begri sambil melempar mungkin bisa dibilang murni, IMO menggunakan pengecualian tidak lebih dari sekadar mempersulit jenis pengembalian setiap fungsi. Ini merusak kompabilitas fungsi. Pertimbangkan implementasi di map : (A->B)->List A ->List Bmana A->Bkesalahan bisa terjadi. Jika kita mengizinkan f untuk melemparkan eksepsi map f L akan mengembalikan sesuatu yang bertipe Exception + List<B>. Jika sebaliknya kami mengizinkannya untuk mengembalikan optionaljenis gaya, map f Lsebagai gantinya akan mengembalikan Daftar <Opsional <B>> `. Opsi kedua ini terasa lebih fungsional bagi saya.
Michael Anderson
7

Semantik Haskell menggunakan "nilai terbawah" untuk menganalisis makna kode Haskell. Ini bukan sesuatu yang benar-benar Anda gunakan langsung dalam pemrograman Haskell, dan kembaliNone sama sekali bukan hal yang sama.

Nilai terbawah adalah nilai yang ditentukan oleh semantik Haskell untuk setiap perhitungan yang gagal mengevaluasi ke suatu nilai secara normal. Salah satu cara yang dapat dilakukan komputasi Haskell adalah dengan melemparkan pengecualian! Jadi jika Anda mencoba menggunakan gaya ini di Python, Anda sebenarnya harus membuang pengecualian seperti biasa.

Semantik Haskell menggunakan nilai terbawah karena Haskell malas; Anda dapat memanipulasi "nilai" yang dikembalikan oleh perhitungan yang belum benar-benar berjalan. Anda dapat meneruskannya ke fungsi, menempelkannya di struktur data, dll. Perhitungan yang tidak dievaluasi seperti itu dapat membuang pengecualian atau loop selamanya, tetapi jika kita tidak pernah benar-benar perlu memeriksa nilainya maka perhitungannya akan tidak pernahjalankan dan temukan kesalahannya, dan program keseluruhan kami mungkin berhasil melakukan sesuatu yang didefinisikan dengan baik dan selesai. Jadi tanpa ingin menjelaskan apa yang dimaksud kode Haskell dengan menentukan perilaku operasional yang tepat dari program pada saat runtime, kami sebaliknya menyatakan perhitungan yang salah seperti menghasilkan nilai bawah, dan menjelaskan apa yang berperilaku nilai; pada dasarnya bahwa setiap ekspresi yang perlu bergantung pada properti di semua nilai bawah (selain yang ada) juga akan menghasilkan nilai bawah.

Untuk tetap "murni" semua cara yang mungkin menghasilkan nilai bawah harus diperlakukan sebagai setara. Itu termasuk "nilai dasar" yang mewakili loop tak terbatas. Karena tidak ada cara untuk mengetahui bahwa beberapa loop tak terbatas sebenarnya tidak terbatas (mereka mungkin selesai jika Anda menjalankannya sedikit lebih lama), Anda tidak dapat memeriksa properti apa pun yang memiliki nilai dasar. Anda tidak dapat menguji apakah sesuatu terbawah, tidak dapat membandingkannya dengan hal lain, tidak dapat mengubahnya menjadi string, tidak ada. Yang dapat Anda lakukan dengan meletakkannya adalah tempat (parameter fungsi, bagian dari struktur data, dll) tidak tersentuh dan tidak diperiksa.

Python sudah memiliki bottom seperti ini; itu adalah "nilai" yang Anda dapatkan dari ekspresi yang melempar pengecualian, atau tidak berakhir. Karena Python ketat daripada malas, "pantat" seperti itu tidak dapat disimpan di mana saja dan berpotensi dibiarkan tidak diperiksa. Jadi tidak ada kebutuhan nyata untuk menggunakan konsep nilai bawah untuk menjelaskan bagaimana perhitungan yang gagal mengembalikan nilai masih dapat diperlakukan seolah-olah mereka memiliki nilai. Tetapi tidak ada alasan Anda tidak bisa berpikir seperti ini tentang pengecualian jika Anda mau.

Melontarkan pengecualian sebenarnya dianggap "murni". Ini menangkap pengecualian yang merusak kemurnian - justru karena memungkinkan Anda untuk memeriksa sesuatu tentang nilai dasar tertentu, alih-alih memperlakukan mereka semua secara bergantian. Di Haskell Anda hanya dapat menangkap pengecualian di IOyang memungkinkan antarmuka tidak murni (sehingga biasanya terjadi pada lapisan yang cukup luar). Python tidak menegakkan kemurnian, tetapi Anda masih bisa memutuskan sendiri fungsi mana yang merupakan bagian dari "lapisan luar yang tidak murni" daripada fungsi murni, dan hanya membiarkan diri Anda menangkap pengecualian di sana.

Mengembalikan Nonesebaliknya sama sekali berbeda. Noneadalah nilai non-bawah; Anda dapat menguji apakah ada sesuatu yang setara dengannya, dan penelepon fungsi yang kembali Noneakan terus berjalan, mungkin menggunakan yang Nonetidak sesuai.

Jadi jika Anda berpikir untuk melempar pengecualian dan ingin "kembali ke bawah" untuk meniru pendekatan Haskell, Anda sama sekali tidak melakukan apa pun. Biarkan pengecualian berkembang. Itulah yang dimaksud oleh pemrogram Haskell ketika mereka berbicara tentang suatu fungsi yang mengembalikan nilai terendah.

Tapi bukan itu yang dimaksud programmer fungsional ketika mereka mengatakan untuk menghindari pengecualian. Pemrogram fungsional lebih suka "fungsi total". Ini selalu mengembalikan nilai non-bottom yang valid dari tipe pengembalian mereka untuk setiap input yang mungkin. Jadi fungsi apa pun yang dapat melempar pengecualian bukanlah fungsi total.

Alasan kami menyukai fungsi total adalah bahwa mereka lebih mudah diperlakukan sebagai "kotak hitam" ketika kita menggabungkan dan memanipulasi mereka. Jika saya memiliki fungsi total mengembalikan sesuatu tipe A dan fungsi total yang menerima sesuatu tipe A, maka saya dapat memanggil yang kedua pada output yang pertama, tanpa mengetahui apa - apa tentang implementasi keduanya; Saya tahu saya akan mendapatkan hasil yang valid, tidak peduli bagaimana kode dari salah satu fungsi diperbarui di masa mendatang (selama totalitasnya dipertahankan, dan selama mereka tetap menggunakan jenis tanda tangan yang sama). Pemisahan kekhawatiran ini dapat menjadi bantuan yang sangat kuat untuk refactoring.

Ini juga agak diperlukan untuk fungsi tingkat tinggi yang dapat diandalkan (fungsi yang memanipulasi fungsi lain). Jika saya ingin menulis kode yang menerima fungsi sepenuhnya arbitrer (dengan antarmuka yang dikenal) sebagai parameter saya harus memperlakukannya sebagai kotak hitam karena saya tidak punya cara untuk mengetahui input mana yang dapat memicu kesalahan. Jika saya diberi fungsi total maka tidak ada input yang akan menyebabkan kesalahan. Demikian pula, penelepon fungsi orde tinggi saya tidak akan tahu persis argumen apa yang saya gunakan untuk memanggil fungsi yang mereka sampaikan kepada saya (kecuali jika mereka ingin bergantung pada detail implementasi saya), jadi memberikan fungsi total berarti mereka tidak perlu khawatir tentang apa yang saya lakukan dengannya.

Jadi seorang programmer fungsional yang menyarankan Anda untuk menghindari pengecualian akan lebih suka Anda mengembalikan nilai yang mengkodekan kesalahan atau nilai yang valid, dan mengharuskan untuk menggunakannya Anda siap untuk menangani kedua kemungkinan. Hal-hal seperti Eitherjenis atau Maybe/ Optionjenis adalah beberapa yang paling sederhana pendekatan untuk melakukan hal ini dalam bahasa lainnya sangat diketik (biasanya digunakan dengan sintaks khusus atau fungsi orde tinggi untuk bantuan lem bersama-sama hal-hal yang perlu Adengan hal-hal yang menghasilkan Maybe<A>).

Sebuah fungsi yang baik pengembalian None(jika kesalahan terjadi) atau beberapa nilai (jika tidak ada error) mengikuti tidak dari strategi di atas.

Dalam Python dengan bebek mengetik gaya Either / Maybe tidak digunakan terlalu banyak, sebagai gantinya membiarkan pengecualian dilemparkan, dengan tes untuk memvalidasi bahwa kode bekerja daripada memercayai fungsi menjadi total dan secara otomatis dapat digabungkan berdasarkan jenisnya. Python tidak memiliki fasilitas untuk menegakkan bahwa kode menggunakan hal-hal seperti Mungkin mengetik dengan benar; bahkan jika Anda menggunakannya sebagai masalah disiplin Anda perlu tes untuk benar-benar menggunakan kode Anda untuk memvalidasi itu. Jadi pendekatan pengecualian / bawah mungkin lebih cocok untuk pemrograman fungsional murni dengan Python.

Ben
sumber
1
+1 jawaban yang luar biasa! Dan sangat komprehensif juga.
Andres F.
6

Selama tidak ada efek samping yang terlihat secara eksternal dan nilai kembali tergantung secara eksklusif pada input, maka fungsinya murni, bahkan jika itu melakukan beberapa hal yang agak tidak murni secara internal.

Jadi itu benar-benar tergantung pada apa yang bisa menyebabkan pengecualian untuk dibuang. Jika Anda mencoba membuka file yang diberikan path, maka tidak, itu tidak murni, karena file tersebut mungkin atau mungkin tidak ada, yang akan menyebabkan nilai kembali bervariasi untuk input yang sama.

Di sisi lain, jika Anda mencoba mengurai integer dari string yang diberikan dan melemparkan pengecualian jika gagal, itu bisa murni, asalkan tidak ada pengecualian yang dapat keluar dari fungsi Anda.

Di samping catatan, bahasa fungsional cenderung mengembalikan tipe unit hanya jika hanya ada satu kondisi kesalahan yang mungkin. Jika ada beberapa kemungkinan kesalahan, mereka cenderung mengembalikan jenis kesalahan dengan informasi tentang kesalahan.

8bree
sumber
Anda tidak dapat mengembalikan tipe terbawah.
Gardenhead
1
@ardenhead Anda benar, saya memikirkan tipe unit. Tetap.
8bittree
Dalam kasus contoh pertama Anda, bukankah sistem file dan file apa yang ada di dalamnya hanya salah satu input ke fungsi?
Vality
2
@Vaility Pada kenyataannya Anda mungkin memiliki pegangan atau jalur ke file. Jika fungsinya fmurni, Anda diharapkan f("/path/to/file")selalu mengembalikan nilai yang sama. Apa yang terjadi jika file aktual dihapus atau diubah di antara dua pemanggilan f?
Andres F.
2
@Vaility Fungsi ini bisa murni jika alih-alih menangani file yang Anda berikan konten sebenarnya (yaitu snapshot byte yang tepat) dari file, dalam hal ini Anda dapat menjamin bahwa jika konten yang sama masuk, output yang sama keluar. Tetapi mengakses file tidak seperti itu :)
Andres F.