10

我正在尝试编写 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')

我希望看到元组列表 - 该列表是列表理解的结果,最终列表中的每个项目都应该是错误单子(value, error元组)中的一个值。就像删除了太多的嵌套级别一样seq_bind

这是我对 monad 的定义,如果它不正确,那么它非常接近,因为两个 monad 都是孤立地工作的,而不是结合起来的。

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

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)

也在寻找改进代码的方法。标记 haskell 和 clojure 因为这是这些语言的惯用语,python 社区对此不感兴趣。

4

3 回答 3

8

我不是 Python 专家,但这个定义:

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

……让我很怀疑。据推测,mf应该返回包含在errorseqmonad 类型中的东西,其中error-ness 最外层;但是,您将它传递给seq_bind,它需要一个返回带有seq-ness 最外层的东西的函数。

您可能想查看 Haskell 中的ErrorTLogicTmonad 转换器的来源,以了解如何正确完成此操作。(LogicT与您的预期相比,您可能会发现令人惊讶的复杂——这是因为天真ListT 的实际上并不是一个单子转换器!)

于 2012-04-07T23:48:01.840 回答
8

在 Haskell 中,像这样通过堆叠组合 monad 是使用Monad Transformers。抛开 Daniel Wagner 的观点,即 ListT 暂时不是单子。你有两个类型的 monad:

  1. List a看起来像[x,y,z]
  2. (Error e) a看起来x, NoneNone, err

如果将一个转换为 monad 转换器并将它们组合起来,有两种方法:

  1. (ErrorT e) List a看起来像[ (x,None), (y,None), (None, err) ]
  2. ListT (ErrorT e) a看起来像[x,y,z], NoneNone, [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')]
于 2012-04-08T07:15:07.733 回答
4

注意:reddit 上的人要求我在此处重新发布我的评论作为答案。

Daniel Wagner 的答案,但我会在这里详细说明,因为这不适合 Stack Overflow 评论。

首先,如果您还没有阅读 Monad Transformers - Step by Step,您应该阅读。

现在,您会期望组合 monad 的类型是(使用 Haskell 表示法):

type Combined r = ListT (Either e) r

如果你不明白为什么ListT在外面,那么在继续之前阅读我上面链接的 Monad Transformers 论文。请记住,如果我要runListT一个 type 的值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,我们可以推断出 monad 中的正确类型(>>=)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)从 bind for派生绑定Either e,更一般地说,您可以在(Monad m) => ListT m不知道要包装的基本 monad 的任何内容的情况下派生 bind for ,除了它具有遵守 monad 法则的(>>=)and操作。return

但是,编写正确的实现并非易事ListT,许多勇敢的人都弄错了。事实上ListT,Haskell 的标准 monad 转换器包附带的是错误的,它既不是 monad 也不是 monad 转换器。我强烈支持的正确实现是这里给出的:

ListT 做得对

您应该从该代码(有点难看,但 100% 正确)中编写一个合适的ListTmonad 转换器。不要试图编写一个一次性返回列表的 monad 转换器:我向你保证它不会也不能工作。

于 2012-04-08T14:31:44.513 回答