如何 return 来自不纯方法的纯值

How to return a pure value from a impure method

我知道这听起来很微不足道,但我想知道如何从仿函数中解包一个值并将其 return 作为纯值?

我试过:

f::IO a->a
f x=(x>>=) 

f= >>=

我应该在右边放什么?我不能使用 return,因为它会再次将其包装回去。

这是微不足道的,所以这将是一个很长的答案。简而言之,问题在于签名 IO a -> a 不是 Haskell 中正确允许的类型。这实际上与 IO 是一个仿函数关系不大,而是 IO 是特殊的。

对于某些函子,您可以恢复纯值。例如,部分应用对 (,) a 是函子。我们通过 snd.

展开值
snd :: (a,b) -> b
snd (_,b) = b

所以这是一个我们可以解包成纯值的函子,但这实际上与函子无关。它更多地与属于不同类别理论概念的对有关,Comonad,其中:

extract :: Comonad w => w a -> a

任何 Comonad 都将是一个函子,您可以为其恢复纯值。

许多(非共生的)仿函数——比方说"evaluators"——允许像被问到的东西。例如,我们可以用 maybe :: a -> Maybe a -> a 计算 Maybe。通过提供默认值,maybe a 具有所需的类型 Maybe a -> aStateevalState :: State s a -> s -> a 中的另一个有用示例,其参数颠倒但概念相同;给定 monad State s a 和初始状态 s,我们展开纯值 a.

终于到了IO的细节了。 Haskell 语言或库中没有为 IO 提供 "evaluator"。我们可能会认为 运行 程序本身就是一个评估器——与 evalState 非常相似。但是,如果这是一个有效的概念性举措,那么它只会帮助您相信没有理智的方法可以从 IO 展开——任何编写的程序都只是其计算器函数的 IO a 输入。

相反,你被迫做的——设计——是在 IO monad 中工作。例如,如果您有一个纯函数 f :: a -> b,您可以通过 fmap f :: IO a -> IO b

IO 上下文中应用它

TL;DR 你无法从 IO monad 中获得纯值。在 IO 上下文中应用纯函数,例如 fmap

这是一个常见问题:如何从我的 monad 中提取 'the' 值,不仅在 Haskell 中,而且在其他语言中也是如此.我有一个关于为什么这个问题不断出现的理论,所以我会尝试根据这个来回答;希望对你有帮助。

单一值的容器

您可以将 functor(因此也是 monad)视为 container 的值。这是最明显的(冗余)Identity 仿函数:

Prelude Control.Monad.Identity> Identity 42
Identity 42

这只是一个值的包装器,在本例中为 42。对于这个特定的容器,您 可以 提取值,因为它保证在那里:

Prelude Control.Monad.Identity> runIdentity $ Identity 42
42

虽然 Identity 看起来毫无用处,但您可以找到其他似乎包含单个值的函子。例如,在 F# 中,您会经常遇到像 Async<'a>Lazy<'a> 这样的容器,它们用于表示异步或惰性计算(Haskell 不需要后者,因为它是惰性的默认情况下)。

您会在 Haskell 中找到许多其他单值容器,例如 SumProductLastFirstMaxMin 等。所有这些的共同点是它们包含单个值,这意味着您可以 提取值。

我认为当人们第一次遇到仿函数和单子时,他们往往会这样想数据容器的概念:作为一个单一值的容器。

可选值的容器

不幸的是,Haskell 中的一些常见单子似乎支持这个想法。例如,Maybe 也是一个数据容器,但可以包含零个或一个值。不幸的是,如果它存在,您仍然可以提取该值:

Prelude Data.Maybe> fromJust $ Just 42
42

这个问题是 fromJust 不是 全部 ,所以如果你用 Nothing 值调用它会崩溃:

Prelude Data.Maybe> fromJust Nothing
*** Exception: Maybe.fromJust: Nothing

您可以在 Either 中看到同样的问题。虽然我不知道提取 Right 值的内置部分函数,​​但您可以轻松地编写一个带有模式匹配的函数(如果您忽略编译器警告):

