我不经常使用Haskell,但我理解Monads的概念。
我被Kleisli三重和类别弄糊涂了,然而,
fmap并加入
尽管Haskell根据返回和绑定函数定义monad,但也可以根据
return
和另外两个操作join
和fmap
定义monad。这个公式更符合范畴理论中单子的原始定义。类型为(t → u) → M t → M u
的fmap
操作在两个类型之间获取一个函数,并生成一个对monad中的值执行"相同操作"的函数。类型为M (M t) → M t
的join
运算将两层一元信息"压平"为一层。
帮助我了解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
我的问题是:我认为由于>>=
可以由fmap
和join
组成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",您可以将fmap
与Int -> String
中的函数一起使用,但该函数的类型告诉您从Int
获取String
不需要进一步的交互。
但情况并非总是如此:也许Int
是学生注册号,String
是他们的名字,所以从一个转换到另一个的计划需要在某个表中进行外部查找。然后我没有从Int
到String
的纯函数,而是从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层计划,然后在外部执行正确数量的join
s(这将是该计划的一个脆弱属性)。
现在,由于Haskell是一种具有"自由变量"one_answers"范围"概念的语言,中的a
(>=>) :: (a -> m b) -> (b -> m c) -> (a -> m c)
表示"总体输入信息"可以被认为来自我们已经拥有的东西的范围,留下
(>>=) :: m b -> (b -> m c) -> m c
我们回到了"绑定",这是一种以最适合程序员的形式呈现组合结构的工具,类似于局部定义。
总之,你可以使用a -> b
,但通常你需要b
来"计划获得一些东西",如果你想以组合的方式构建计划,这是一个很有帮助的选择。
我很难理解你的问题到底是什么,但无论如何我都会尝试一下。
我认为,由于
>>=
可以由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
。
,但网络上的许多教程仍然坚持使用一元函数,因为这是Kleisli三重和一元定律。
我不确定你在看哪些教程。根据我的经验,教程的本质是它们坚持自己的教程。如果Monad
的教程开始提出问题,然后提出Monad
以外的东西作为解决方案,那将是奇怪的;至少,这将超出本教程的范围,并且对于任何阅读本教程的人来说,学习Monad
s都是浪费时间。
为了简单起见,我们不应该只使用非一元函数吗?我想念什么?
内函数是a -> a
类型的函数。考虑到你问题的上下文,我认为你实际上是指a -> b
类型的纯函数("纯",而不是像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。
没有任何原因
我认为由于
>>=
可以由fmap
和join
组成,因此不需要一个一元函数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和函子。
多功能性
我们可以从fmap
和join
导出>>=
,但我们只能从>>=
导出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(你的问题)。