2012-04-07 21 views
10

Estoy intentando componer seq-m y error-m para hacer una lista de las cosas que pueden devolver errores. Mi salida tiene tipos inesperados, aunque aparte de eso, en realidad parece ser sensato. Exploté mi código a continuación, pero aquí también está working gist.combinando mónadas de may y seq: confundidas en la salida

aquí es mi lógica de negocio monádico

def get_loan(name): 
    m_qualified_amounts = (
      bind(get_banks(name), lambda bank: 
      bind(get_accounts(bank, name), lambda account: 
      bind(get_balance(bank, account), lambda balance: 
      bind(get_qualified_amount(balance), lambda qualified_amount: 
        unit(qualified_amount)))))) 
    return m_qualified_amounts 

names = ["Irek", "John", "Alex", "Fred"] 
for name, loans in zip(names, map(get_loan, names)): 
    print "%s: %s" % (name, loans) 

salida

Irek: [None, 'Insufficient funds for loan, current balance is 35000', None, 'Insufficient funds for loan, current balance is 70000', None, 'Unable to get balance due to technical issue for Wells Fargo: 3'] 
John: [None, 'Insufficient funds for loan, current balance is 140000'] 
Alex: [[245000], None, [280000], None] 
Fred: (None, 'No bank associated with name Fred') 

espero ver una lista de tuplas - la lista es el resultado de la lista por comprensión, y cada elemento de la lista definitiva debe ser un valor en mónada de error (value, error tupla). Es exactamente como si se eliminaran demasiados niveles de anidación por seq_bind.

aquí está mi definición de las mónadas, que si no es correcta, está muy cerca porque ambas mónadas trabajan de forma aislada, simplemente no combinadas.

def success(val): return val, None 
def error(why): return None, why 
def get_value(m_val): return m_val[0] 
def get_error(m_val): return m_val[1] 

# error monad 
def error_unit(x): return success(x) 
def error_bind(mval, mf): 
    assert isinstance(mval, tuple) 
    error = get_error(mval) 
    if error: return mval 
    else: return mf(get_value(mval)) 

def flatten(listOfLists): 
    "Flatten one level of nesting" 
    return [x for sublist in listOfLists for x in sublist]  

# sequence monad 
def seq_unit(x): return [x] 
def seq_bind(mval, mf): 
    assert isinstance(mval, list) 
    return flatten(map(mf, mval)) 

# combined monad !! 
def unit(x): return error_unit(seq_unit(x)) 
def bind(m_error_val, mf): 
    return error_bind(m_error_val, lambda m_seq_val: seq_bind(m_seq_val, mf)) 

API monádico

def get_banks(name): 
    if name == "Irek": return success(["Bank of America", "Wells Fargo"]) 
    elif name == "John": return success(["PNC Bank"]) 
    elif name == "Alex": return success(["TD Bank"]) 
    else: return error("No bank associated with name %s" % name) 

def get_accounts(bank, name): 
    if name == "Irek" and bank == "Bank of America": return success([1, 2]) 
    elif name == "Irek" and bank == "Wells Fargo": return success([3]) 
    elif name == "John" and bank == "PNC Bank": return success([4]) 
    elif name == "John" and bank == "Wells Fargo": return success([5, 6]) 
    elif name == "Alex" and bank == "TD Bank": return success([7, 8]) 
    else: return error("No account associated with (%s, %s)" % (bank, name)) 

def get_balance(bank, account): 
    if bank == "Wells Fargo": 
     return error("Unable to get balance due to technical issue for %s: %s" % (bank, account)) 
    else: 
     return success([account * 35000]) #right around 200,000 depending on acct number 

def get_qualified_amount(balance): 
    if balance > 200000: 
     return success([balance]) 
    else: 
     return error("Insufficient funds for loan, current balance is %s" % balance) 

también en busca de maneras de mejorar el código. etiquetado haskell y clojure porque esto es idiomático en estos idiomas, la comunidad python no está interesada en esto.

+0

Bueno, aquí hay un pythonista al menos que está interesado. ¿Qué pasó con tu blog, Dustin? –

Respuesta

8

Combinando mónadas apilando como este es, en Haskell, utilizando Monad Transformers. Dejar de lado el punto de Daniel Wagner de que ListT no es una mónada por el momento.Tiene dos mónadas con tipos:

  1. List a que se parece a [x,y,z]
  2. (Error e) a que parece x, None o None, err

Si convierte una a un transformador mónada y combinarlos, hay dos maneras:

  1. (ErrorT e) List a que se parece a [ (x,None), (y,None), (None, err) ]
  2. ListT (ErrorT e) a que se parece a [x,y,z], None o None, [x,y,z]

