绑定可以由fmap和join组成,那么我们是否必须使用monadic函数a-> m b?

时间:2018-07-17 08:14:48

标签: haskell math functional-programming monads functor

我不常使用Haskell,但我了解Monads的概念。

我对Kleisli triple和类别感到迷惑

fmap and join

  

尽管Haskell根据return和bind函数定义了monad,但也可以根据return和另外两个操作joinfmap定义monad。这种表述与范畴论中单子的最初定义更加吻合。类型为fmap的{​​{1}}操作采用两种类型之间的函数,并产生对monad中的值执行“相同操作”的函数。类型为(t → u) → M t → M u的{​​{1}}操作将两层信息“平化”为一层。

帮助我了解Monads的背景原理。

  

这两个表述如下:

join

我的问题是: 我认为,由于M (M t) → M t可以由fmap f m = m >>= (return . f) join n = n >>= id fmap :: (a -> b) -> (m a -> m b) unit :: a -> m a join :: m (m a) -> m a >>= :: m a -> (a -> m b) -> m b m >>= f = join $ fmap f m >>=组成,所以一个单子函数 fmap不是必需的,普通函数join可以满足操作要求,但是网络上的许多教程仍然坚持使用monadic函数,因为这是Kleisli三元组和monad-laws。

为简单起见,我们是否不应该仅使用非一元函数(只要它们是内函数)?我想念什么?

相关主题是

Monad join function

Haskell Monad bind operator confusion

Difference in capability between fmap and bind?

4 个答案:

答案 0 :(得分:9)

从某种意义上说,你是对的。由于每个单子m都是函子,因此我们可以将fmap f与函数f :: a -> b结合使用,将m a变成m b,但是有一个陷阱。什么是b

我喜欢将m理解为“计划获得”的意思,其中“计划”涉及除纯计算之外的某种其他交互。如果您有“计划获得Int”,并且想要“计划获得String”,则可以将fmapInt -> String中的函数一起使用,但是该函数的类型告诉您,从String获取Int不需要进行进一步的交互。

并非总是如此:也许Int是学生注册号,而String是他们的名字,所以从一个转换为另一个的计划需要在某个表中进行外部查找。然后,我没有从IntString的纯函数,而是从Int到“计划获得String”的纯函数。如果我在我的“计划获得fmap”中Int这么做,那很好,但是我最终得到了“计划获得(计划获得String)”并且我需要join外部和内部计划。

通常情况是,我们有足够的信息来计算计划以获得更多。这就是a -> m b模型。尤其是,我们拥有return :: a -> m a,它将我们所拥有的信息转变为计划,而无需采取进一步的行动就可以为我们提供准确的信息;而我们拥有(>=>) :: (a -> m b) -> (b -> m c) -> (a -> m c),它可以构成两件事。我们还拥有(>=>)是关联的并且在左右吸收return,这与;是关联的并且吸收skip的经典命令式编程一样。

使用这种组合方法从较小的计划中构建较大的计划更为方便,同时将“计划获得”层的数量保持一致。否则,您需要使用fmap来构建 n 层计划,然后在外部执行正确数量的join(这将是脆弱的属性)。计划)。

现在,因为Haskell是一种具有“自由变量”和“范围”概念的语言,所以{p {1}}

a

代表“总体输入信息”只是从我们已经拥有的事物的范围出发而已,

(>=>) :: (a -> m b) -> (b -> m c) -> (a -> m c)

然后我们得到“ bind”,它是一种以程序员最友好的形式呈现组成结构的工具,类似于本地定义。

总而言之,您可以使用(>>=) :: m b -> (b -> m c) -> m c ,但是通常您需要a -> b才能“计划得到一些东西”,这对于选择计划是很有帮助的选择组成上。

答案 1 :(得分:6)

我很难理解您的问题是什么,但是无论如何我都会努力解决。

  

我认为,由于>>=可以由fmapjoin组成,因此不需要一元函数a -> m b,普通函数a -> b将满足操作

我希望您在a -> m b的类型中引用“单函数>>=”,对吗?好吧,让我们看看用类型a -> b的函数替换它会发生什么:

(>>=) :: m a -> (a -> m b) -> m b  -- standard version
(>>=) :: m a -> (a -> b) -> m b    -- your version

但这看起来不熟悉吗?它等效于fmap :: (a -> b) -> m a -> m b,但参数已切换。实际上,实现只是x >>= y = fmap y x,不需要join。因此,您的答案是:如果您使用“普通函数a -> b”而不是“单子函数a -> m b”,则您将不再有单子。相反,您有一个Functor


  

但是网上很多教程仍然坚持使用monadic函数,因为那是Kleisli三元组和monad-laws。

我不确定您要查看哪些教程。以我的经验,教程的本质是他们坚持要使用任何教程。如果Monad的教程开始提出问题,然后提出除Monad之外的其他东西作为解决方案,那将很奇怪。至少,这超出了本教程的范围,并且使任何阅读它的人都浪费时间来了解Monad


  

为简单起见,我们是否不应该仅使用非一元函数(只要它们是内函数)?我想念什么?

