我学习Haskell的方法我开始掌握monad概念并开始在我的代码中使用已知的monad但是从设计师的角度来看我仍然很难接近monad。在OO中有几个规则,比如“识别名词”对象,注意某种状态和界面......但是我无法找到monad的等效资源。
那么你如何将问题确定为monadic? monadic设计有哪些好的设计模式?当你意识到某些代码会更好地重构为monad时,你的方法是什么?
答案 0 :(得分:58)
一个有用的经验法则是当您在上下文中看到值时; monads可以看作是对“效果”的分层:
通常,你应该通过在标准Monad Transformer Library上对monad变换器进行分层来设计你的monad,这样你就可以将上面的效果组合成一个monad。它们共同处理您可能想要使用的大多数monad。 MTL中还包含一些其他monad,例如probability和supply monad。
就新定义的类型是否为monad以及它如何表现为一个直觉而言,你可以通过从Functor
上升到Monad
来考虑它:
(<*>)
允许您从嵌入式函数及其嵌入式参数转换为嵌入式结果。理解这一点的最简单方法是查看join
的类型:
join :: (Monad m) => m (m a) -> m a
这意味着如果您的嵌入式计算结果是 new 嵌入式计算,则可以创建执行该计算结果的计算。因此,您可以使用monadic效果基于先前计算的值创建新计算,并将传输控制流程转换为该计算。
有趣的是,这可能是单独构造事物的弱点:使用Applicative
,计算的结构是静态的(即给定的Applicative
计算具有一定的结构不能基于中间值改变的效果),而Monad
则是动态的。这可能会限制您可以执行的优化;例如,应用解析器不如monadic解析器(嗯,这不是strictly true,但它实际上是有效的),但它们可以更好地进行优化。
请注意,(>>=)
可以定义为
m >>= f = join (fmap f m)
所以可以使用return
和join
简单地定义monad(假设它是Functor
;所有monad都是适用的函子,但不幸的是Haskell的类型类层次结构不需要historical reasons)。
作为补充说明,你可能不应该过分关注monad,无论他们从被误导的非Haskeller那里得到什么样的嗡嗡声。有许多类型类代表有意义和强大的模式,并不是所有的东西都被最好地表达为monad。 Applicative,Monoid,Foldable ...使用哪种抽象完全取决于您的情况。而且,当然,仅仅因为某些东西是monad并不意味着它也不能成为其他东西;作为一个单子只是一种类型的另一种财产。
所以,你不应该过多考虑“识别monads”;问题更像是:
答案 1 :(得分:15)
按照类型进行操作。
如果您发现所有这些类型的书面函数
(a -> b) -> YourType a -> YourType b
a -> YourType a
YourType (YourType a) -> YourType a
或所有这些类型
a -> YourType a
YourType a -> (a -> YourType b) -> YourType b
然后YourType
可能成为monad。 (我说“可能”,因为这些功能也必须遵守monad法则。)
(请记住,您可以重新排序参数,例如YourType a -> (a -> b) -> YourType b
只是伪装成(a -> b) -> YourType a -> YourType b
。)
不要只注意单子!如果你有所有这些类型的功能
YourType
YourType -> YourType -> YourType
他们遵守幺半群定律,你有一个幺半群!这也很有价值。类似地,对于其他类型类,最重要的是Functor。
答案 2 :(得分:7)
有monad的效果视图:
一旦熟悉了这些效果,就可以轻松构建monad,将它们与monad变换器相结合。请注意,组合一些monad需要特别小心(特别是Cont和任何带回溯的monad)。
有一点需要注意的是,没有很多单子。在标准库中有一些奇特的东西,例如概率monad和Cont monad的变体,如Codensity。但是,除非你正在做一些数学上的东西,否则你不可能发明(或发现)一个新的monad,但是如果你使用Haskell的时间足够长,你将构建许多不同组合的monad。
编辑 - 另请注意,堆叠monad变换器的顺序会产生不同的monad:
如果将ErrorT(变换器)添加到Writer monad,则会获得此monad Either err (log,a)
- 如果没有错误,则只能访问日志。
如果将WriterT(transfomer)添加到Error monad,则会获得此monad (log, Either err a)
,它始终可以访问日志。
答案 3 :(得分:4)
这是一种非答案,但我觉得无论如何都要说。 请问! StackOverflow,/ r / haskell和#haskell irc频道都是从聪明人那里获得快速反馈的好地方。如果你正在解决一个问题,并且你怀疑有一些可以让它更容易的monadic魔法,那就问问吧! Haskell社区喜欢解决问题,并且非常友好。
不要误解,我不鼓励你永远不要为自己学习。恰恰相反,与Haskell社区的互动是最佳学习方式之一。 LYAH和RWH,强烈推荐2本可在线免费获取的Haskell书籍。
哦,不要忘记玩,玩,玩!当你玩monadic代码时,你会开始感受到monad的“形状”,以及monadic组合器可能很有用。如果您正在推出自己的monad,那么通常类型系统将引导您找到一个明显,简单的解决方案。但说实话,你应该很少需要推出自己的Monad实例,因为Haskell库提供了其他回答者提到的大量有用的东西。
答案 4 :(得分:0)
有一种普遍的观念,即人们在许多编程语言中都看到“传染性功能标签”,这是一种对功能的特殊行为,必须同时扩展到其调用者。
unsafe
,这意味着它们执行的操作有可能违反内存不安全性。 unsafe
函数可以调用普通函数,但是任何调用unsafe
函数的函数也必须是unsafe
。async
,这意味着它们返回的是承诺而不是实际值。 async
函数可以调用普通函数,但是只能通过另一个async
函数来调用await
函数(通过async
)。IO a
而不是a
。不纯函数可以调用纯函数,但是不纯函数只能由其他不纯函数调用。虽然可能有从未标记函数调用标记函数的方法,但没有 general 方法,这样做通常很危险,并且有可能破坏该语言试图提供的抽象。
那么,拥有标签的好处是,您可以公开给此标签指定的一组特殊基元,并且任何使用这些基元的功能都可以在其签名中清楚地说明这一点。
假设您是语言设计师,并且意识到这种模式,然后决定要允许用户定义的标签。假设用户定义了一个标签Err
,该标签表示可能会引发错误的计算。使用Err
的函数可能如下所示:
function div <Err> (n: Int, d: Int): Int
if d == 0
throwError("division by 0")
else
return (n / d)
如果我们想简化事情,我们可能会发现接受参数并没有错误-它正在计算可能出现问题的返回值。因此,我们可以将标记限制为不带参数的函数,并让div
返回一个闭包而不是实际值:
function div(n: Int, d: Int): <Err> () -> Int
() =>
if d == 0
throwError("division by 0")
else
return (n / d)
在像Haskell这样的惰性语言中,我们不需要闭包,而可以直接返回一个惰性值:
div :: Int -> Int -> Err Int
div _ 0 = throwError "division by 0"
div n d = return $ n / d
现在很明显,在Haskell中,标记不需要特殊的语言支持-它们是普通的类型构造函数。让我们为他们创建一个类型类!
class Tag m where
我们希望能够从标记函数中调用未标记函数,这等效于将未标记值(a
)转换为标记值(m a
)。
addTag :: a -> m a
我们还希望能够获取标记的值(m a
)并应用标记的函数(a -> m b
)以获得标记的结果(m b
):
embed :: m a -> (a -> m b) -> m b
这当然是monad的定义! addTag
对应于return
,embed
对应于(>>=)
。
现在很明显,“标记函数”仅仅是monad的一种。这样,每当您发现可以应用“功能标签”的地方时,就有可能有一个适合于monad的地方。
P.S。关于我在此答案中提到的标签:Haskell用IO
单子模拟杂质,并用Maybe
单子模拟偏性。大多数语言都相当透明地实现了异步/承诺,并且似乎有一个名为promise的Haskell软件包可以模仿此功能。 Err
单子等效于Either String
单子。我不知道有任何一种语言可以一味地模拟内存安全性,可以做到这一点。