Querías una lista de pares, por lo que espero desea que la primera forma. Pero su simple prueba no está de acuerdo con esto. Su unit no devuelve una lista de pares como en (1.) sino un par de la lista y Ninguno que es (2.).

Por lo tanto, tiene las cosas al revés o tiene una mónada más complicada en mente. Trataré de modificar tu esencia para que se vea como (1.).

creo que este código podría hacer lo que quiera:

def flatten(listOfLists): 
    "Flatten one level of nesting" 
    assert isinstance(listOfLists, list) 
    if len(listOfLists) > 0: 
     assert isinstance(listOfLists[0], list) 
    return [x for sublist in listOfLists for x in sublist] 

# sequence monad 
def seq_unit(x): return [x] 
def seq_bind(mval, mf): return flatten(map(mf, mval)) 

# Decompose ErrorT e m a 
def get_value(m_val): return m_val[0] 
def get_error(m_val): return m_val[1] 

# hard coded "(ErrorT e) List a" instance of throwError, note that seq_unit is hardcoded 
def error_throwError(err): return (None, err) 
def errorT_list_throwError(err): return seq_unit(error_throwError(err)) 

# "(ErrorT e) List a" monad 
def error_unit(x): return (x,None) 
def errorT_list_unit(x): return seq_unit(error_unit(x)) 

def error_bind(mval, mf): 
    assert isinstance(mval, tuple) 
    error = get_error(mval) 
    if error: 
     return error_throwError(error) 
    else: 
     return mf(get_value(mval)) 

# Cannot have multi-line lambda 
def errorT_list_bind_helper(mval, mf): 
    assert isinstance(mval, tuple) 
    error = get_error(mval) 
    if error: 
     return errorT_list_throwError(error) 
    else: 
     return mf(get_value(mval)) 

def errorT_list_bind(mval, mf): return seq_bind(mval, lambda v: errorT_list_bind_helper(v, mf)) 

# combined monad !! (ErrorT e) List a 
unit = errorT_list_unit 
bind = errorT_list_bind 
throwError = errorT_list_throwError 

# hard coded "lift :: List a -> (ErrorT e) List a" 
def lift(mval): 
    assert isinstance(mval, list) 
    # return [ (val,None) for val in mval ] 
    # return [ errorT_list_unit(val) for val in mval ] 
    return seq_bind(mval, lambda v : unit(v)) 

def get_banks(name): 
    if name == "Irek": return lift(["Bank of America", "Wells Fargo"]) 
    elif name == "John": return unit("PNC Bank") 
    elif name == "Alex": return unit("TD Bank") 
    else: return throwError("No bank associated with name %s" % name) 

def get_accounts(bank, name): 
    if name == "Irek" and bank == "Bank of America": return lift([1, 2]) 
    elif name == "Irek" and bank == "Wells Fargo": return unit(3) 
    elif name == "John" and bank == "PNC Bank": return unit(4) 
    elif name == "John" and bank == "Wells Fargo": return lift([5, 6]) 
    elif name == "Alex" and bank == "TD Bank": return lift([7, 8]) 
    else: return throwError("No account associated with (%s, %s)" % (bank, name)) 

def get_balance(bank, account): 
    if bank == "Wells Fargo": 
     return throwError("Unable to get balance due to technical issue for %s: %s" % (bank, account)) 
    else: 
     return unit(account * 35000) #right around 200,000 depending on acct number 

def get_qualified_amount(balance): 
    if balance > 200000: 
     return unit(balance) 
    else: 
     return throwError("Insufficient funds for loan, current balance is %s" % balance) 

# monadic business logic 
def get_loan(name): 

    m_qualified_amounts = (
      bind(get_banks(name), lambda bank: 
      bind(get_accounts(bank, name), lambda account: 
      bind(get_balance(bank, account), lambda balance: 
      bind(get_qualified_amount(balance), lambda qualified_amount: 
        unit(qualified_amount)))))) 

    assert isinstance(m_qualified_amounts, list) 
    assert isinstance(m_qualified_amounts[0], tuple) 
    return m_qualified_amounts 

names = ["Irek", "John", "Alex", "Fred"] 

for name, loans in zip(names, map(get_loan, names)): 
    print "%s: %s" % (name, loans) 

salida es

Irek: [(None, 'Insufficient funds for loan, current balance is 35000'), (None, 'Insufficient funds for loan, current balance is 70000'), (None, 'Unable to get balance due to technical issue for Wells Fargo: 3')] 
John: [(None, 'Insufficient funds for loan, current balance is 140000')] 
Alex: [(245000, None), (280000, None)] 
Fred: [(None, 'No bank associated with name Fred')] 
8

