Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

combining maybe and seq monads: confused at the output

I'm trying to compose seq-m and error-m to do list comprehensions on things that can return errors. My output has unexpected types, though other than that it does actually seem to be sensible. i've exploded my code below, but here is a working gist as well.

here is my 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))))))
    return m_qualified_amounts

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

output

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')

i expect to see lists of tuples - the list is the result of the list comprehension, and each item in the final list should be a value in error-monad (value, error tuple). Its exactly as if one-too-many levels of nesting were removed by seq_bind.

here is my definition of the monads, which if its not correct, its very close because both monads work in isolation, just not combined.

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))

monadic API

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)

also looking for ways to improve the code. tagged haskell and clojure because this is idiomatic in these languages, the python community isn't interested in this.

like image 391
Dustin Getz Avatar asked Apr 07 '12 23:04

Dustin Getz


2 Answers

Combining monads by stacking like this is, in Haskell, using Monad Transformers. Set aside Daniel Wagner's point that ListT is not a monad for moment. You have two monads with types:

  1. List a which looks like [x,y,z]
  2. (Error e) a which looks x, None or None, err

If you convert one to a monad transformer and combine them, there are two ways:

  1. (ErrorT e) List a which looks like [ (x,None), (y,None), (None, err) ]
  2. ListT (ErrorT e) a which looks like [x,y,z], None or None, [x,y,z]

You wanted a list of pairs, so I expect you want the first form. But your simple test does not agree with this. Your unit does not return a list of pairs as in (1.) but a pair of the list and None which is (2.).

So you either have things backwards or you have a more complicated monad in mind. I will try and modify your gist to look like (1.).

I think this code might do what you want:

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)

Output is

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')]
like image 85
Chris Kuklewicz Avatar answered Sep 30 '22 09:09

Chris Kuklewicz


I am not a Python expert, but this definition:

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

...makes me very suspicious. Presumably, mf is supposed to return something that's wrapped in both the error and seq monad types, with the error-ness outermost; however, you're passing it to seq_bind, which expects a function that returns something with the seq-ness outermost.

You might like to take a look at the source of the ErrorT and LogicT monad transformers in Haskell to get an idea of how this might be done correctly. (You might find LogicT surprisingly complicated compared to what you were expecting -- this is because the naive ListT isn't actually a monad transformer!)

like image 32
Daniel Wagner Avatar answered Sep 30 '22 09:09

Daniel Wagner