İşlevsel stil istisnası işleme


24

İşlevsel programlamada birinin istisnalar atmaması ve / veya gözetmemesi gerektiği söylendi. Bunun yerine hatalı bir hesaplama en düşük değer olarak değerlendirilmelidir. Python'da (veya işlevsel programlamayı tam olarak desteklemeyen diğer dillerde ), "saf kalmaya" ters giden bir şey yanlış giderse, ancak tanımlamaya kesinlikle uymuyorsa ) bir kişi geri dönebilir None(veya en düşük değer olarak kabul edilen başka bir alternatif None) öyleyse, kişi ilk başta bir hata gözlemlemek zorundadır, yani

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

Bu saflığı ihlal ediyor mu? Ve eğer öyleyse, bu, yalnızca Python'daki hataları ele almanın imkansız olduğu anlamına mı geliyor?

Güncelleştirme

Eric Lippert, yorumunda, AP'de istisnaları tedavi etmenin başka bir yolunu hatırlattı. Bunu Python'da pratikte hiç görmedim, bir yıl önce FP okuduğumda onunla oynadım. Burada, herhangi bir optionaldekore edilmiş işlev Optional, normal çıktılar için ve ayrıca bir istisnalar listesi için boş olabilecek değerleri döndürür (belirtilmemiş istisnalar, yürütmeyi sona erdirebilir). CarryHer bir adımın (gecikmeli fonksiyon çağrısı) Optionalönceki adımdan boş bir çıktı aldığı ve basitçe üzerinden geçtiği veya başka bir şekilde kendisini yenisini değerlendirdiği gecikmeli bir değerlendirme yaratır Optional. Sonunda son değer ya normaldir ya da Empty. Burada try/exceptblok bir dekoratörün arkasına gizlenmiştir, bu nedenle belirtilen istisnalar iade tipi imzanın bir parçası olarak kabul edilebilir.

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

1
Sorunuz çoktan cevaplandı, bu yüzden sadece bir yorum: Fonksiyonel kullanımınızın işlevsel programlamada neden bir istisna attığını anlıyor musunuz? Bu keyfi bir heves değil :)
Andres F.

6
Hatayı gösteren bir değer döndürmenin başka bir alternatifi var. Unutmayın, istisna işleme kontrol akışıdır ve kontrol akışlarını yeniden düzenlemek için işlevsel mekanizmalarımız vardır. İki işlevi alacak yönteminizi yazarak, istisnai durumları işlevsel dillerde taklit edebilirsiniz: başarı ve hata devam . İşlevinizin yaptığı son şey, ya başarı sonucunu, "sonucu" geçen ya da "istisnayı" geçen hata devamıdır. Aşağı tarafı, programınızı bu dışa dönük biçimde yazmak zorunda olmanızdır.
Eric Lippert,

3
Hayır, bu durumda devlet ne olurdu? Birden fazla sorun var, fakat burada birkaçı var: 1- Tipte kodlanmamış olası bir akış var, yani bir fonksiyonun sadece tipine bakarak bir fonksiyonun istisna ataıp atmayacağını bilemezsiniz (sadece kontrol etmediğiniz sürece istisnalar, elbette, ama sadece onların dilini bilen bir dil bilmiyorum ). Tip sistemin dışına etkili bir şekilde çalışıyorsunuz, 2- işlevsel programlayıcılar mümkün olduğunda "toplam" işlevlerini, yani her giriş için bir değer döndüren işlevleri (sonlandırmayı yasaklayarak) yazmaya çalışıyorlar. İstisnalar buna karşı çalışıyor.
Andres F.

3
3- Toplam fonksiyonlarla çalışırken, bunları diğer fonksiyonlarla oluşturabilir ve "türünde kodlanmayan" hata sonuçları, yani istisnalar hakkında endişelenmeden daha yüksek dereceli fonksiyonlarda kullanabilirsiniz.
Andres F.

1
@EliKorvigo Bir değişkenin ayarlanması, tanımı gereği tam durma durumunu gösterir. Değişkenlerin tüm amacı, durumlarını tutmaktır. Bunun istisnalar ile ilgisi yok. Bir işlevin dönüş değerini gözlemlemek ve gözleme dayalı bir değişkenin değerini ayarlamak, değerlendirmeye durumu yansıtır, değil mi?
user253751

Yanıtlar:


20