No soy un experto en Python, pero esta definición:

def bind(mval, mf): 
    return error_bind(mval, lambda mval: seq_bind(mval, mf)) 

... me hace muy sospechoso. Presumiblemente, se supone que mf devuelve algo que está incluido en los tipos de mónada error y seq, con error -ness outermost; sin embargo, lo está pasando al seq_bind, que espera una función que devuelve algo con seq -ness outermost.

Puede que desee echar un vistazo a la fuente de los transformadores de mónada ErrorT y LogicT en Haskell para tener una idea de cómo se puede hacer esto correctamente. (Es posible encontrar LogicT sorprendentemente complicado en comparación con lo que se esperaba - esto se debe a los ingenuos ListTisn't actually a monad transformer!)

+1

Esa sugerencia fue muy útil, muchas gracias. Estoy traduciendo transformadores de mónada para Frege (http://code.google.com/p/frege/), y encontré la advertencia en el "viejo" ListT "bastante alarmante. Es bueno saber la versión correcta. – Landei

+2

Vea también [este comentario en Haskell Reddit] (http://www.reddit.com/r/haskell/comments/ryo5t/combining_monads_in_python_wtf_is_wrong_with_my/c49p72l) por Tekmo. – dave4420

4

Nota: La gente en Reddit me pidieron volver a publicar mi comentario aquí como una respuesta.

La respuesta de Daniel Wagner, pero la detallaré aquí ya que esto no encajará en un comentario de Desbordamiento de pila.

En primer lugar, debería leer Monad Transformers - Step by Step si aún no lo hizo.

Ahora, se esperaría que el tipo de su mónada combinado para ser (usando la notación de Haskell):

type Combined r = ListT (Either e) r 

Si no entienden por qué ListT está en el exterior, y luego ir sobre el papel Monad Transformers Me he vinculado anteriormente antes de continuar. Recuerde que si tuviera que runListT un valor de tipo Combined r, me gustaría conseguir algo como:

-- Actually, this is WRONG, but see below for the warning about ListT 
runListT (x :: ListT (Either e) r) :: Either e [r] 

Con base en el tipo de Combined r, podemos inferir que el tipo correcto de (>>=) en el Combined mónada sería:

(>>=) :: ListT (Either e) a -> (a -> ListT (Either e) b) -> ListT (Either e) b 

Así que ahora voy a fingir que soy el compilador GHC dotado de la capacidad de compilar el código Python y tratar de ir a través de su función de bind e inferir el tipo de todo.Me inferir del tipo anterior para (>>=), que el tipo de los argumentos sería:

mval :: ListT (Either e) a 
mf :: a -> ListT (Either e b) 

Entonces miro seq_bind, que debe tener inferir el tipo:

seq_bind :: ListT (Either e) a -> (a -> ListT (Either e) b) -> c 

... donde c aún no se ha determinado. Ya que su código no-tipo de verificación (suponiendo pitón tenía una cosa tal como los tipos), ya que el tipo de seq_bind se supone que es:

seq_bind :: [a] -> (a -> [b]) -> [b] 

No se puede utilizar un ListT, donde una función espera una lista , entonces ese es tu primer problema. De hecho, no puede derivar el enlace ListT del enlace List, en absoluto. Esto es cierto para (casi) todos los transformadores de mónada.

Sin embargo, puede derivar la ListT (Either e) se unen de la vinculación para Either e, y más en general, se puede derivar la vinculación para (Monad m) => ListT m sin saber nada de lo que mónada de base que se ajuste a no ser que tenga un funcionamiento (>>=) y return que obedecen las leyes de la mónada

Sin embargo, es no trivial para escribir una implementación correcta ListT y muchas almas valientes lo han entendido mal. De hecho, el ListT que viene con los paquetes de transformadores mónada estándar de Haskell es incorrecto y no es un transformador monad ni un monad. La aplicación correcta, que fuertemente estoy de acuerdo, es la que se da aquí:

ListT done right

Debe cuna de ese código (que es un poco feo, pero el 100% correcto) para escribir un adecuado transformador ListT mónada. No se sienta tentado a escribir un transformador de mónada que devuelva la lista de una sola vez: le garantizo que no funcionará y no puede funcionar.

+0

Como escribió "espero ver listas de tuplas, la lista es el resultado de la lista de comprensión, y cada elemento de la lista final debe tener un valor en mónada de error (valor, tupla de error)". Creo que quiere que las mónadas se apilen en el otro orden. –

+1

Sí. Estaba basando en el orden en que aplicó sus dos unidades y el orden de sus ataduras, no en función de lo que dijo que quería, que era exactamente lo contrario. –