Объединение возможно и seq monads: путают на выходе

Я пытаюсь составить seq-m и error-m, чтобы сделать список понятий о вещах, которые могут возвращать ошибки. Мой вывод имеет неожиданные типы, хотя и не похоже на то, что он действительно кажется разумным. я взорвал код ниже, но здесь рабочий стиль.

вот моя монадическая бизнес-логика

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)

Выход

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

Я ожидаю увидеть списки кортежей - список является результатом понимания списка, и каждый элемент в конечном списке должен быть значением в error-monad (value, error tuple). Его точно так же, как если бы один или несколько уровней гнездования были удалены с помощью seq_bind.

вот мое определение монадов, которое, если оно не правильно, очень близко, потому что обе монады работают изолированно, просто не объединены.

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)

также ищет способы улучшить код. tagged haskell и clojure, потому что это идиоматично на этих языках, сообщество python этого не интересует.

Ответ 1

Объединение монад путем укладки как в Haskell, используя Monad Transformers. Отложите в сторону Даниэля Вагнера, указывайте, что ListT не является монадой на мгновение. У вас есть две монады с типами:

  • List a, который выглядит как [x,y,z]
  • (Error e) a, который выглядит x, None или None, err

Если вы конвертируете один в монадный трансформатор и объединяете их, существует два способа:

  • (ErrorT e) List a, который выглядит как [ (x,None), (y,None), (None, err) ]
  • ListT (ErrorT e) a, который выглядит как [x,y,z], None или None, [x,y,z]

Вам нужен список пар, поэтому я ожидаю, что вам нужна первая форма. Но ваш простой тест не согласен с этим. Ваш unit не возвращает список пар, как в (1.), а пару списка и None, который является (2.).

Итак, у вас либо есть вещи назад, либо у вас есть более сложная монада. Я попытаюсь изменить свой смысл, чтобы он выглядел как (1.).

Я думаю, что этот код может делать то, что вы хотите:

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)

Выход

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

Ответ 2

Я не эксперт Python, но это определение:

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

... делает меня очень подозрительным. Предположительно, предполагается, что mf возвращает что-то, что завернуто в типы монады error и seq, с error -ness outermost; однако вы передаете его на seq_bind, который ожидает функцию, которая возвращает что-то с seq -ness outermost.

Вам может понравиться источник ErrorT и LogicT monad-трансформаторы в Haskell, чтобы понять, как это можно сделать правильно. (Вы можете найти LogicT удивительно сложный по сравнению с тем, что вы ожидали - это потому, что наивный ListT на самом деле не является монадным трансформатором!)

Ответ 3

Примечание. Люди из reddit попросили меня переписать мой комментарий здесь как ответ.

Ответ Дэниела Вагнера, но я расскажу об этом здесь, так как это не будет соответствовать комментарию Stack Overflow.

Прежде всего, вы должны прочитать Monad Transformers - шаг за шагом, если вы еще этого не сделали.

Теперь вы ожидаете, что тип вашей объединенной монады (используя нотацию Haskell):

type Combined r = ListT (Either e) r

Если вы не понимаете, почему ListT находится снаружи, перейдите к вышеперечисленной выше статье Monad Transformers, которую я связал выше. Помните, что если бы я был runListT значением типа Combined r, я бы получил что-то вроде:

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

В зависимости от типа Combined r мы можем заключить, что правильный тип (>>=) в монаде Combined был бы:

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

Итак, теперь я буду притворяться, что я - компилятор GHC, наделенный способностью компилировать код Python и пытаюсь пройти через вашу функцию bind и вывести все типы. Я бы сделал вывод из вышеприведенного типа для (>>=), что тип аргументов будет:

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

Затем я смотрю на seq_bind, который, как я полагаю, должен иметь тип:

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

... где c еще не определено. Уже ваш код не проверяет тип (предполагая, что у Python есть такие вещи, как типы), поскольку тип seq_bind должен быть:

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

Вы не можете использовать ListT, где функция ожидает список, так что ваша первая проблема. На самом деле вы не можете получить привязку ListT от связывания List вообще. Это верно для (почти) всех монадных трансформаторов.

Однако вы можете получить ListT (Either e) привязку из привязки для Either e, и в более общем плане вы можете получить привязку для (Monad m) => ListT m, не зная ничего о том, какая базовая монада вы кроме того, что он имеет операцию (>>=) и return, которая подчиняется законам монады.

Однако, это не тривиально, чтобы написать правильную реализацию ListT, и многие храбрые души ошибались. На самом деле ListT, который поставляется с стандартными пакетами трансформаторов монадов Haskell, неверен и не является монадом или монадным трансформатором. Правильная реализация, которую я сильно одобряю, такова, что здесь:

ListT done right

Вы должны вырезать из этого кода (который немного уродливый, но на 100% правильный), чтобы написать правильный ListT monad transformer. Не будьте соблазн написать монадный трансформатор, который сразу возвращает список: я гарантирую, что он не будет и не сможет работать.