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 None
tidak 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, optional
fungsi -decorated mengembalikan Optional
nilai, yang bisa kosong, untuk output normal serta untuk daftar pengecualian yang ditentukan (pengecualian yang tidak ditentukan masih dapat menghentikan eksekusi). Carry
membuat evaluasi tertunda, di mana setiap langkah (panggilan fungsi tertunda) baik mendapatkan Optional
output 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/except
blok 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
sumber
Jawaban:
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:
kita tahu bahwa
do_thing
tidak pernah bisa kembali, karena itu harus mengembalikan nilai tipeBottom
. Dengan demikian, hanya ada dua kemungkinan:do_thing
tidak berhentido_thing
melempar pengecualian (dalam bahasa dengan mekanisme pengecualian)Perhatikan bahwa saya membuat jenis
Bottom
yang sebenarnya tidak ada dalam bahasa Python.None
adalah keliru; itu sebenarnya nilai unit , satu-satunya nilai dari tipe unit , yang disebutNoneType
dengan Python (lakukantype(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
Option
jenis. Misalnya, kami akan membuat fungsi pembagian aman sebagai berikut:Sayangnya, karena Python sebenarnya tidak memiliki tipe penjumlahan, ini bukan pendekatan yang layak. Anda dapat kembali
None
sebagai tipe opsi orang miskin untuk menunjukkan kegagalan, tetapi ini benar-benar tidak lebih baik daripada kembaliNull
. 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.
sumber
None
tidak 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?try/except/finally
seperti alternatif lainif/else
, yaitutry: 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 menggunakanif/else
blok untuk ini juga). Apakah maksud Anda, bahwa saya tidak masuk akal dan harus membiarkan pola seperti itu karena "ini adalah Python"?try... catch...
seharusnya tidak digunakan untuk aliran kontrol. Entah mengapa, komunitas Python memutuskan untuk melakukan banyak hal. Sebagai contoh,safe_div
fungsi yang saya tulis di atas biasanya akan ditulistry: 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.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?
sumber
T + { Exception }
(di manaT
adalah 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.map : (A->B)->List A ->List B
manaA->B
kesalahan bisa terjadi. Jika kita mengizinkan f untuk melemparkan eksepsimap f L
akan mengembalikan sesuatu yang bertipeException + List<B>
. Jika sebaliknya kami mengizinkannya untuk mengembalikanoptional
jenis gaya,map f L
sebagai gantinya akan mengembalikan Daftar <Opsional <B>> `. Opsi kedua ini terasa lebih fungsional bagi saya.Semantik Haskell menggunakan "nilai terbawah" untuk menganalisis makna kode Haskell. Ini bukan sesuatu yang benar-benar Anda gunakan langsung dalam pemrograman Haskell, dan kembali
None
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
IO
yang 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
None
sebaliknya sama sekali berbeda.None
adalah nilai non-bawah; Anda dapat menguji apakah ada sesuatu yang setara dengannya, dan penelepon fungsi yang kembaliNone
akan terus berjalan, mungkin menggunakan yangNone
tidak 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
Either
jenis atauMaybe
/Option
jenis 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 perluA
dengan hal-hal yang menghasilkanMaybe<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.
sumber
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.
sumber
f
murni, Anda diharapkanf("/path/to/file")
selalu mengembalikan nilai yang sama. Apa yang terjadi jika file aktual dihapus atau diubah di antara dua pemanggilanf
?