Monad的join
的直观含义是什么?
monads-as-containers 类比对我有意义,并且在这些类比join
内部是有道理的。值是双重包装的,我们打开一个图层。但众所周知,monad不是一个容器。
在正常情况下,如何使用join
编写合理且易于理解的代码,例如何时在IO
?
答案 0 :(得分:3)
action :: IO (IO a)
是一种产生a
的方法。那么,join action
是一种通过运行a
的最外层生成器生成action
的方法,将生成的生成器运行,然后再运行它,最终得到那个多汁的a
{ {1}}。
答案 1 :(得分:3)
join
折叠了类型构造函数的连续图层。
有效的join
必须满足以下属性:对于类型构造函数的任意数量的连续应用程序,它不应该与我们折叠图层的顺序相关。
例如
ghci> let lolol = [[['a'],['b','c']],[['d'],['e']]]
ghci> lolol :: [[[Char]]]
ghci> lolol :: [] ([] ([] Char)) -- the type can also be expressed like this
ghci> join (fmap join lolol) -- collapse inner layers first
"abcde"
ghci> join (join lolol) -- collapse outer layers first
"abcde"
(我们使用fmap
来"进入"外部monadic层,以便我们可以先折叠内层。)
join
有用的小型非容器示例:对于函数monad (->) a
,join
等同于\f x -> f x x
,类型为(a -> a -> b) -> a -> b
的函数将相同参数的两倍应用于另一个函数。
答案 2 :(得分:2)
对于List monad,@pytest.fixture
def blue_frob(tmpdir, blueness):
return Frob(blueness, workdir=tmpdir)
@pytest.fixture
def green_frob(greenness, tmpsocket):
return Frob(greenness, sock=tmpsocket)
all_frobs = [blue_frob, green_frob]
frobs = make_fixture_from_fixtures(all_frobs)
只是join
,concat
是concatMap
。
因此join . fmap
隐式出现在使用join
的任何列表表达式中
或concat
。
假设您被要求查找所有任何除数的数字
输入列表中的数字。如果您有concatMap
函数:
divisors
你可以解决这个问题:
divisors :: Int -> [Int]
divisors n = [ d | d <- [1..n], mod n d == 0 ]
这里我们正在考虑通过首先映射来解决问题 除数在所有输入元素上起作用,然后连接 所有结果列表。你甚至可能认为这是非常的 &#34;功能&#34;解决问题的方法。
另一个approch是编写列表理解:
foo xs = concat $ (map divisors xs)
或使用do-notation:
bar xs = [ d | x <- xs, d <- divisors x ]
在这里可以说我们正在考虑更多
势在必行 - 首先从列表bar xs = do x <- xs
d <- divisors
return d
中抽取一个数字;然后画画
从数字的除数中除数并得出它。
但事实证明,xs
和foo
功能完全相同。
另外,这两种方法在任何 monad中完全相同。 也就是说,对于任何monad,以及适当的monadic函数f和g:
bar
例如,如果我们设置do x <- f
y <- g x is the same as: (join . fmap g) f
return y
和f = getLine
,则在IO monad中,
我们有:
g = readFile
do-block是表达动作的更为必要的方式:首先阅读a 输入线;然后将返回的字符串视为文件名,读取内容 的文件,最后返回结果。
等效的连接表达式在IO-monad中看起来有点不自然。
但是它不应该像我们一样使用它,就像我们一样
在第一个示例中使用了do x <- getLine
y <- readFile x is the same as: (join . fmap readFile) getLine
return y
。
答案 3 :(得分:1)
如果某个操作产生另一个操作,请运行该操作,然后运行它产生的操作。
如果您想象某种Parser x
monad解析x
,那么Parser (Parser x)
是一个解析器,它会进行一些解析,然后返回另一个解析器。因此,join
会将其展平为只运行两个操作并返回最终Parser x
的{{1}}。
为什么你首先要有一个x
?基本上,因为Parser (Parser x)
。如果你有一个解析器,你可以fmap
一个改变结果的函数。但是如果你fmap
一个函数本身返回一个解析器,你最终得到一个fmap
,你可能只想运行这两个动作。 Parser (Parser x)
实现“只运行两个动作”。
我喜欢解析示例,因为解析器通常具有join
函数。很明显,runParser
不是整数。它可以解析一个整数,在之后给它一些输入来解析。我想很多人最终都认为Parser Int
只是一个普通的整数,但是这个烦人的IO Int
位是你无法摆脱的。 不是。这是一个未执行的I / O操作。它内部没有“整数”整数;在您实际执行 I / O之前,整数不存在。
答案 4 :(得分:1)
通过写出类型并稍微重构它们来揭示这些函数的用途,我发现这些东西更容易理解。
因此定义了Reader
类型,其join
函数的类型如下所示:
newtype Reader r a = Reader { runReader :: r -> a }
join :: Reader r (Reader r a) -> Reader r a
由于这是newtype
,这意味着类型Reader r a
同构到r -> a
。所以我们可以重构类型定义给我们这种类型,尽管它不一样,它与恐慌引用真的“相同”:
在与(->) r
同构的Reader r
monad中,join
是函数:
join :: (r -> r -> a) -> r -> a
所以Reader
连接是一个带两位函数(r -> r -> a
)的函数,并且在它的两个参数位置都应用相同的值。
由于Writer
类型具有此定义:
newtype Writer w a = Writer { runWriter :: (a, w) }
...然后当我们删除newtype
时,其join
函数的类型为isomorphic:
join :: Monoid w => ((a, w), w) -> (a, w)
Monoid
约束需要存在,因为Monad
的{{1}}实例需要它,它让我们马上猜测函数的作用:
Writer
同样,由于join ((a, w0), w1) = (a, w0 <> w1)
有这个定义:
State
...然后它的newtype State s a = State { runState :: s -> (a, s) }
是这样的:
join
......你也可以冒险直接写作:
join :: (s -> (s -> (a, s), s)) -> s -> (a, s)
如果你稍微盯着这种类型,你可能会认为它与{em>两者 join f s0 = (a, s2)
where
(g, s1) = f s0
(a, s2) = g s1
{- Here's the "map" to the variable names in the function:
f g s2 s1 s0 s2
join :: (s -> (s -> (a, s ), s )) -> s -> (a, s )
-}
和Reader
的{{1}}类型有一些相似之处操作。你是对的! Writer
,join
和Reader
monad都是名为update monads的更一般模式的实例。
Writer
正如其他人所指出的,这是State
函数的类型。
这是一个非常巧妙的事情。通常情况下,“花哨”的monad会成为join :: [[a]] -> [a]
,concat
,Reader
或列表等“基本”的组合或变体。因此,当我遇到一个小说的monad时,我常常会问:哪个基本monad与它相似,以及如何?
以解析monad为例,这里已经提到了其他答案。一个简单的解析器monad(不支持错误报告等重要的事情)看起来像这样:
Writer
State
是一个以字符串作为输入的函数,并返回候选解析的列表,其中每个候选解析都是一对:
newtype Parser a = Parser { runParser :: String -> [(a, String)] }
; 但请注意,这种类型看起来非常像州monad:
Parser
这不是偶然的!解析器monad是非确定状态monad ,其中状态是输入字符串的未消耗部分,解析步骤生成替代,可能随后根据进一步输入被拒绝。列表monad通常被称为“nondeterminism”monad,因此解析器类似于state和list monad的混合也就不足为奇了。
使用 monad transfomers 可以使这种直觉系统化。状态monad变换器的定义如下:
a
这意味着上面的newtype Parser a = Parser { runParser :: String -> [(a, String)] }
newtype State s a = State { runState :: s -> (a, s) }
类型也可以这样写:
newtype StateT s m a = StateT { runStateT :: s -> m (a, s) }
...其Parser
个实例跟随机械来自type Parser a = StateT String [] a
和Monad
。
StateT
monad 想象一下,我们可以枚举所有可能的原始 []
操作,有点像这样:
IO
然后我们可以将IO
类型视为此(我从高度推荐的Operational monad tutorial改编而来):
{-# LANGUAGE GADTs #-}
data Command a where
-- An action that writes a char to stdout
putChar :: Char -> Command ()
-- An action that reads a char from stdin
getChar :: Command Char
-- ...
然后IO
行动将如下所示:
data IO a where
-- An `IO` action that just returns a constant value.
Return :: a -> IO a
-- An action that binds the result of a `Command` to
-- a function that computes the next step after it.
Bind :: Command x -> (x -> IO a) -> IO a
instance Monad IO where ...
因此join
所做的一切都是“追逐”join :: IO (IO a) -> IO a
-- If the action is just `Return`, then its payload already
-- is what we need to return.
join (Return ioa) = ioa
-- If the action is a `Bind`, then its "next step" function
-- `f` produces `IO (IO a)`, so we can just recursively stick
-- a `join` to its result end.
join (Bind cmd f) = Bind cmd (join . f)
操作,直到它看到符合模式join
的结果,然后删除外部IO
}。
那我在这做什么?就像解析器monad一样,我只是定义(或者更确切地说是)具有透明优点的Return (ma :: IO a)
类型的玩具模型。然后我从玩具模型中找出Return
的行为。