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



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

我被Kleisli三重和类别弄糊涂了,然而,

fmap并加入

尽管Haskell根据返回和绑定函数定义monad,但也可以根据return和另外两个操作joinfmap定义monad。这个公式更符合范畴理论中单子的原始定义。类型为(t → u) → M t → M ufmap操作在两个类型之间获取一个函数,并生成一个对monad中的值执行"相同操作"的函数。类型为M (M t) → M tjoin运算将两层一元信息"压平"为一层。

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

这两种配方的关系如下:

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

我的问题是:我认为由于>>=可以由fmapjoin组成a -> m b是不需要的,普通函数a -> b可以满足操作,但网络上的许多教程仍然坚持使用一元函数,因为这是Kleisli三重和一元定律。

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

相关主题为

Monad连接函数

Haskell-Monad绑定运算符混淆

fmap和bind之间的功能差异?

从某种意义上说,你是对的。由于每个monadm都是一个函子,我们可以将fmap f与函数f :: a -> b一起使用,将m a转换为m b,但有一个陷阱。什么是b

我喜欢把这样的m看作是"计划得到"的意思,其中"计划"涉及到超越纯粹计算的某种额外的交互。如果您有一个"plan-to-getInt",并且您想要一个"plan-to-get CCD-21",您可以将fmapInt -> String中的函数一起使用,但该函数的类型告诉您从Int获取String不需要进一步的交互。

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

一般情况下,我们有足够的信息来计算计划,以获得更多信息。这就是a -> m b的模型。特别是,我们有return :: a -> m a,它将我们所掌握的信息转化为计划,通过不采取进一步行动来提供确切的信息,而我们有(>=>) :: (a -> m b) -> (b -> m c) -> (a -> m c),它构成了两个这样的东西。我们还知道(>=>)是关联的,并吸收了左右两侧的return,就像经典命令式编程中;是关联的并吸收了skip一样。

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

现在,由于Haskell是一种具有"自由变量"one_answers"范围"概念的语言,中的a

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

表示"总体输入信息"可以被认为来自我们已经拥有的东西的范围,留下

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

我们回到了"绑定",这是一种以最适合程序员的形式呈现组合结构的工具,类似于局部定义。

总之,你可以使用a -> b,但通常你需要b来"计划获得一些东西",如果你想以组合的方式构建计划,这是一个很有帮助的选择。

我很难理解你的问题到底是什么,但无论如何我都会尝试一下。

我认为,由于>>=可以由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


,但网络上的许多教程仍然坚持使用一元函数,因为这是Kleisli三重和一元定律。

我不确定你在看哪些教程。根据我的经验,教程的本质是它们坚持自己的教程。如果Monad的教程开始提出问题,然后提出Monad以外的东西作为解决方案,那将是奇怪的;至少,这将超出本教程的范围,并且对于任何阅读本教程的人来说,学习Monads都是浪费时间。


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

内函数是a -> a类型的函数。考虑到你问题的上下文,我认为你实际上是指a -> b类型的纯函数("纯",而不是像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

没有任何原因

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

是的,你完全正确。我们不需要为monad要求>>=,我们也可以要求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给出了一个更好的概念来区分monad和函子。

多功能性

我们可以从fmapjoin导出>>=,但我们只能从>>=导出join。事实上,我们甚至可以从>>=return导出fmap。那么,我们应该说>>=比其他CCD_93更基本吗?更强大?或者只是:更复杂?

我们宁愿写

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

使用CCD_ 94的前一种解决方案是最小的。

便利性

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

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

monad背后的整个想法是,我们可以任意地将它们链接和嵌套,最终得到一个普通类型。解析完一个数字后,我们需要验证monad(fmap validate)中的可选结果(给我们另一个可选结果)-,然后再将其取出。通常没有直接产生嵌套数据的操作,我们只得到嵌套的monad类型,因为我们在monadic类型中进行进一步的monadic操作。我们更愿意写

tryRead = (=<<) validate . tryParse

tryRead = join . fmap validate . tryParse

这就是为什么>>=在日常生活中比join更重要。我还想,必须直接实现>>=,而不是显式地实现join并从中派生>>=,可以实现更好(更容易)的编译器优化。

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:Monads

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
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(你的问题)。

最新更新