内在功能是a -> a类型的功能。给定问题的上下文,我认为您实际上是指a -> b类型的 pure函数(“ pure”,与诸如readIORef之类的固有单子函数相反,它们需要类型a -> m b)。如果我的假设是错误的,请告诉我,我将编辑问题。

编辑:
如@duplode在评论中所建议,您可能是说 endofunctor ,在Haskell中,它只是任何类型的Functor。在这种情况下,以下内容仍然适用。

在不需要Monad的情况下,通常更简单地使用ApplicativeFunctor或仅使用基本的纯函数。在这些情况下,应该(通常)使用这些东西来代替Monad。例如:

ws <- getLine >>= return . words  -- Monad
ws <- words <$> getLine           -- Functor (much nicer)

要清楚:如果没有monad可以实现,并且如果没有monad则更简单易读,那么您应该在没有monad的情况下做到!如果monad使代码变得比所需复杂或混乱,请不要使用monad! Haskell具有monad的唯一目的是使某些复杂的计算更简单,更易于阅读且更易于推理。如果这没有发生,则您不应该使用monad

答案 2 :(得分:0)

没有理由。

  

我认为,由于>>=可以由fmapjoin组成,因此不需要一元函数a -> m b

是的,您完全正确。我们不需要>>=来代表单子,也可以要求join来代表单子。两者完全相等。由于我们也可以组成join = (>>= id),所以我们可以

class Monad m where
    return :: a -> m a
    fmap :: (a -> b) -> m a -> m b
    (=<<) :: (a -> m b) -> m a -> m b
    -- join = (=<<) id

class Monad m where
    return :: a -> m a
    fmap :: (a -> b) -> m a -> m b
    join :: m (m a) -> m a
    -- (=<<) = (join .) . fmap

我们使用哪一个都没关系。不可否认,后者看起来更简单,因为只有一个高阶函数(fmap),在前者中fmap=<<的类型看起来太相似了。 join可以更好地了解单子与函子的区别。

多功能

我们可以从>>=fmap派生join,但是我们只能从join派生>>=。实际上,我们甚至可以从fmap>>=导出return。因此,我们应该说>>=比另一个更基础吗?更有力?也许只是:更令人费解?

我们想写

data Maybe a = Just a | Nothing
implement Functor Maybe where
    fmap = (=<<) . (return .) -- maybe not even write this ourselves
implement Monad Maybe where
    return = Just
    f =<< Just x = f x
    _ =<< Nothing  = Nothing

data Maybe a = Just a | Nothing
implement Functor Maybe where
    fmap f (Just x) = Just (f x)
    fmap f Nothing  = Nothing
implement Monad Maybe where
    return x = Just x
    join (Just (Just x)) = Just x
    join (Just Nothing)  = Nothing
    join Nothing         = Nothing

以前使用>>=的解决方案很小。

方便

  

为简单起见,我们不应该只使用非一元函数吗?

不。定义monad类型类的整个想法是简化使用Monadic函数的工作。 return / fmap / join函数本身就毫无用处,我们感兴趣的是其他返回一元数据类型的函数,例如tryParse :: String -> Maybe Int

monads背后的整个想法是,我们可以任意地链接和嵌套它们,最后返回到普通类型。解析数字后,我们需要验证可选结果(为我们提供另一个可选结果)-在单子(fmap validate中输入 in ,然后将其取回。通常没有直接产生嵌套数据的操作,我们仅获得嵌套的monad类型,因为我们在monadic类型内进行了进一步的monadic操作。而且我们宁愿写

tryRead = (=<<) validate . tryParse

tryRead = join . fmap validate . tryParse

这就是>>=比起join在日常生活中使用monad更为重要的原因。我还要猜测,必须直接实现>>=而不是显式实现join并从中获得>>=,这样才能更好(更轻松)地优化编译器。

答案 3 :(得分:0)

Kleisli箭头a的组成-> [b]

 (a->mb) -> (b->mc) -> (a->mc)
    f          g  

a,b,c是任意类型,m始终相同,这里是[]

我们必须产生一个函数(a-> mc)
函数是用lambda产生的,我们有一个参数a。

f >=> g =  \a -> ..f..g..
           \a -> let mb = f a;
                 in mc = mb >>= g

以上摘录自Bartosz Milewski类别理论10.1:单子。

mb >>= g producing mc    That looks like a functor

mb >>= b->mc     variable substitution: mc -> c'
mb >>= b->c'     now this is a functor!
fmap b->c' mb     fmap goes under the hood
m (b->c' b)      back substitution c' -> mc
m (b->mc b)
m (mc)    is not mc, so  another try!

join fmap g mb
join m(mc)     Monoid
mc       is mc   OK!

mc = mb >>= g
mc = join $ fmap g mb

g是b-> mc,所以>> =和join-fmap以相同的方式使用它。 如果不可用,则可以使用return构建它。

return
If we have b->c instead of b->mc

mb >>= (b->c) -> (b->mc)
         f
mb >>= \b -> let c = f b ;
             mc = return c
             in  mc
return :: c -> mc

mc = mb >>= \b -> return $ f b
and
mc = join $ fmap (\b -> return $ f b) mb

f是b-> c,您可以将它与return一起使用,而不是b-> mc(您的问题)。