¿Python es "con" monádico?
functional-programming monads (4)
Es casi demasiado trivial mencionar, pero el primer problema es que with
no es una función y no toma una función como argumento. Puede solucionar esto escribiendo un contenedor de funciones with
:
def withf(context, f):
with context as x:
f(x)
Dado que esto es tan trivial, no podrías molestarte en distinguir withf
y with
.
El segundo problema con ser una mónada es que, como una declaración en lugar de una expresión, no tiene un valor. Si pudieras darle un tipo, sería M a -> (a -> None) -> None
(este es en realidad el tipo de withf
arriba). Hablando de manera práctica, puede usar el _
de Python para obtener un valor para la declaración with
. 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)
Como withf
usa una función en lugar de un bloque de código, una alternativa a _
es devolver el valor de la función:
def withf(context, f):
with context as x:
return f(x)
Hay otra cosa que previene with
(y con withf
) ser un enlace monádico. El valor del bloque tendría que ser un tipo monádico con el mismo tipo de constructor que el elemento with
. Como es, with
es más genérico. Teniendo en cuenta la nota de agf de que cada interfaz es un constructor de tipo, vinculo el tipo de como M a -> (a -> b) -> b
, donde M es la interfaz del administrador de contexto (los métodos __enter__
y __exit__
). Entre los tipos de bind
y with
está el tipo M a -> (a -> N b) -> N b
. Para ser una mónada, with
tendría que fallar en el tiempo de ejecución cuando b
no era M a
. Por otra parte, si bien podría usarse with
una operación de enlace monádica, rara vez tendría sentido hacerlo.
La razón por la que necesita hacer estas sutiles distinciones es que si, por error, considera que es monádico, terminará utilizándolo mal y escribiendo programas que fallarán debido a errores de tipo. En otras palabras, escribirás basura. Lo que debes hacer es distinguir una construcción que es una cosa particular (por ejemplo, una mónada) de una que se puede usar a la manera de esa cosa (por ejemplo, una mónada). El último requiere disciplina por parte de un programador, o la definición de construcciones adicionales para hacer cumplir la disciplina. Aquí hay una versión casi monádica de with
(el tipo es M a -> (a -> b) -> M b
):
def withm(context, f):
with context as x:
return type(context)(f(x))
En el análisis final, se podría considerar with
ser como un combinador, pero uno más general que el combinador requerido por las mónadas (que es un enlace). Puede haber más funciones usando las mónadas que las dos requeridas (la lista mónada también tiene contras, anexos y longitud, por ejemplo), por lo que si define el operador de enlace apropiado para los administradores de contexto (como withm
), entonces podría ser monádico en el sentido de involucrar mónadas
Como muchos pioneros temerarios antes que yo, me esfuerzo por cruzar el páramo sin caminos que es Understanding Monads.
Todavía estoy tambaleándome, pero no puedo dejar de notar una cierta calidad de tipo mónada sobre la declaración de Python. Considera este fragmento:
with open(input_filename, ''r'') as f:
for line in f:
process(line)
Considere la llamada open()
como la "unidad" y el bloque en sí mismo como el "enlace". La mónada real no está expuesta (uh, a menos que f
sea la mónada), pero el patrón está ahí. ¿No es así? ¿O simplemente estoy confundiendo todo FP con monadry? ¿O son solo las 3 de la mañana y cualquier cosa parece plausible?
Una pregunta relacionada: si tenemos mónadas, ¿necesitamos excepciones?
En el fragmento anterior, cualquier falla en la E / S se puede ocultar del código. La corrupción del disco, la ausencia del archivo nombrado y un archivo vacío se pueden tratar de la misma manera. Entonces no hay necesidad de una excepción IO visible.
Ciertamente, la clase de tipos de Option
Scala ha eliminado la temida Null Pointer Exception
. Si reconsideraste los números como Mónadas (con NaN
y DivideByZero
como casos especiales) ...
Como dije, 3 de la mañana.
Haskell tiene un equivalente de with
para archivos, se llama withFile
. Esta:
with open("file1", "w") as f:
with open("file2", "r") as g:
k = g.readline()
f.write(k)
es equivalente a:
withFile "file1" WriteMode $ /f ->
withFile "file2" ReadMode $ /g ->
do k <- hGetLine g
hPutStr f k
Ahora, con withFile
podría parecer algo monádico. Su tipo es:
withFile :: FilePath -> IOMode -> (Handle -> IO r) -> IO r
el lado derecho se ve como (a -> mb) -> mb
.
Otra similitud: en Python puede omitir as
, y en Haskell puede usar >>
lugar de >>=
(o, un bloque do
sin <-
flecha).
Así que contestaré esta pregunta: ¿es withFile
?
Se podría pensar que se puede escribir así:
do f <- withFile "file1" WriteMode
g <- withFile "file2" ReadMode
k <- hGetLine g
hPutStr f k
Pero esto no marca el cheque. Y no puede.
Es porque en Haskell la mónada IO es secuencial : si escribes
do x <- a
y <- b
c
después de que se ejecuta a, b
se ejecuta y luego c
. No hay un "retroceso" para limpiar a
final o algo así. withFile
, por otro lado, tiene que cerrar el identificador después de que se ejecuta el bloque.
Hay otra mónada, llamada mónada de continuación, que permite hacer tales cosas. Sin embargo, ahora tiene dos mónadas, IO y continuaciones, y usar los efectos de dos mónadas a la vez requiere el uso de transformadores de mónada.
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
Eso es feo. Así que la respuesta es: algo, pero eso requiere lidiar con muchas sutilezas que hacen que el problema sea bastante opaco.
Python''s with
puede simular solo un poco de lo que las mónadas pueden hacer: agregar código de entrada y finalización. No creo que puedas simular, por ejemplo.
do x <- [2,3,4]
y <- [0,1]
return (x+y)
usando with
(podría ser posible con algunos hacks sucios). En cambio, use para:
for x in [2,3,4]:
for y in [0,1]:
print x+y
Y hay una función de Haskell para esto - forM
:
forM [2,3,4] $ /x ->
forM [0,1] $ /y ->
print (x+y)
Recomendé leer sobre el yield
que se parece más a las mónadas que with
: http://www.valuedlessons.com/2008/01/monads-in-python-with-nice-syntax.html
Una pregunta relacionada: si tenemos mónadas, ¿necesitamos excepciones?
Básicamente no, en lugar de una función que lanza A o devuelve B, puede crear una función que devuelva Either AB
. La mónada de Either A
se comportará como excepciones, si una línea de código devuelve un error, todo el bloque lo hará.
Sin embargo, eso significaría que la división tendría tipo Integer -> Integer -> Either Error Integer
y así sucesivamente, para tomar la división por cero. Debería detectar errores (coincidencia de patrón explícita o uso de enlace) en cualquier código que use división o que tenga la más mínima posibilidad de fallar. Haskell usa excepciones para evitar hacer esto.
He pensado innecesariamente en esto y creo que la respuesta es "sí, cuando se usa de cierta manera" (gracias :), pero no por la razón que pensé antes.
Mencioné en un comentario a la respuesta de agf , que >>=
es solo un estilo de continuación de paso: darle un productor y una devolución de llamada y "ejecuta" al productor y lo envía a la devolución de llamada. Pero eso no es del todo cierto. También es importante que >>=
tiene que ejecutar alguna interacción entre el productor y el resultado de la devolución de llamada.
En el caso de la mónada List, esto sería listas concatenadas. Esta interacción es lo que hace que las mónadas sean especiales.
Pero creo que Python''s with
sí hace esta interacción, pero no de la manera que cabría esperar.
Aquí hay un ejemplo de un programa de Python que emplea dos with
declaraciones:
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()
Cuando se ejecuta la salida es:
Enter A
Enter B
Inside
Exit B
Exit A
Ahora, Python es un lenguaje imperativo , así que en lugar de simplemente producir datos, produce efectos secundarios. Pero puedes pensar en esos efectos secundarios como datos (como IO ()
); no puedes combinarlos de todas las maneras geniales en que podrías combinar IO ()
, pero están alcanzando el mismo objetivo.
Entonces, lo que debe enfocarse es la secuencia de esas operaciones, es decir, el orden de las instrucciones de impresión.
Ahora compare el mismo programa 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 ()
Lo que produce:
Context ["Enter A","Enter B"] "Inside" ["Exit B","Exit A"]
Y el orden es el mismo.
La analogía entre los dos programas es muy directa: un Context
tiene algunos bits "entrantes", un "cuerpo" y algunos bits "salientes". Utilicé String
lugar de acciones IO
porque es más fácil, creo que debería ser similar con las acciones IO
(corríjanme si no es así).
Y >>=
para Context
hace exactamente lo mismo que en Python: ejecuta las instrucciones que ingresan, alimenta el valor al body
y ejecuta las declaraciones de salida.
(Hay otra gran diferencia, que es que el cuerpo debe depender de las declaraciones que entran. De nuevo, creo que eso debería ser posible).
Sí.
Justo debajo de la definición, Wikipedia dice :
En términos de programación orientada a objetos, la construcción de tipo correspondería a la declaración del tipo monádico, la función de unidad asume el papel de un método constructor y la operación de enlace contiene la lógica necesaria para ejecutar sus devoluciones de llamada registradas (las funciones monádicas).
Esto me suena exactamente como el protocolo del administrador de contexto, la implementación del protocolo del administrador de contexto por el objeto y la declaración with
.
De @Owen en un comentario en esta publicación:
Las mónadas, en su nivel más básico, son más o menos una forma genial de usar el estilo de continuación de paso: >> = toma un "productor" y una "devolución de llamada"; esto también es básicamente lo que ocurre con: un productor como open (...) y un bloque de código que se llamará una vez que se haya creado.
La definición completa de Wikipedia:
Una construcción de tipo que define, para cada tipo subyacente, cómo obtener un tipo monádico correspondiente. En la notación de Haskell, el nombre de la mónada representa el constructor de tipo. Si M es el nombre de la mónada y t es un tipo de datos, entonces "M t" es el tipo correspondiente en la mónada.
Esto me suena como el protocolo de administrador de contexto .
Una función de unidad que asigna un valor en un tipo subyacente a un valor en el tipo monádico correspondiente. El resultado es el valor "más simple" en el tipo correspondiente que conserva por completo el valor original (entendiéndose la simplicidad de forma apropiada para la mónada). En Haskell, esta función se llama retorno debido a la forma en que se usa en la notación de do que se describe más adelante. La función de la unidad tiene el tipo polimórfico t → M t.
La implementación real del protocolo del gestor de contexto por parte del objeto.
Una operación vinculante de tipo polimórfico (M t) → (t → M u) → (M u), que Haskell representa mediante el operador infijo >> =. Su primer argumento es un valor en un tipo monádico, su segundo argumento es una función que se correlaciona desde el tipo subyacente del primer argumento a otro tipo monádico, y su resultado se encuentra en ese otro tipo monádico.
Esto corresponde a la declaración with
y su suite.
Entonces sí, yo diría que es una mónada. Busqué PEP 343 y todas las PEP relacionadas rechazadas y retiradas, y ninguna mencionó la palabra "mónada". Sin duda, se aplica, pero parece que el objetivo de la declaración with
era la gestión de recursos, y una mónada es solo una forma útil de obtenerla.