Her şeyden önce, bazı yanlış anlamaları çözelim. "En düşük değer" yoktur. Alt tip, dilin her türünün alt türü olan bir tür olarak tanımlanır. Bundan, kişi (en azından herhangi bir ilginç tipte sistemde), alt tipin hiçbir değeri olmadığını ispatlayabilir - boş . Yani alt bir değer diye bir şey yoktur.

Alt tip neden faydalıdır? Eh, boş olduğunu bilmek, bize program davranışında bazı kesintiler yapalım. Örneğin, eğer fonksiyonumuz varsa:

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

do_thingBunun asla geri gelemeyeceğini biliyoruz , çünkü bir tür değer döndürmek zorunda kalacak Bottom. Böylece, sadece iki olasılık var:

  1. do_thing durmuyor
  2. do_thing istisna atar (istisna mekanizmasına sahip dillerde)

BottomGerçekten Python dilinde olmayan bir tür yarattığımı unutmayın . Nonebir yanlıştır; gerçekte olduğundan birim değer , tek ölçüsü birimi türüne denir, NoneTypePython (do type(None)kendiniz için onaylayın).

Şimdi, bir başka yanlış anlama da işlevsel dillerin istisna olmadığıdır. Bu da doğru değil. Örneğin SML çok hoş bir istisna mekanizmasına sahiptir. Bununla birlikte, istisnalar, SML'de örneğin Python'dan çok daha az kullanılır. Dediğiniz gibi , işlevsel dillerde bir tür başarısızlık belirtmenin en yaygın yolu bir tür döndürmektir Option. Örneğin, aşağıdaki gibi güvenli bir bölme işlevi oluşturacağız:

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

Ne yazık ki, Python aslında toplam türlere sahip olmadığından, bu uygulanabilir bir yaklaşım değildir. Sen olabilir dönmek Nonedelalet başarısızlığa bir yoksul adamın opsiyon türü olarak, ancak bu dönen daha iyi gerçekten Null. Tip güvenliği yok.

Bu yüzden dilin sözleşmelerini bu durumda takip etmenizi tavsiye ederim. Python, kontrol akışını işlemek için deyimsel olmayan istisnalar kullanır (ki bu kötü tasarım, IMO, ancak yine de standarttır), bu nedenle yalnızca kendiniz yazdığınız kodla çalışmadığınız sürece standart uygulamaları takip etmenizi tavsiye ederim. Bunun "saf" olup olmadığı önemli değildir.


"Alt değer" derken aslında "alt tip" demek istedim, bu yüzden Nonetanımına uymadığını yazdım . Neyse, beni düzelttiğin için teşekkürler. İstisnai durumun yalnızca yürütmeyi durdurmak veya isteğe bağlı bir değer döndürmek için kullanılmasının Python'un ilkelerine uygun olduğunu düşünmüyor musunuz? Yani, neden karmaşık kontrol için istisna kullanmaktan kaçınmak kötü?
Eli Korvigo

@EliKorvigo Söylediğim şey az ya da çok, doğru mu? İstisnalar aptalca Python'dur.
gardenhead,

1
Örneğin, lisans öğrencilerimi try/except/finallybaşka bir alternatif gibi kullanmalarına teşvik ediyorum if/else, yani try: var = expession1; except ...: var = expression 2; except ...: var = expression 3..., zorunlu bir dilde yapmak yaygın bir şey olsa da (btw, bunun if/elseiçin de blokları kullanmaktan kesinlikle vazgeçiyorum ). Yani, mantıksız davranıyorum ve "bu Python" dan beri bu tarz kalıplara izin vermek zorunda mıyım?
Eli Korvigo

@EliKorvigo Genel olarak sizinle aynı fikirdeyim (btw, sen profesör müsün?). try... catch...gerektiğini değil , kontrol akışı için kullanılabilir. Nedense Python topluluğu böyle şeyler yapmaya karar verdi. Örneğin, safe_divyukarıda yazdığım fonksiyon genellikle yazılır try: result = num / div: except ArithmeticError: result = None. Bu yüzden, onlara genel yazılım mühendisliği ilkelerini öğretiyorsanız, kesinlikle bunu reddetmelisiniz. if ... else...Aynı zamanda bir kod kokusu, ama buraya gitmek çok uzun.
Gardenhead

