36 votes

Est "avec" monadique?

Comme de nombreux téméraire pionnier devant moi, je me suis efforcé de franchir la sans traces friche qui est de la Compréhension des Monades.

Je suis toujours en titubant à travers, mais je ne peux m'empêcher de remarquer une certaine monade-comme la qualité de Python with déclaration. Considérer ce fragment:

with open(input_filename, 'r') as f:
   for line in f:
       process(line)

Envisager de l'ouvrir() l'appel comme "l'unité" et le bloc lui-même comme le "bind". La réelle de la monade n'est pas exposé (euh, à moins d' f est la monade), mais la tendance est là. N'est-ce pas? Ou suis-je en prenant l'ensemble de FP pour monadry? Ou est-ce juste 3 heures du matin, et tout semble plausible?

Une question connexe: si nous avons des monades, faut-il des exceptions?

Dans le fragment ci-dessus, toute défaillance dans l'I/O peuvent être cachés dans le code. La corruption de disque, l'absence du nom de fichier et un fichier vide peuvent tous être traités de la même façon. Donc pas besoin d'un visible IO Exception.

Certes, Scala Option typeclass a éliminé le redoutable Exception de Pointeur Null. Si vous repensé les nombres comme des Monades (avec NaN et DivideByZero que les cas particuliers)...

Comme je l'ai dit, à 3 heures du matin.

23voto

outis Points 39377

C'est presque trop trivial pour parler, mais le premier problème est qu' with n'est pas une fonction et ne prennent pas une fonction comme argument. Vous pouvez facilement contourner ce problème en écrivant une fonction wrapper pour with:

def withf(context, f):
    with context as x:
        f(x)

Puisque c'est si insignifiant, vous ne pouviez pas pris la peine de distinguer withf et with.

Le deuxième problème avec with être une monade, c'est que, comme une déclaration plutôt que d'une expression, il n'a pas de valeur. Si vous pouviez lui donner un type, il serait M a -> (a -> None) -> None (c'est en fait le type d' withf ci-dessus). Parlant pratiquement, vous pouvez utiliser Python _ pour obtenir une valeur pour l' with déclaration. En Python 3.1:

class DoNothing (object):
    def __init__(self, other):
        self.other = other
    def __enter__(self):
        print("enter")
        return self.other
    def __exit__(self, type, value, traceback):
        print("exit %s %s" % (type, value))

with DoNothing([1,2,3]) as l:
    len(l)

print(_ + 1)

Depuis withf utilise une fonction plutôt qu'un bloc de code, une alternative à l' _ est de retourner la valeur de la fonction:

def withf(context, f):
    with context as x:
        return f(x)

