好的,所以回答您自己的问题并不是一种很好的形式,但是我要记下我的想法,以防他人启发。(我对此表示怀疑...)
如果一个单子可以被看作是一个“容器”,那么这两个return
和join
具有很明显的语义。return
生成一个1元素的容器,然后join
将一个容器的容器变成一个容器。没什么难的。
因此,让我们关注更自然地被认为是“动作”的单子。在这种情况下,m x
某种动作会x
在您“执行”时产生type值。return x
没什么特别的,然后产生x
。fmap f
采取产生一个的动作x
,并构造一个计算x
然后应用于该动作f
并返回结果的动作。到现在为止还挺好。
很明显,如果f
它本身产生一个动作,那么最终的结果就是m (m x)
。也就是说,一个动作将计算另一个动作。在某种程度上,这比起>>=
采取行动的功能和“产生行动的功能”等更容易使您着迷。
因此,从逻辑上讲,似乎join
将运行第一个动作,执行它产生的动作,然后运行该动作。(更确切地说,join
,如果您想分开头发将返回执行我刚才描述的操作。)
这似乎是中心思想。要实现join
,您需要运行一个动作,然后再给您另一个动作,然后运行该动作。(无论“运行”对这个特定的monad意味着什么。)
有了这种见识,我可以尝试编写一些join
实现:
join Nothing = Nothing
join (Just mx) = mx
如果外部动作为Nothing
,则返回Nothing
,否则返回内部动作。再说一次,Maybe
它更多的是一个容器而不是一个动作,所以让我们尝试其他的东西...
newtype Reader s x = Reader (s -> x)
join (Reader f) = Reader (\ s -> let Reader g = f s in g s)
真是无痛。AReader
实际上只是一个具有全局状态并随后返回其结果的函数。因此,要取消堆叠,请将全局状态应用于外部操作,该操作会返回一个新的Reader
。然后,您也将状态应用于此内部函数。
在某种程度上,它可能比通常的方法容易:
Reader f >>= g = Reader (\ s -> let x = f s in g x)
现在,哪个是阅读器功能,哪个是计算下一个阅读器的功能...?
现在,让我们尝试一下好老的State
monad。在这里,每个函数都将初始状态作为输入,但也会返回新状态及其输出。
data State s x = State (s -> (s, x))
join (State f) = State (\ s0 -> let (s1, State g) = f s0 in g s1)
不太难。基本上先运行,然后运行。
我现在要停止输入。随意指出我示例中的所有小故障和错别字...:-/
join m = m >>= id