extractRight :: Either l r -> r
extractRight (Right x) = x

同样,它适用于 'happy path' 场景,但也很容易崩溃:

Prelude> extractRight $ Right 42
42
Prelude> extractRight $ Left "foo"
*** Exception: <interactive>:12:1-26: Non-exhaustive patterns in function extractRight

尽管如此,由于像 fromJust 这样的函数存在,我想它会欺骗那些不熟悉仿函数和单子概念的人,让他们将它们视为可以从中提取值的数据容器。

当你第一次遇到像 IO Int 这样的东西时,我能理解你为什么会想把它当作一个单一值的容器。从某种意义上说是,但从另一种意义上说,又不是。

多个值的容器

即使使用列表,您也可以(尝试)从列表中提取 'the' 值:

Prelude> head [42..1337]
42

不过,它可能会失败:

Prelude> head []
*** Exception: Prelude.head: empty list

然而,在这一点上,应该清楚的是,试图从任意函子中提取 'the' 值是无稽之谈。列表是一个函子,但它包含任意数量的值,包括零和无限多。

但是,您可以 始终做的是编写将 'contained' 值作为输入并 returns 另一个值作为输出的函数。这是此类函数的任意示例:

countAndMultiply :: Foldable t => (t a, Int) -> Int
countAndMultiply (xs, factor) = length xs * factor

虽然您不能 'extract the value' 超出列表,但您可以将函数应用于列表中的每个值:

Prelude> fmap countAndMultiply [("foo", 2), ("bar", 3), ("corge", 2)]
[6,9,10]

因为 IO 是一个仿函数,你也可以用它做同样的事情:

Prelude> foo = return ("foo", 2) :: IO (String, Int)
Prelude> :t foo
foo :: IO (String, Int)
Prelude> fmap countAndMultiply foo
6

重点是您不从函子中提取值,您进入函子

单子

有时,您应用于仿函数的函数 returns 一个值已经包装在同一个数据容器中。例如,您可能有一个将字符串拆分为特定字符的函数。为了简单起见,我们只看一下将字符串拆分为单词的内置函数 words

Prelude> words "foo bar"
["foo","bar"]

如果您有一个字符串列表,并对每个字符串应用 words,您将得到一个嵌套列表:

Prelude> fmap words ["foo bar", "baz qux"]
[["foo","bar"],["baz","qux"]]

结果是一个嵌套的数据容器,在本例中是一个列表列表。您可以使用 join:

将其展平
Prelude Control.Monad> join $ fmap words ["foo bar", "baz qux"]
["foo","bar","baz","qux"]

这是 monad 的原始定义:它是一个可以展平的函子。在现代 Haskell 中,Monadbind (>>=) 定义,从中可以推导出 join,但也有可能从 join.

导出 >>=

IO 作为所有值

说到这里,你可能会疑惑:这跟IO有什么关系?IO a不就是一个容器吗? a?

类型的值

不是真的。 IO 的一个 解释 是它是一个容器,可以容纳 a 类型的任意值。根据这种解释,它类似于量子力学的 多世界 解释。 IO aa.

类型所有可能值的叠加

在薛定谔最初的思想实验中,盒子里的猫在被观察之前既是活的又是死的。这是叠加的两种可能状态。如果我们考虑一个名为 catIsAlive 的变量,它就相当于 TrueFalse 的叠加。因此,您可以将 IO Bool 视为一组可能的值 {True, False},只有在观察到这些值时才会折叠成一个值。

同样,IO Word8可以解释为所有可能Word8值集合的叠加,即{0, 1, 2,.. 255}IO Int是所有可能Int 值,IO String 所有可能的 String 值(即无限集),依此类推。

那么你如何观察这个值呢?

您不提取它,您在 数据容器内工作。如上所示,您可以在其上添加 fmapjoin。因此,您可以将应用程序编写为纯函数,然后用 fmap>>=join 等不纯值组合。