Il y a une autre chose qui empêche with (et withf) d'être un monadique lier. La valeur du bloc devrait être un monadique type avec le même type de constructeur comme l' with élément. Comme il est, with est plus générique. Considérant agf noter que chaque interface est un constructeur de type, je peg le type d' with comme M a -> (a -> b) -> b, où M est le gestionnaire de contexte de l'interface (l' __enter__ et __exit__ méthodes). Entre les types d' bind et with est le type M a -> (a -> N b) -> N b. Pour être une monade, with serait à l'échec lors de l'exécution lors de l' b n'était pas M a. En outre, alors que vous pourriez utiliser with monadically comme une opération de liaison, il serait raisonnable de le faire.

La raison vous avez besoin de faire ces distinctions subtiles, c'est que si par erreur vous envisagez with à être monadique, vous vous retrouverez à en abuser et à écrire des programmes qui échouent en raison d'erreurs de type. En d'autres termes, vous allez écrire des ordures. Ce que vous devez faire est de distinguer d'une construction qui est une chose en particulier (par exemple, une monade), qui peut être utilisé dans les conditions de cette chose (par exemple, encore une fois, une monade). Ce dernier exige de la discipline de la part du programmeur, ou la définition de constructions supplémentaires pour faire respecter la discipline. Voici un près de monadique version de with (le type est - M a -> (a -> b) -> M b):

def withm(context, f):
    with context as x:
        return type(context)(f(x))

Dans l'analyse finale, vous pourriez envisager with comme un combinator, mais le plus général que le combinateur requis par les monades (qui est lier). Il peut y avoir plusieurs fonctions à l'aide des monades que les deux (la liste monade a aussi des inconvénients, l'ajout et la longueur, par exemple), donc si vous avez défini le approprié de lier l'opérateur pour les gestionnaires de contexte (comme withm) alors with pourrait être monadique dans le sens de l'implication des monades.

11voto

agf Points 45052

Oui.

Juste en dessous de la définition, Wikipédia dit:

Dans la programmation orientée objet termes, le type de construction correspond à la déclaration du type monadique, l'unité de la fonction prend le rôle d'un constructeur de la méthode et de l'opération de liaison contient la logique nécessaire à l'exécution de ses rappels enregistrés (le monadique fonctions).

Ça me fait exactement comme le gestionnaire de contexte de protocole, la mise en œuvre du gestionnaire de contexte de protocole par l'objet, et l' with déclaration.

À partir de @Owen dans un commentaire sur ce post:

Les monades, au niveau le plus fondamental, plus ou moins une façon cool pour utiliser la continuation, passant du style: >>= prend un "producteur" et un "rappel"; c'est aussi, essentiellement, ce est la suivante: un producteur comme ouvert(...) et un bloc de code pour être appelé une fois qu'il est créé.

La pleine définition de Wikipedia:

Un type de construction qui définit, pour chaque type sous-jacent, comment obtenir un correspondant monadique type. En Haskell notation, le nom de la monade représente le constructeur de type. Si M est le nom de la monade et de t est un type de données, puis "M t" est le type correspondant dans la monade.

Cela sonne comme le gestionnaire de contexte de protocole de moi.

Une unité de fonction qui fait correspondre une valeur dans un type sous-jacent à une valeur correspondant à la monadique type. Le résultat est le plus "simple" de la valeur dans le type correspondant qui préserve totalement la valeur d'origine (la simplicité d'être compris de manière appropriée à la monade). En Haskell, cette fonction est appelée le retour en raison de la façon dont il est utilisé dans la notation décrite plus loin. L'unité de fonction de type polymorphe t→M t.

La mise en œuvre effective du gestionnaire de contexte de protocole par l'objet.

Une opération de liaison de type polymorphe (M, t)→(t→M u)→(M, u), qui Haskell représente par l'opérateur infixe >>=. Son premier argument est une valeur dans un type monadique, son second argument est une fonction qui cartes du type sous-jacent du premier argument à un autre type monadique, et son résultat est dans cet autre type monadique.

Cela correspond à l' with déclaration et de sa suite.

Donc oui, je dirais with est une monade. J'ai cherché PEP 343 et toutes les rejetées et retirées de PEPs, et aucun d'eux n'a mentionné le mot "monade". Il vaut certainement, mais il semble que le but de l' with déclaration a été de gestion des ressources, et une monade est juste un moyen utile pour l'obtenir.

9voto

sdcvvc Points 14968

Haskell a un équivalent de with pour les fichiers, il est appelé withFile. Ce:

with open("file1", "w") as f:
    with open("file2", "r") as g:
        k = g.readline()
        f.write(k)

est équivalent à:

withFile "file1" WriteMode $ \f ->
  withFile "file2" ReadMode $ \g ->
    do k <- hGetLine g
       hPutStr f k

Maintenant, withFile pourrait ressembler à quelque chose monadique. Son type est:

withFile :: FilePath -> IOMode -> (Handle -> IO r) -> IO r

côté droit ressemble (a -> m b) -> m b.

Une autre similitude: En Python, vous pouvez sauter as, et en Haskell vous pouvez utiliser >> au lieu de >>= (ou do bloc <- flèche).

Donc je vais répondre à cette question: est - withFile monadique?

Vous pourriez penser qu'il puisse être écrit comme ceci:

do f <- withFile "file1" WriteMode
   g <- withFile "file2" ReadMode
   k <- hGetLine g
   hPutStr f k

Mais ce n'est pas de vérification de type. Et il ne peut pas.

C'est parce que dans Haskell le IO monade est séquentielle: si vous écrivez

do x <- a
   y <- b
   c

après l' a est exécutée, b est exécutée, puis c. Il n'y a pas de "revenir en arrière" pour nettoyer a à la fin ou quelque chose comme ça. withFile, d'autre part, a proximité de la poignée lorsque le bloc est exécuté.

Il y a une autre monade, appelé la continuation de la monade, qui permet de faire de telles les choses. Cependant, vous avez maintenant deux monades, IO et les continuations, et l'utilisation d'effets de deux monades à la fois nécessite l'utilisation de monade transformateurs.

import System.IO
import Control.Monad.Cont

k :: ContT r IO ()
k = do f <- ContT $ withFile "file1" WriteMode 
       g <- ContT $ withFile "file2" ReadMode 
       lift $ hGetLine g >>= hPutStr f

main = runContT k return

C'est laid. Donc la réponse est: un peu, mais qui exige de faire face avec beaucoup de subtilités qui rendent le tout assez opaques.

Python with pouvez simuler seul un petit peu de ce que les monades peuvent y ajouter de la saisie et de la finalisation du code. Je ne pense pas que vous pouvez simuler par exemple

do x <- [2,3,4]
   y <- [0,1]
   return (x+y)

à l'aide de with (il pourrait être possible avec certains sale hacks). Au lieu de cela, utilisez-le pour:

for x in [2,3,4]:
    for y in [0,1]:
        print x+y

Et il y a un Haskell fonction de ce - forM:

forM [2,3,4] $ \x ->
  forM [0,1] $ \y ->
    print (x+y)

Je recommed de lecture à propos de yield qui ressemble davantage à monades qu' with: http://www.valuedlessons.com/2008/01/monads-in-python-with-nice-syntax.html

Une question connexe: si nous avons des monades, faut-il des exceptions?

Fondamentalement non, au lieu d'une fonction qui renvoie Un ou retourne B, vous pouvez faire une fonction qui renvoie Either A B. La monade pour Either A va alors se comportent comme des exceptions - si une ligne de code renvoie une erreur, l'ensemble du bloc.

Toutefois, cela voudrait dire que la division aurait de type Integer -> Integer -> Either Error Integer et ainsi de suite, pour attraper la division par zéro. Vous devrez détecter les erreurs (qui est explicitement mise en correspondance du modèle ou de l'utilisation de bind) dans le code qui utilise la division ou a même possibilité d'un moindre mal. Haskell utilise des exceptions pour éviter de le faire.

4voto

Owen Points 14439

J'ai pensé à unnecesarily long à ce sujet et je crois que la réponse est "oui, quand il est utilisé d'une certaine manière" (merci outis :), mais pas pour la raison que j'ai pensé avant.

Je l'ai mentionné dans un commentaire à agf qu' >>= est juste la continuation passing style -- donnez-lui un producteur et un rappel et il "fonctionne" le producteur et l'alimente la de rappel. Mais ce n'est pas tout à fait vrai. Il est également important que >>= a courir certains interaction entre le producteur et le résultat de la fonction de rappel.

Dans le cas de la Liste monade, ce serait la concaténation de listes. Cette l'interaction est ce qui rend les monades spécial.

Mais je crois que Python with ne faire cette interaction, tout simplement pas dans le façon dont vous pourriez vous attendre.

Voici un exemple de programme en python employant deux déclarations:

class A:

    def __enter__(self):
        print 'Enter A'

    def __exit__(self, *stuff):
        print 'Exit A'

class B:

    def __enter__(self):
        print 'Enter B'

    def __exit__(self, *stuff):
        print 'Exit B'

def foo(a):
    with B() as b:
        print 'Inside'

def bar():
    with A() as a:
        foo(a)

bar()

Lorsque vous exécutez la sortie est

Enter A
Enter B
Inside
Exit B
Exit A

Maintenant, Python est un langage impératif, de sorte qu'au lieu de la simple production de données, il produit d'effets secondaires. Mais vous pouvez penser de ces effets secondaires comme des données (comme IO ())- vous ne pouvez pas combiner dans toute la fraîcheur des moyens de combiner IO (), mais ils sont arriver au même but.

Donc, ce que vous devriez vous concentrer sur est le séquençage de ces opérations-qui est, l'ordre des instructions d'impression.

Comparons maintenant le même programme en Haskell:

data Context a = Context [String] a [String]
    deriving (Show)

a = Context ["Enter A"] () ["Exit A"]
b = Context ["Enter B"] () ["Exit B"]

instance Monad Context where
    return x = Context [] x []
    (Context x1 p y1) >>= f =
        let
            Context x2 q y2 = f p
        in
            Context (x1 ++ x2) q (y2 ++ y1)

foo :: a -> Context String
foo _ = b >> (return "Inside")

bar :: () -> Context String
bar () = a >>= foo

main = do
    print $ bar ()

Qui produit:

Context ["Enter A","Enter B"] "Inside" ["Exit B","Exit A"]

Et l'ordre est le même.

L'analogie entre les deux programmes est très direct: un Context a certains "d'entrer" dans bits, un "corps", et "sortie" de bits. J'ai utilisé des Cordes à la place de IO actions car il est plus facile -- je pense qu'il devrait être similaire avec IO actions (corrigez-moi si elle n'est pas).

Et >>= pour Context fait exactement ce qu' with Python: il exécute le la saisie des instructions, nourrit la valeur de l' body, et exécute la sortie des déclarations.

(Il y a un autre énorme différence, c'est que le corps doit être dépend de l' la saisie des instructions. Encore une fois je pense que devrait être fixible).

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X