首页 文章

结合也许和seq monads:在输出中混淆

提问于
浏览
10

我在下面展开了我的代码,但这里也是working gist .

这是我的monadic业务逻辑

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 元组)中的值 . 它就像是 seq_bind 删除了太多级别的嵌套 .

这是我对monads的定义,如果它不正确,它非常接近,因为两个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))

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)

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

3 回答

  • 4

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

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

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

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

    type Combined r = ListT (Either e) r
    

    如果您不明白为什么 ListT 在外面,那么在继续之前,请查看我上面链接的Monad变形金刚论文 . 请记住,如果我 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 monad中正确的 (>>=) 类型是:

    (>>=) :: 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 ,以便's your first problem. In fact, you can' t从 List 绑定派生 ListT 的绑定 . 对于(几乎)所有monad变换器都是如此 .

    但是,你 canEither e 的绑定派生 ListT (Either e) 绑定,更一般地,你可以派生 (Monad m) => ListT m 的绑定,而不知道你正在包装什么基础monad,除了它有一个遵守monad定律的 (>>=)return 操作 .

    然而,写一个正确的 ListT 实现是微不足道的,许多勇敢的灵魂都弄错了 . 事实上,Haskell的标准monad变压器包附带的 ListTwrong ,既不是monad也不是monad变换器 . 我强烈支持的正确实施是这里给出的:

    ListT done right

    你应该从那个代码(这有点难看,但100%正确)中编写一个适当的 ListT monad变换器 . 不要试图写一个monad变换器一次性返回列表:我保证你不会也不能工作 .

  • 8

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

    • List a ,看起来像 [x,y,z]

    • (Error e) a 看起来 x, NoneNone, err

    如果将one转换为monad变换器并将它们组合,有两种方法:

    • (ErrorT e) List a ,看起来像 [ (x,None), (y,None), (None, err) ]

    • ListT (ErrorT e) a ,看起来像 [x,y,z], NoneNone, [x,y,z]

    你想要一个对列表,所以我希望你想要第一个表格 . 但是你的简单测试并不同意这一点 . 您的 unit 不会返回(1.)中的对列表,而是返回一对列表和None,即(2.) .

    所以你要么倒退,要么你有一个更复杂的monad . 我会尝试修改你的要点看起来像(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')]
    
  • 8

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

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

    ......让我非常怀疑 . 据推测, mf 应该返回包含在 errorseq monad类型中的东西,其中 error -最外层;但是,你将它传递给 seq_bind ,它需要一个返回 seq -ness最外层的函数 .

    您可能希望查看Haskell中ErrorTLogicT monad转换器的来源,以了解如何正确完成此操作 . (你可能会发现 LogicT 比你期望的要复杂得多 - 这是因为天真的 ListT isn't actually a monad transformer!)

相关问题