我不常使用Haskell,但我了解Monads的概念。
我对Kleisli triple和类别感到迷惑
尽管Haskell根据return和bind函数定义了monad,但也可以根据
return
和另外两个操作join
和fmap
定义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。
为简单起见,我们是否不应该仅使用非一元函数(只要它们是内函数)?我想念什么?
相关主题是
答案 0 :(得分:9)
从某种意义上说,你是对的。由于每个单子m
都是函子,因此我们可以将fmap f
与函数f :: a -> b
结合使用,将m a
变成m b
,但是有一个陷阱。什么是b
?
我喜欢将m
理解为“计划获得”的意思,其中“计划”涉及除纯计算之外的某种其他交互。如果您有“计划获得Int
”,并且想要“计划获得String
”,则可以将fmap
与Int -> String
中的函数一起使用,但是该函数的类型告诉您,从String
获取Int
不需要进行进一步的交互。
并非总是如此:也许Int
是学生注册号,而String
是他们的名字,所以从一个转换为另一个的计划需要在某个表中进行外部查找。然后,我没有从Int
到String
的纯函数,而是从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)
我很难理解您的问题是什么,但是无论如何我都会努力解决。
我认为,由于
>>=
可以由fmap
和join
组成,因此不需要一元函数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
的情况下,通常更简单地使用Applicative
,Functor
或仅使用基本的纯函数。在这些情况下,应该(通常)使用这些东西来代替Monad
。例如:
ws <- getLine >>= return . words -- Monad
ws <- words <$> getLine -- Functor (much nicer)
要清楚:如果没有monad可以实现,并且如果没有monad则更简单易读,那么您应该在没有monad的情况下做到!如果monad使代码变得比所需复杂或混乱,请不要使用monad! Haskell具有monad的唯一目的是使某些复杂的计算更简单,更易于阅读且更易于推理。如果这没有发生,则您不应该使用monad 。
答案 2 :(得分:0)
我认为,由于
>>=
可以由fmap
和join
组成,因此不需要一元函数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(您的问题)。