2
Bir "alt değer" (örneğin Haskell'in anlambiliminden bahsetmek için kullanılır) vardır ve alt tiple ilgisi yoktur. Yani bu OP'nin yanlış anlaşılması değil, sadece farklı bir şeyden bahsediyor.
Ben,

11

Son birkaç gün boyunca saflığa çok ilgi duyduğundan, neden saf bir fonksiyonun nasıl olduğunu incelemiyoruz.

Saf bir işlev:

  • Referans olarak saydamdır; yani, belirli bir giriş için, her zaman aynı çıkışı üretecektir.

  • Yan etki üretmez; harici ortamında girişleri, çıkışları veya başka herhangi bir şeyi değiştirmez. Sadece bir dönüş değeri üretir.

Öyleyse kendine sor. İşleviniz bir girdi kabul edip bir çıktı döndürmekten başka bir şey yapar mı?


3
Yani önemli değil, işlevsel olarak davrandığı sürece içine ne kadar çirkin yazılmış?
Eli Korvigo

8
Doğru, sadece saflıkla ilgileniyorsan. Elbette göz önünde bulundurulması gereken başka şeyler olabilir.
Robert Harvey,

3
Şeytanların savunuculuğunu oynamak için, bir istisna atmanın sadece bir çıktı biçimi olduğunu ve fırlatan fonksiyonların saf olduğunu savunurum. Bu bir problem mi?
Bergi,

1
@Bergi Şeytanın savunuculuğunu oynuyorsunuz bilmiyorum, çünkü bu tam olarak bu cevabın ima ettiği şey :) Sorun şu ki, saflığın yanı sıra başka düşünceler de var . Denetlenmeyen istisnalara izin verirseniz (tanımı gereği işlev imzasının bir parçası değildir), o zaman her işlevin geri dönüş tipi etkin bir şekilde T + { Exception }( Taçıkça bildirilen geri dönüş türünün olduğu) olur, bu sorunludur. Bir işlevin kaynak koduna bakmadan bir istisna atıp atmayacağını bilemezsiniz; bu da daha yüksek dereceli fonksiyonlar yazmayı problemli hale getirir.
Andres F.

2
@Begri atma tartışmasız saf olabilir, istisnalar kullanarak IMO, her bir işlevin geri dönüş türünü karmaşıklaştırmaktan fazlasını yapar. İşlevlerin birleştirilebilirliğine zarar verir. map : (A->B)->List A ->List BNerede A->Bhata yapılabileceğini uygulayın . F'nin bir istisna atmasına izin verirsek map f L , türünde bir şey döndürürüm Exception + List<B>. Bunun yerine bir optionalstil türü döndürmesine izin verirsek map f L, bunun yerine <İsteğe Bağlı <B>> `Listesini döndüreceğiz. Bu ikinci seçenek bana daha işlevsel geliyor.
Michael Anderson,

7

Haskell anlambilimi, Haskell kodunun anlamını analiz etmek için bir "alt değer" kullanır. Bu, doğrudan Haskell'i programlamakta doğrudan kullandığınız bir şey değildir ve geri dönmek Nonede aynı şey değildir.

En düşük değer, Haskell semantics tarafından normalde bir değeri değerlendirmede başarısız olan tüm hesaplamalara atfedilen değerdir. Haskell hesaplamasının yapabileceği yollardan biri aslında bir istisna atmaktır! Bu yüzden Python'da bu stili kullanmaya çalışıyorsanız, aslında sadece istisnaları normal şekilde atmalısınız.

Haskell anlambilimi, alt değeri kullanır, çünkü Haskell tembeldir; Henüz çalışmayan hesaplamalar tarafından döndürülen "değerleri" değiştirebileceksiniz. Onları fonksiyonlara aktarabilir, bunları veri yapılarına vb. Sokabilirsiniz. Böyle bir değerlendirilmemiş hesaplama sonsuza kadar bir istisna veya döngü atabilir, ancak değeri gerçekten incelememiz gerekmiyorsa hesaplama aslahatayı çalıştırın ve karşılaştığınızda genel programımız iyi tanımlanmış ve bitmiş bir şey yapabilir. Bu yüzden Haskell kodunun ne anlama geldiğini programın çalışma sırasındaki tam çalışma davranışını belirleyerek açıklamak istemiyorsak, bunun yerine bu hatalı hesaplamaları en düşük değeri ürettiğini ilan ediyoruz ve bu değerin ne işe yaradığını açıklıyoruz; Temelde (mevcut hariç) alt değerin hiç bir özelliklerine bağlı gerektiği her ifade edeceğini de alt değere neden.

