在最近简要回顾了Haskell之后,对于monad本质上是什么,有什么简单、简洁、实用的解释?
我发现,我遇到的大多数解释都很难理解,而且缺乏实际细节。
在最近简要回顾了Haskell之后,对于monad本质上是什么,有什么简单、简洁、实用的解释?
我发现,我遇到的大多数解释都很难理解,而且缺乏实际细节。
当前回答
Monad是一种带有特殊机器的盒子,它允许你从两个嵌套的盒子中制作一个普通的盒子,但仍然保持两个盒子的一些形状。
具体来说,它允许您执行连接,类型为Monad m=>m(m a)->m a。
它还需要一个返回操作,它只包装一个值。return::Monad m=>a->m a你也可以说joinunboxes和return wrappes,但join不是Monad m=>m a->a类型的(它不会打开所有Monad,而是打开Monad,Monad在其中)
所以它取一个Monad盒子(Monad m=>,m),里面有一个盒子((m a)),然后生成一个普通盒子(m a。
然而,Monad通常用于(>>=)(口语“bind”)运算符,它本质上只是一个fmap和一个接一个的join。具体而言,
x >>= f = join (fmap f x)
(>>=) :: Monad m => (a -> m b) -> m a -> m b
请注意,函数出现在第二个参数中,而不是fmap。
此外,join=(>>=id)。
为什么这有用?本质上,它允许您在某种框架(Monad)中工作时制作将动作串在一起的程序。
Haskell中Monad的最突出用途是IO Monad。现在,IO是对Haskell中的Action进行分类的类型。在这里,Monad系统是唯一的保存方式(华丽的词):
参考透明度懒惰纯洁
本质上,像getLine::IOString这样的IO操作不能被String替换,因为它总是具有不同的类型。把IO想象成一种神奇的盒子,可以把东西传送给你。然而,仍然只是说getLine::IOString和所有函数都接受IOa会导致混乱,因为可能不需要这些函数。const“üp§”getLine会做什么?(const丢弃第二个参数。const a b=a。)getLine不需要求值,但应该执行IO!这使得行为相当不可预测,也使得类型系统不那么“纯粹”,因为所有函数都将采用a和IOa值。
输入IO Monad。
要将动作串在一起,只需展平嵌套的动作。要将函数应用于IO操作的输出,IO a类型中的a,只需使用(>>=)。
例如,输出输入的行(输出行是一个生成IO操作的函数,匹配右参数>>=):
getLine >>= putStrLn :: IO ()
-- putStrLn :: String -> IO ()
这可以用do环境更直观地写出来:
do line <- getLine
putStrLn line
本质上,这样的do块:
do x <- a
y <- b
z <- f x y
w <- g z
h x
k <- h z
l k w
…转化为:
a >>= \x ->
b >>= \y ->
f x y >>= \z ->
g z >>= \w ->
h x >>= \_ ->
h z >>= \k ->
l k w
还有m>>=\_->f的>>运算符(当框中的值不需要在框中创建新框时)也可以写成a>>b=a>>=constb(consta b=a)
此外,返回运算符是根据IO直觉建模的-它返回一个具有最小上下文的值,在这种情况下没有IO。由于IO a中的a表示返回的类型,这类似于命令式编程语言中的return(a),但它不会停止操作链!f>>=return>>=g与f>>=g相同。仅当您返回的术语在链中较早创建时才有用-请参见上文。
当然,还有其他Monad,否则它不会被称为Monad,它会被称为“IO控制”之类的东西。
例如,List Monad(Monad[])通过串联变平-使(>>=)运算符对列表的所有元素执行函数。这可以被视为“不确定性”,其中列表是许多可能的值,而Monad框架正在进行所有可能的组合。
例如(GHCi):
Prelude> [1, 2, 3] >>= replicate 3 -- Simple binding
[1, 1, 1, 2, 2, 2, 3, 3, 3]
Prelude> concat (map (replicate 3) [1, 2, 3]) -- Same operation, more explicit
[1, 1, 1, 2, 2, 2, 3, 3, 3]
Prelude> [1, 2, 3] >> "uq"
"uququq"
Prelude> return 2 :: [Int]
[2]
Prelude> join [[1, 2], [3, 4]]
[1, 2, 3, 4]
因为:
join a = concat a
a >>= f = join (fmap f a)
return a = [a] -- or "= (:[])"
如果出现这种情况,“也许莫纳德”只会将所有结果作废为“无”。也就是说,绑定自动检查函数(a>>=f)是否返回或值(a>>>=f)是否为Nothing,然后也返回Nothing。
join Nothing = Nothing
join (Just Nothing) = Nothing
join (Just x) = x
a >>= f = join (fmap f a)
或者更明确地说:
Nothing >>= _ = Nothing
(Just x) >>= f = f x
State Monad用于同时修改某些共享状态-s->(a,s)的函数,因此>>=的参数为:a->s->(a,s)。这个名称有点用词不当,因为State实际上是用于状态修改功能,而不是用于状态——状态本身确实没有有趣的财产,它只是被改变了。
例如:
pop :: [a] -> (a , [a])
pop (h:t) = (h, t)
sPop = state pop -- The module for State exports no State constructor,
-- only a state function
push :: a -> [a] -> ((), [a])
push x l = ((), x : l)
sPush = state push
swap = do a <- sPop
b <- sPop
sPush a
sPush b
get2 = do a <- sPop
b <- sPop
return (a, b)
getswapped = do swap
get2
那么:
Main*> runState swap [1, 2, 3]
((), [2, 1, 3])
Main*> runState get2 [1, 2, 3]
((1, 2), [1, 2, 3]
Main*> runState (swap >> get2) [1, 2, 3]
((2, 1), [2, 1, 3])
Main*> runState getswapped [1, 2, 3]
((2, 1), [2, 1, 3])
也:
Prelude> runState (return 0) 1
(0, 1)
其他回答
解释
当用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是一个函数和一个类型。就像一个特殊接口的实现。
monad是一种具有两个操作的数据类型:>>=(又名bind)和return(又名unit)。return接受一个任意值并用它创建monad的实例。>>=接受monad的一个实例并在其上映射一个函数。(您已经可以看到monad是一种奇怪的数据类型,因为在大多数编程语言中,您无法编写一个接受任意值并从中创建类型的函数。monad使用一种参数多态性。)
在Haskell表示法中,monad接口是
class Monad m where
return :: a -> m a
(>>=) :: forall a b . m a -> (a -> m b) -> m b
这些操作应该遵守某些“法则”,但这并不是非常重要的:“法则”只是将操作的合理实现行为化(基本上,>>=和return应该就如何将值转换为monad实例达成一致,并且>>=是关联的)。
Monad不仅仅是关于状态和I/O:它们抽象了一种常见的计算模式,包括处理状态、I/O、异常和非确定性。可能最容易理解的单子是列表和选项类型:
instance Monad [ ] where
[] >>= k = []
(x:xs) >>= k = k x ++ (xs >>= k)
return x = [x]
instance Monad Maybe where
Just x >>= k = k x
Nothing >>= k = Nothing
return x = Just x
其中[]和:是列表构造函数,++是串联运算符,Just和Nothing是Maybe构造函数。这两个monad都在各自的数据类型上封装了常见的有用的计算模式(请注意,两者都与副作用或I/O无关)。
你真的需要写一些非平凡的Haskell代码来理解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概念的普遍性,这是因为范畴理论非常抽象,所以它的概念对编程非常有用。因此,我们在下面介绍的单子的推广也与范畴理论有着密切的联系,这一点不足为奇。但我们强调,我们的目的非常实用:它不是“实现范畴理论”,而是找到一种更通用的方法来构造组合子库。数学家已经为我们做了很多工作,这是我们的幸运!
从约翰·休斯的《概括单子到箭头》
公主对F#计算表达式的解释帮助了我,尽管我仍然不能说我真的理解了。
编辑:这个系列-用javascript解释monad-对我来说是一个“打破平衡”的系列。
http://blog.jcoglan.com/2011/03/05/translation-from-haskell-to-javascript-of-selected-portions-of-the-best-introduction-to-monads-ive-ever-read/http://blog.jcoglan.com/2011/03/06/monad-syntax-for-javascript/http://blog.jcoglan.com/2011/03/11/promises-are-the-monad-of-asynchronous-programming/
我认为理解单子是一件让你毛骨悚然的事。从这个意义上说,尽可能多地阅读“教程”是一个好主意,但通常奇怪的东西(不熟悉的语言或语法)会让你的大脑无法专注于基本内容。
有些事情我很难理解:
基于规则的解释对我来说从未奏效,因为大多数实际示例实际上需要的不仅仅是返回/绑定。此外,称之为规则也无济于事。这更像是“有些东西有共同点,我们把它们称为‘单子’,把共同点称为‘规则’”。Return(a->M<a>)和Bind(M<a>->(a->M<b>)->M<b>)很好,但我永远无法理解Bind如何从M<a>中提取a,以便将其传递给a->M<b>。我不认为我在任何地方读过(也许这对其他人来说都很明显),Return(M<a>->a)的反面必须存在于monad内部,它只是不需要暴露。
在几年前回答了这个问题之后,我相信我可以通过。。。
monad是一种函数组合技术,它使用组合函数bind将某些输入场景的处理具体化,以在组合过程中预处理输入。
在正常合成中,函数compose(>>)用于按顺序将合成的函数应用于其前身的结果。重要的是,所组成的函数需要处理其输入的所有场景。
(x->y)>>(y->z)
这种设计可以通过重组输入来改进,以便更容易地询问相关状态。因此,如果y包含有效性的概念,则值可以变成Mb,例如(is_OK,b),而不是简单的y。
例如,当输入仅可能是一个数字时,而不是返回一个可以尽职尽责地包含数字或不包含数字的字符串,您可以将类型重新构造为bool,以指示元组中存在有效数字和数字,例如bool*float。组合函数现在不再需要解析输入字符串来确定数字是否存在,而只需要检查元组的布尔部分。
(Ma->Mb)>>(Mb->Mc)
在这里,合成与合成一起自然发生,因此每个函数必须单独处理其输入的所有场景,尽管现在这样做要容易得多。
然而,如果我们能够将审讯的工作外化,以应对那些处理场景是常规的情况,那又会怎样呢。例如,如果我们的程序在输入不正常时什么都不做,比如is_OK为false时。如果做到了这一点,那么组合函数就不需要自己处理该场景,从而大大简化了代码并实现了另一个级别的重用。
为了实现这种外部化,我们可以使用bind(>>=)函数来执行组合而不是组合。因此,不是简单地将值从一个函数的输出传递到另一个函数输入,而是检查Ma的M部分,并决定是否以及如何将组合函数应用于a。当然,函数绑定将专门为我们的特定M定义,以便能够检查其结构并执行我们想要的任何类型的应用。尽管如此,a可以是任何东西,因为bind仅在确定应用程序需要时将未检查的a传递给组合函数。此外,组合函数本身也不再需要处理输入结构的M部分,从而简化了它们。因此
(a->Mb)>>=(b->Mc)或更简洁地Mb>>=
简言之,一旦输入被设计为充分暴露某些输入场景,monad就外部化了,从而提供了关于处理这些输入场景的标准行为。这种设计是一种外壳和内容模型,其中外壳包含与组合函数的应用程序相关的数据,并由绑定函数查询,并且仅对绑定函数可用。
因此,单子是三件事:
M外壳,用于保存monad相关信息,实现的绑定函数,用于在将组合函数应用于其在外壳中找到的内容值时使用该外壳信息,以及形式为a->Mb的可组合函数,生成包含单元管理数据的结果。
一般来说,函数的输入比其输出更具限制性,其中可能包括错误条件等;因此,Mb结果结构通常非常有用。例如,当除数为0时,除法运算符不返回数字。
此外,monad可以包括将值a包装成monadic类型Ma的包装函数,以及将一般函数a->b包装成monodic函数a->Mb的包装函数。当然,像bind一样,这样的包装函数是M特有的。例如:
let return a = [a]
let lift f a = return (f a)
绑定函数的设计假定了不可变的数据结构和纯函数,其他事情变得复杂,无法保证。因此,有一元定律:
鉴于
M_
return = (a -> Ma)
f = (a -> Mb)
g = (b -> Mc)
然后
Left Identity : (return a) >>= f === f a
Right Identity : Ma >>= return === Ma
Associative : Ma >>= (f >>= g) === Ma >>= ((fun x -> f x) >>= g)
关联性意味着无论何时应用绑定,绑定都会保留求值顺序。也就是说,在上述关联性的定义中,对f和g的括号化绑定的强制早期评估只会导致期望Ma的函数完成绑定。因此,必须先确定Ma的值,然后才能将其值应用于f,进而将结果应用于g。