在最近简要回顾了Haskell之后,对于monad本质上是什么,有什么简单、简洁、实用的解释?
我发现,我遇到的大多数解释都很难理解,而且缺乏实际细节。
在最近简要回顾了Haskell之后,对于monad本质上是什么,有什么简单、简洁、实用的解释?
我发现,我遇到的大多数解释都很难理解,而且缺乏实际细节。
当前回答
解释
当用C#/Java术语解释时,这很简单:
monad是一个接受参数并返回特殊类型的函数。这个monad返回的特殊类型也称为monad。(monad是#1和#2的组合)有一些语法糖可以使调用此函数和类型转换更容易。
实例
monad有助于使函数式程序员的生活更轻松。典型示例:Maye monad接受两个参数,一个值和一个函数。如果传递的值为null,则返回null。否则它将计算函数。如果我们需要一个特殊的返回类型,我们也可以调用这个返回类型Maybe。一个非常粗糙的实现如下所示:
object Maybe(object value, Func<object,object> function)
{
if(value==null)
return null;
return function(value);
}
这在C#中是非常无用的,因为这种语言缺乏使monad有用所需的语法糖。但是monad允许您用函数式编程语言编写更简洁的代码。
通常程序员在链中调用monad,如下所示:
var x = Maybe(x, x2 => Maybe(y, y2 => Add(x2, y2)));
在本例中,只有当x和y都为非null时,才会调用Add方法,否则将返回null。
答复
回答最初的问题:monad是一个函数和一个类型。就像一个特殊接口的实现。
其他回答
tl;博士
{-# LANGUAGE InstanceSigs #-}
newtype Id t = Id t
instance Monad Id where
return :: t -> Id t
return = Id
(=<<) :: (a -> Id b) -> Id a -> Id b
f =<< (Id x) = f x
开场白
应用程序运算符$of函数
forall a b. a -> b
是规范定义的
($) :: (a -> b) -> a -> b
f $ x = f x
infixr 0 $
根据Haskell基函数应用f x(infixl 10)。
作文定义为$as
(.) :: (b -> c) -> (a -> b) -> (a -> c)
f . g = \ x -> f $ g x
infixr 9 .
并且满足所有f g h的等价性。
f . id = f :: c -> d Right identity
id . g = g :: b -> c Left identity
(f . g) . h = f . (g . h) :: a -> d Associativity
.是关联的,id是它的右标识和左标识。
克莱斯利三人组
在编程中,monad是带有monad类型类实例的函子类型构造函数。定义和实现有几个等价的变体,每个变体对monad抽象的直觉略有不同。
函子是带有函子类型类实例的*->*类型的类型构造函数f。
{-# LANGUAGE KindSignatures #-}
class Functor (f :: * -> *) where
map :: (a -> b) -> (f a -> f b)
除了遵循静态强制类型协议之外,函子类型类的实例必须遵守所有f g的代数函子定律。
map id = id :: f t -> f t Identity
map f . map g = map (f . g) :: f a -> f c Composition / short cut fusion
函数计算具有以下类型
forall f t. Functor f => f t
计算c r包含上下文c中的结果r。
一元一元函数或Kleisli箭头的类型为
forall m a b. Functor m => a -> m b
Kleisi箭头是接受一个参数a并返回一元计算m b的函数。
Monads是用Kleisli三重函数定义的
(m, return, (=<<))
实现为类型类
class Functor m => Monad m where
return :: t -> m t
(=<<) :: (a -> m b) -> m a -> m b
infixr 1 =<<
Kleisli标识返回是一个Kleisli箭头,它将值t提升为单元上下文m。
Kleisli组成<=<根据扩展定义为
(<=<) :: Monad m => (b -> m c) -> (a -> m b) -> (a -> m c)
f <=< g = \ x -> f =<< g x
infixr 1 <=<
<=<组成两个Kleisli箭头,将左箭头应用于右箭头应用的结果。
monad类型类的实例必须遵守monad定律,这在Kleisli组合中最为优雅地表述为:forall f g h。
f <=< return = f :: c -> m d Right identity
return <=< g = g :: b -> m c Left identity
(f <=< g) <=< h = f <=< (g <=< h) :: a -> m d Associativity
<=<是关联的,返回是它的右标识和左标识。
身份
标识类型
type Id t = t
是类型上的标识函数
Id :: * -> *
被解释为函子,
return :: t -> Id t
= id :: t -> t
(=<<) :: (a -> Id b) -> Id a -> Id b
= ($) :: (a -> b) -> a -> b
(<=<) :: (b -> Id c) -> (a -> Id b) -> (a -> Id c)
= (.) :: (b -> c) -> (a -> b) -> (a -> c)
在规范的Haskell中,定义了身份monad
newtype Id t = Id t
instance Functor Id where
map :: (a -> b) -> Id a -> Id b
map f (Id x) = Id (f x)
instance Monad Id where
return :: t -> Id t
return = Id
(=<<) :: (a -> Id b) -> Id a -> Id b
f =<< (Id x) = f x
选项
选项类型
data Maybe t = Nothing | Just t
编码计算可能t不一定产生结果t,计算可能“失败”。选项monad已定义
instance Functor Maybe where
map :: (a -> b) -> (Maybe a -> Maybe b)
map f (Just x) = Just (f x)
map _ Nothing = Nothing
instance Monad Maybe where
return :: t -> Maybe t
return = Just
(=<<) :: (a -> Maybe b) -> Maybe a -> Maybe b
f =<< (Just x) = f x
_ =<< Nothing = Nothing
a->Maybe b仅在Maybe a产生结果时应用于结果。
newtype Nat = Nat Int
自然数可以编码为大于或等于零的整数。
toNat :: Int -> Maybe Nat
toNat i | i >= 0 = Just (Nat i)
| otherwise = Nothing
自然数在减法下不是封闭的。
(-?) :: Nat -> Nat -> Maybe Nat
(Nat n) -? (Nat m) = toNat (n - m)
infixl 6 -?
选项monad涵盖了异常处理的基本形式。
(-? 20) <=< toNat :: Int -> Maybe Nat
List
列表monad,覆盖列表类型
data [] t = [] | t : [t]
infixr 5 :
及其加法幺半群运算“append”
(++) :: [t] -> [t] -> [t]
(x : xs) ++ ys = x : xs ++ ys
[] ++ ys = ys
infixr 5 ++
编码非线性计算[t],产生自然量0,1。。。结果t。
instance Functor [] where
map :: (a -> b) -> ([a] -> [b])
map f (x : xs) = f x : map f xs
map _ [] = []
instance Monad [] where
return :: t -> [t]
return = (: [])
(=<<) :: (a -> [b]) -> [a] -> [b]
f =<< (x : xs) = f x ++ (f =<< xs)
_ =<< [] = []
Extension=<<将从Kleisli箭头a->[b]的应用f x到[a]的元素的所有列表[b]连接到一个结果列表[b]。
设正整数n的正除数为
divisors :: Integral t => t -> [t]
divisors n = filter (`divides` n) [2 .. n - 1]
divides :: Integral t => t -> t -> Bool
(`divides` n) = (== 0) . (n `rem`)
then
forall n. let { f = f <=< divisors } in f n = []
在定义monad类型类时,Haskell标准使用其flip,即绑定运算符>>=,而不是extension=<<。
class Applicative m => Monad m where
(>>=) :: forall a b. m a -> (a -> m b) -> m b
(>>) :: forall a b. m a -> m b -> m b
m >> k = m >>= \ _ -> k
{-# INLINE (>>) #-}
return :: a -> m a
return = pure
为了简单起见,本解释使用了类型类层次结构
class Functor f
class Functor m => Monad m
在Haskell中,当前的标准层次结构是
class Functor f
class Functor p => Applicative p
class Applicative m => Monad m
因为不仅每个单子都是函子,而且每个应用格也是函子,每个单子也是应用格。
使用列表monad,命令式伪代码
for a in (1, ..., 10)
for b in (1, ..., 10)
p <- a * b
if even(p)
yield p
大致翻译为do块,
do a <- [1 .. 10]
b <- [1 .. 10]
let p = a * b
guard (even p)
return p
等效的monad理解,
[ p | a <- [1 .. 10], b <- [1 .. 10], let p = a * b, even p ]
和表达式
[1 .. 10] >>= (\ a ->
[1 .. 10] >>= (\ b ->
let p = a * b in
guard (even p) >> -- [ () | even p ] >>
return p
)
)
Do符号和monad理解是嵌套绑定表达式的语法糖。绑定运算符用于一元结果的本地名称绑定。
let x = v in e = (\ x -> e) $ v = v & (\ x -> e)
do { r <- m; c } = (\ r -> c) =<< m = m >>= (\ r -> c)
哪里
(&) :: a -> (a -> b) -> b
(&) = flip ($)
infixl 0 &
定义了防护功能
guard :: Additive m => Bool -> m ()
guard True = return ()
guard False = fail
其中单位类型或“空元组”
data () = ()
支持选择和失败的加法单子可以通过使用类型类抽象
class Monad m => Additive m where
fail :: m t
(<|>) :: m t -> m t -> m t
infixl 3 <|>
instance Additive Maybe where
fail = Nothing
Nothing <|> m = m
m <|> _ = m
instance Additive [] where
fail = []
(<|>) = (++)
其中fail和<|>形成所有k l m的幺半群。
k <|> fail = k
fail <|> l = l
(k <|> l) <|> m = k <|> (l <|> m)
失败的是吸收/消灭零元素的加法单体
_ =<< fail = fail
如果在
guard (even p) >> return p
即使p为真,则保护产生[()],并且根据>>的定义,产生局部常数函数
\ _ -> return p
应用于结果()。如果为false,则保护生成列表monad的fail([]),这不会产生要应用>>的Kleisli箭头的结果,因此跳过此p。
状态
不光彩的是,monad用于编码有状态计算。
状态处理器是一种功能
forall st t. st -> (t, st)
转换状态st并产生结果t。状态st可以是任何东西。没有,标志,计数,数组,句柄,机器,世界。
状态处理器的类型通常称为
type State st t = st -> (t, st)
状态处理器monad是kind*->*函子state st.Kleisli状态处理器mond的箭头是函数
forall st a b. a -> (State st) b
在规范的Haskell中,定义了状态处理器monad的惰性版本
newtype State st t = State { stateProc :: st -> (t, st) }
instance Functor (State st) where
map :: (a -> b) -> ((State st) a -> (State st) b)
map f (State p) = State $ \ s0 -> let (x, s1) = p s0
in (f x, s1)
instance Monad (State st) where
return :: t -> (State st) t
return x = State $ \ s -> (x, s)
(=<<) :: (a -> (State st) b) -> (State st) a -> (State st) b
f =<< (State p) = State $ \ s0 -> let (x, s1) = p s0
in stateProc (f x) s1
状态处理器通过提供初始状态来运行:
run :: State st t -> st -> (t, st)
run = stateProc
eval :: State st t -> st -> t
eval = fst . run
exec :: State st t -> st -> st
exec = snd . run
状态访问由原语get和put提供,它们是对有状态monad的抽象方法:
{-# LANGUAGE MultiParamTypeClasses, FunctionalDependencies #-}
class Monad m => Stateful m st | m -> st where
get :: m st
put :: st -> m ()
m->st声明状态类型st对monad m的函数依赖性;例如,状态t将确定状态类型为t唯一。
instance Stateful (State st) st where
get :: State st st
get = State $ \ s -> (s, s)
put :: st -> State st ()
put s = State $ \ _ -> ((), s)
单位类型类似于C中的空隙。
modify :: Stateful m st => (st -> st) -> m ()
modify f = do
s <- get
put (f s)
gets :: Stateful m st => (st -> t) -> m t
gets f = do
s <- get
return (f s)
gets通常与记录字段访问器一起使用。
状态monad等价于变量线程
let s0 = 34
s1 = (+ 1) s0
n = (* 12) s1
s2 = (+ 7) s1
in (show n, s2)
其中s0::Int,是同样透明的,但更加优雅和实用
(flip run) 34
(do
modify (+ 1)
n <- gets (* 12)
modify (+ 7)
return (show n)
)
modify(+1)是一种类型为State Int()的计算,但其效果等同于return()。
(flip run) 34
(modify (+ 1) >>
gets (* 12) >>= (\ n ->
modify (+ 7) >>
return (show n)
)
)
结合性的单子定律可以用>>=forall m f g来表示。
(m >>= f) >>= g = m >>= (\ x -> f x >>= g)
or
do { do { do {
r1 <- do { x <- m; r0 <- m;
r0 <- m; = do { = r1 <- f r0;
f r0 r1 <- f x; g r1
}; g r1 }
g r1 }
} }
与面向表达式的编程(例如Rust)一样,块的最后一条语句表示其产量。绑定运算符有时被称为“可编程分号”。
对结构化命令式编程中的迭代控制结构原语进行单点仿真
for :: Monad m => (a -> m b) -> [a] -> m ()
for f = foldr ((>>) . f) (return ())
while :: Monad m => m Bool -> m t -> m ()
while c m = do
b <- c
if b then m >> while c m
else return ()
forever :: Monad m => m t
forever m = m >> forever m
输入/输出
data World
I/O世界状态处理器monad是纯Haskell和真实世界的协调,是功能外延和命令式操作语义的协调。与实际严格执行情况类似:
type IO t = World -> (t, World)
不纯洁的原语促进了交互
getChar :: IO Char
putChar :: Char -> IO ()
readFile :: FilePath -> IO String
writeFile :: FilePath -> String -> IO ()
hSetBuffering :: Handle -> BufferMode -> IO ()
hTell :: Handle -> IO Integer
. . . . . .
使用IO原语的代码的杂质由类型系统永久协议化。因为纯净是可怕的,在IO中发生的一切,都留在IO中。
unsafePerformIO :: IO t -> t
或者,至少应该。
Haskell程序的类型签名
main :: IO ()
main = putStrLn "Hello, World!"
扩展到
World -> ((), World)
改变世界的函数。
后记
对象是Haskell类型,态射是Haskelr类型之间的函数的类别是,“快速和松散”,类别是Hask。
函子T是从范畴C到范畴D的映射;对于C中的每个对象,D中的一个对象
Tobj : Obj(C) -> Obj(D)
f :: * -> *
对于C中的每个态射,D中的一个态射
Tmor : HomC(X, Y) -> HomD(Tobj(X), Tobj(Y))
map :: (a -> b) -> (f a -> f b)
其中X,Y是C中的对象。HomC(X,Y)是C中所有态射X->Y的同态类。
Tmor Tobj
T(id) = id : T(X) -> T(X) Identity
T(f) . T(g) = T(f . g) : T(X) -> T(Z) Composition
范畴C的Kleisli范畴由Kleisli三元组给出
<T, eta, _*>
内函子的
T : C -> C
(f) 、同一态射eta(return)和扩展运算符*(=<<)。
Hask中的每个Kleisli态射
f : X -> T(Y)
f :: a -> m b
由扩展运算符
(_)* : Hom(X, T(Y)) -> Hom(T(X), T(Y))
(=<<) :: (a -> m b) -> (m a -> m b)
在Hask的Kleisli范畴中给出了一个态射
f* : T(X) -> T(Y)
(f =<<) :: m a -> m b
Kleisli范畴中的成分。T以扩展的形式给出
f .T g = f* . g : X -> T(Z)
f <=< g = (f =<<) . g :: a -> m c
并且满足范畴公理
eta .T g = g : Y -> T(Z) Left identity
return <=< g = g :: b -> m c
f .T eta = f : Z -> T(U) Right identity
f <=< return = f :: c -> m d
(f .T g) .T h = f .T (g .T h) : X -> T(U) Associativity
(f <=< g) <=< h = f <=< (g <=< h) :: a -> m d
应用等价变换
eta .T g = g
eta* . g = g By definition of .T
eta* . g = id . g forall f. id . f = f
eta* = id forall f g h. f . h = g . h ==> f = g
(f .T g) .T h = f .T (g .T h)
(f* . g)* . h = f* . (g* . h) By definition of .T
(f* . g)* . h = f* . g* . h . is associative
(f* . g)* = f* . g* forall f g h. f . h = g . h ==> f = g
在扩展方面是规范给出的
eta* = id : T(X) -> T(X) Left identity
(return =<<) = id :: m t -> m t
f* . eta = f : Z -> T(U) Right identity
(f =<<) . return = f :: c -> m d
(f* . g)* = f* . g* : T(X) -> T(Z) Associativity
(((f =<<) . g) =<<) = (f =<<) . (g =<<) :: m a -> m c
Monads也可以不使用Kleislian扩展来定义,而是在称为join的编程中使用自然转换mu来定义。一个单元是用μ来定义的,它是一个内函子的范畴C上的三元组
T : C -> C
f :: * -> *
和两种自然变形
eta : Id -> T
return :: t -> f t
mu : T . T -> T
join :: f (f t) -> f t
满足等效条件
mu . T(mu) = mu . mu : T . T . T -> T . T Associativity
join . map join = join . join :: f (f (f t)) -> f t
mu . T(eta) = mu . eta = id : T -> T Identity
join . map return = join . return = id :: f t -> f t
然后定义monad类型类
class Functor m => Monad m where
return :: t -> m t
join :: m (m t) -> m t
选项monad的规范mu实现:
instance Monad Maybe where
return = Just
join (Just m) = m
join Nothing = Nothing
concat函数
concat :: [[a]] -> [a]
concat (x : xs) = x ++ concat xs
concat [] = []
是列表monad的连接。
instance Monad [] where
return :: t -> [t]
return = (: [])
(=<<) :: (a -> [b]) -> ([a] -> [b])
(f =<<) = concat . map f
联接的实现可以使用等价项从扩展形式转换
mu = id* : T . T -> T
join = (id =<<) :: m (m t) -> m t
从mu到扩展形式的反向转换如下
f* = mu . T(f) : T(X) -> T(Y)
(f =<<) = join . map f :: m a -> m b
Philip Wadler:函数编程的MonadsSimon L Peyton Jones,Philip Wadler:强制函数式编程Jonathan M.D.Hill,Keith Clarke:范畴理论、范畴理论单子及其与函数编程的关系简介´Kleisli类别Eugenio Moggi:计算和单子的概念莫纳德不是什么
但为什么如此抽象的理论对编程有用呢?答案很简单:作为计算机科学家,我们重视抽象!当我们设计软件组件的接口时,我们希望它尽可能少地揭示实现。我们希望能够用许多替代方案来替代实现,许多其他“实例”都是相同的“概念”。当我们为许多程序库设计通用接口时,更重要的是我们选择的接口具有多种实现。我们非常重视monad概念的普遍性,这是因为范畴理论非常抽象,所以它的概念对编程非常有用。因此,我们在下面介绍的单子的推广也与范畴理论有着密切的联系,这一点不足为奇。但我们强调,我们的目的非常实用:它不是“实现范畴理论”,而是找到一种更通用的方法来构造组合子库。数学家已经为我们做了很多工作,这是我们的幸运!
从约翰·休斯的《概括单子到箭头》
monad实际上是“类型运算符”的一种形式。它将做三件事。首先,它会将一种类型的值“包装”(或以其他方式转换)为另一种类型(通常称为“一元类型”)。第二,它将使底层类型上的所有操作(或函数)在monadic类型上可用。最后,它将为将自身与另一个monad组合以生成复合monad提供支持。
“可能monad”本质上等同于Visual Basic/C#中的“可为null的类型”。它接受不可为null的类型“T”并将其转换为“可为null<T>”,然后定义所有二进制运算符在可为null><T>上的含义。
副作用也有类似的表现。创建了一个结构,该结构包含函数返回值旁边的副作用描述。当值在函数之间传递时,“提升”操作会复制副作用。
它们被称为“monad”,而不是更容易理解的“类型运算符”的名称,原因如下:
Monad对他们的行为有限制(详见定义)。这些限制,加上涉及三个运算,符合范畴理论中一个叫做monad的结构,这是一个模糊的数学分支。它们是由“纯”函数语言的支持者设计的纯函数语言的支持者,如模糊的数学分支由于数学晦涩难懂,而且monad与特定的编程风格相关,人们倾向于使用monad这个词作为一种秘密握手。正因为如此,没有人费心去投资一个更好的名字。
事实上,与一般人对蒙得斯的理解相反,他们与国家无关。Monads只是一种包装东西的方法,它提供了对包装好的东西进行操作而不展开的方法。
例如,您可以在Haskell中创建一个类型来包装另一个类型:
data Wrapped a = Wrap a
包装我们定义的东西
return :: a -> Wrapped a
return x = Wrap x
要在不展开的情况下执行操作,假设您有一个函数f::a->b,然后您可以执行此操作来提升该函数以作用于包装的值:
fmap :: (a -> b) -> (Wrapped a -> Wrapped b)
fmap f (Wrap x) = Wrap (f x)
这就是所有需要理解的。然而,事实证明,有一个更通用的函数来执行此提升,即bind:
bind :: (a -> Wrapped b) -> (Wrapped a -> Wrapped b)
bind f (Wrap x) = f x
bind可以比fmap做得更多,但反之亦然。实际上,fmap只能用绑定和返回来定义。因此,在定义monad时。。您给出它的类型(这里是Wrapped a),然后说明它的返回和绑定操作是如何工作的。
很酷的是,这是一个普遍的模式,它会在所有地方弹出,以纯方式封装状态只是其中之一。
有关如何使用monad来引入函数依赖关系,从而控制求值顺序(如Haskell的IO monad中所用)的好文章,请查看IOInside。
至于理解单子,不要太担心。读一些你觉得有趣的东西,如果你不马上理解,也不要担心。那就用Haskell这样的语言潜水吧。修道院就是这样一种东西,在那里,通过练习,理解慢慢地进入你的大脑,有一天你突然意识到你理解了它们。
如果我理解正确的话,IEnumerable是从monad派生出来的。我想知道,对于我们这些来自C#世界的人来说,这可能是一个有趣的视角吗?
值得一提的是,这里有一些帮助我的教程链接(不,我还不知道单子是什么)。
http://osteele.com/archives/2007/12/overloading-semicolonhttp://spbhug.folding-maps.org/wiki/MonadsEnhttp://www.loria.fr/~kow/monads/
[免责声明:我仍在努力完全了解monads。以下是我目前所了解的情况。如果这是错误的,希望有有知识的人会在地毯上给我打电话。]
Arnar写道:
Monads只是一种包装东西的方法,它提供了对包装好的东西进行操作而不展开的方法。
正是这样。想法是这样的:
你需要一些价值,并用一些附加信息来包装它。就像值是某种类型的(例如整数或字符串)一样,附加信息也是某种类型的。例如,该额外信息可能是“可能”或“IO”。然后,您有一些运算符,允许您在携带附加信息的同时对打包的数据进行操作。这些运算符使用附加信息来决定如何更改包装值上的操作行为。例如,Maybe Int可以是Just Int或Nothing。现在,如果您将Maybe Int添加到Maybe Int,则运算符将检查它们是否都是内部的Just Int,如果是,则将展开Int,将其传递给加法运算符,将生成的Int重新包装为新的Just Int(这是有效的Maybe Int),从而返回Maybe Int。但如果其中一个是内部的Nothing,则该运算符将立即返回Nothing,这也是一个有效的Maybe Int。这样,你可以假装Maybe Ints只是正常的数字,并对它们进行常规运算。如果你得到了一个Nothing,你的方程仍然会产生正确的结果——而不必到处乱检查Nothing。
但这个例子正是Maybe所发生的事情。如果额外的信息是IO,那么将调用为IO定义的特殊运算符,并且在执行添加之前,它可以执行完全不同的操作。(好吧,将两个IO Int加在一起可能是荒谬的——我还不确定。)
基本上,“monad”大致意思是“模式”。但是,您现在有了一种语言构造(语法和所有),可以将新模式声明为程序中的东西,而不是一本充满了非正式解释和专门命名的模式的书。(这里的不精确之处在于所有模式都必须遵循特定的形式,因此monad不像模式那样通用。但我认为这是大多数人都知道和理解的最接近的术语。)
这就是为什么人们觉得单子如此令人困惑:因为它们是一个通用的概念。问是什么使某物成为monad与问是什么让某物成为模式类似。
但是想想在语言中对模式的概念提供语法支持的含义:你不必阅读“四人帮”一书,记住特定模式的构造,只需编写一次代码,以不可知的通用方式实现这个模式,然后就完成了!然后,您可以重用此模式,如Visitor或Strategy或Façade等,只需用它装饰代码中的操作,而无需反复重新实现它!
所以,这就是为什么理解monad的人会发现它们如此有用的原因:这并不是知识势利者以理解为荣的象牙塔概念(好吧,当然也是如此,teehee),而是实际上让代码更简单。