"Saf" kalmak için, alt değer üretmenin tüm olası yolları eşdeğer olarak ele alınmalıdır. Bu sonsuz bir döngüyü temsil eden "alt değeri" içerir. Bazı sonsuz döngülerin aslında sonsuz olduğunu bilmenin bir yolu olmadığı için (eğer onları biraz daha uzun süre çalıştırırsanız bitirebilirler), en düşük değere sahip herhangi bir özelliği inceleyemezsiniz . Bir şeyin alt olup olmadığını test edemezsiniz, başka bir şeyle karşılaştıramazsınız, bir dizgeye dönüştüremezsiniz, hiçbir şey. Biriyle yapabileceğiniz tek şey, dokunulmamış ve incelenmemiş yerleştirilmiş (işlev parametreleri, veri yapısının bir parçası vb.) Yerleştirmektir.

Python zaten bu tür bir tabana sahiptir; bir istisna atan veya sonlanmayan bir ifadeden aldığınız "değerdir". Python tembel olmaktan ziyade katı olduğundan, bu tür "dipler" hiçbir yerde saklanamaz ve potansiyel olarak incelenmemiş bırakılabilir. Bu nedenle, bir değeri geri getiremeyen hesaplamaların hala bir değeri varmış gibi nasıl değerlendirilebileceğini açıklamak için en düşük değer kavramını kullanmaya gerek yoktur. Ancak, eğer isterseniz, istisnalar hakkında bu şekilde düşünememeniz için hiçbir sebep yok.

İstisnaları atma aslında "saf" olarak kabul edilir. Bu oluyor yakalamak sonları saflık olduğunu istisnalar - bu belirli alt değerler hakkında bir şeyler incelememizi sağlar kesin çünkü yerine birbirinin hepsini tedavi. Haskell'de, sadece IO, saf olmayan arayüzlemeye izin veren istisnaları yakalayabilirsiniz (bu yüzden genellikle oldukça dış bir katmanda gerçekleşir). Python saflığı zorlamaz, ancak hangi fonksiyonların saf fonksiyonlardan ziyade "dış saf olmayan katmanın" bir parçası olduğuna kendiniz karar verebilirsiniz ve sadece orada istisnalar yakalamanıza izin verir.

NoneBunun yerine geri dönüş tamamen farklı. Nonealt olmayan bir değerdir; Bir şeyin ona eşit olup olmadığını test edebilirsiniz ve geri dönen işlevin arayanı None, muhtemelen Noneuygun olmayan şekilde kullanarak çalışmaya devam edecektir .

Eğer bir istisna atmayı düşünüyorsanız ve Haskell'in yaklaşımını taklit etmek için "alttan dönmek" istiyorsanız, hiçbir şey yapmazsınız. İstisna yayılsın. Bu tam olarak Haskell programcılarının en düşük değeri veren bir fonksiyondan bahsettiklerinde kastettikleri şeydir.

Ancak, istisnai durumlardan kaçınmak için söylediklerinde fonksiyonel programcıların kastettiği bu değildir . İşlevsel programcılar "toplam işlevleri" tercih eder. Bunlar her zaman mümkün olan her giriş için kendi dönüş tipinin geçerli bir dip-dışı değerini döndürür . Yani bir istisna atabilecek herhangi bir işlev toplam bir işlev değildir.

Toplam fonksiyonlardan hoşlanmamızın nedeni, onları birleştirip manipüle ettiğimizde "kara kutu" olarak ele almanın çok daha kolay olmasıdır. Eğer A tipi bir şeyi döndüren toplam bir fonksiyonum ve A tipi bir şeyi kabul eden toplam bir fonksiyonum varsa, o zaman ikisinin de uygulaması hakkında hiçbir şey bilmeden, birincinin çıktısında ikinciyi çağırabilirim ; Her iki fonksiyonun kodunun gelecekte nasıl güncellendiği önemli değildir (bütünlükleri korundukları ve aynı tip imzaları korudukları sürece), geçerli bir sonuç alacağımı biliyorum. Kaygıların bu şekilde ayrılması, yeniden yapılanma için son derece güçlü bir yardım olabilir.

Aynı zamanda, güvenilir yüksek dereceli fonksiyonlar (diğer fonksiyonları manipüle eden fonksiyonlar) için de biraz gereklidir. Parametre olarak tamamen rastgele bir işlev (bilinen bir arayüzle) alan kod yazmak istersem , kara kutu olarak değerlendirmek zorundayım çünkü hangi girdilerin bir hata tetikleyebileceğini bilme imkanım yok. Toplam bir işlev verilirse hiçbir giriş bir hataya neden olmaz. Benzer şekilde, üst düzey fonksiyonumun arayanı tam olarak hangi işlevi kullandığımı (benim uygulama detaylarıma bağlı kalmak istemiyorlarsa) bana çağırmak için kullandığım argümanları bilmeyecek, bu yüzden toplam bir işlevi geçmek endişelenmek zorunda kalmayacakları anlamına geliyor onunla ne yapıyorum.

Bu nedenle, istisnalardan kaçınmanızı öneren işlevsel bir programcı, bunun yerine, hatayı ya da geçerli bir değeri kodlayan bir değer döndürmenizi tercih eder ve bunu kullanmak için her iki olasılığı da hazırlamanızı gerektirir. Türler Eitherve Maybe/ Optiontürler gibi şeyler , bunu daha güçlü şekilde yazılmış dillerde yapmak için en basit yaklaşımlardan bazılarıdır (genellikle özel bir sözdizimi veya daha üst düzey işlevlerle birlikte kullanılan ve bir şeyleri gerektiren şeyleri birbirine yapıştırmak Aiçin kullanılır Maybe<A>).

Dönen None(bir hata olursa) veya bir değer (hiçbir hata yoksa), yukarıdaki stratejilerin hiçbirini izlemeyen bir işlev .

Ördek yazarak Python'da Ya / Belki tarzı çok fazla kullanılmaz, bunun yerine istisnaların yapılmasına izin verilir, testlerin kodların türlerine göre otomatik olarak birleştirilebilir ve otomatik olarak birleştirilebilir olduğuna güvenmek yerine çalıştığını doğrulayan testler yapılır. Python'un, bu tür kodları Belki türlerini doğru bir şekilde kullanma; Disiplin meselesi olarak kullanıyor olsanız bile, bunu doğrulamak için kodunuzu gerçekten uygulamak için testler yapmanız gerekir. Dolayısıyla, istisna / alt yaklaşım Python'da saf işlevsel programlama için muhtemelen daha uygundur.


1
+1 Müthiş cevap! Ve ayrıca çok kapsamlı.
Andres F.

6

Dışarıdan görülebilen herhangi bir yan etki olmadığı ve geri dönüş değeri yalnızca girdilere bağlı olduğu sürece, bazı şeyleri dahili olarak engellemesine rağmen, işlev saftır.

Bu, gerçekten istisnaların atılmasına neden olabilecek şeyin ne olduğuna bağlı. Yol verilen bir dosyayı açmaya çalışıyorsanız, hayır, saf değildir, çünkü dosya var olabilir veya olmayabilir, bu da dönüş değerinin aynı giriş için değişmesine neden olur.

Öte yandan, verilen bir dizgeden bir tamsayı ayrıştırmaya ve başarısız olursa bir istisna atmaya çalışıyorsanız, hiçbir istisna dışında sizin işlevinizden dışarı çıkmadığı sürece bu saf olabilir.

Bir yandan notta, işlevsel diller yalnızca olası bir hata durumu varsa ünite tipini döndürme eğilimindedir . Birden fazla olası hata varsa, hatayla ilgili bilgileri içeren bir hata türü döndürürler.


Alt tipi geri döndüremezsiniz.
gardenhead 27:16

1
@gardenhead Haklısın, birim tipini düşünüyordum. Sabit.
8,

İlk örneğinizde, dosya sistemi değil ve içinde hangi dosyalar var? Fonksiyona girilen girdilerden biri?
Vality

2
@Vaility Gerçekte, muhtemelen bir tanıtıcı veya dosyaya bir yol var. İşlev fsafsa, f("/path/to/file")her zaman aynı değeri döndürmeyi beklersiniz . İki dosya arasındaki asıl dosya silinir veya değiştirilirse ne olur f?
Andres F.,

2
@Vaility Dosyanın tanıtıcısı yerine dosyanın asıl içeriğini (yani baytların tam görüntüsünü) geçtiyseniz, işlev saf olabilir; bu durumda aynı içeriğin içeri girmesi durumunda aynı çıktıyı garanti edersiniz. söner Ancak bir dosyaya erişmek böyle değil :)
Andres F. 23
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.