【问题标题】:How to read a haskell string如何读取haskell字符串
【发布时间】:2026-01-25 14:20:23
【问题描述】:

我完全是 Haskell 的初学者,虽然熟悉 Python、F#、Java、C# 和 C++ 等语言的函数范式(在有限的程度上)。

一直在逃避我的是haskell中的IO。我尝试了几次,甚至在尝试绕过它的过程中学习了 C# 和 F#。

更具体地说,我指的是在没有 do 表示法的情况下获取 IO,使用 do 表示法,IO 变得微不足道。这可能是不好的做法,但在业余时间,我喜欢看看我是否可以用一个连续的表情完成事情。尽管这是一种糟糕的做法,但它很有趣。

这样的表达式通常是这样的(在伪haskell中):

main = getStdinContentsAsString 
           >>= ParseStringToDataStructureNeeded
           >>= DoSomeComputations 
           >>= ConvertToString 
           >>= putStrLn

我对最后四个部分没有问题。我学习 F# 的原因之一是只是想看看除了 IO 之外是否有什么我没有想到的,但是一旦我有了方便的 F# Console.ReadLine() 它返回一个普通的旧字符串,它基本上就是一帆风顺。

这让我又回到了haskell,再次被IO机制阻止。

我已经设法(在这里使用另一个问题)从控制台读取 int,并打印“Hello World!”很多次

main = (readLn :: IO Int) >>= \n -> mapM_ putStrLn $ replicate n "Hello World!"

我想至少获得一些“通用”方式来读取标准输入的全部内容(可能是多行,所以 getContents 需要成为选择的函数)作为字符串,然后我可以处理使用 unlines 等其他函数的字符串,然后使用 map。

我已经尝试过的一些事情:

正如我所说,getContents 将是我所需要的(除非有一些等价物)。

使用逻辑,因为

getContents :: IO String

然后我需要一个接受 IO 字符串并返回一个普通字符串的东西。这是(据我所知)

unsafePerformIO :: IO a -> a

但是由于某种原因 ghc 不高兴:

* Couldn't match type `[Char]' with `IO (IO b)'
  Expected type: String -> IO b
    Actual type: IO (IO b) -> IO b
* In the second argument of `(>>=)', namely `unsafePerformIO'
  In the expression: getContents >>= unsafePerformIO

我尝试的另一件事:这没有问题;

main = getContents >>= putStrLn

即使 getContents 返回的类型是 IO 操作,而不是 putStrLn 想要的 String 本身

getContents :: IO String
putStrLn    :: String -> IO ()

不知何故,动作被神奇地执行,结果字符串被传递给 put 函数。

但是当我尝试添加一些东西时,比如在打印之前简单地将“hello”附加到输入中:

main = getContents >>= (++ " hello") >>= putStrLn

我突然发现类型不匹配:

Couldn't match type `[]' with `IO'
  Expected type: String -> IO Char
    Actual type: [Char] -> [Char]
* In the second argument of `(>>=)', namely `(++ " hello")'
  In the first argument of `(>>=)', namely
    `getContents >>= (++ " hello")'
  In the expression: getContents >>= (++ " hello") >>= putStrLn

不知何故,IO 操作不再执行(或者我只是不明白这一点)。

我也尝试了很多东西,getLinereadLngetContentsunsafePerformIOreadfmap 的组合都无济于事。

这只是一个非常基本的例子,但它完美地说明了让我现在多次放弃 haskell 的问题(而且可能我不是唯一一个),尽管我很固执地想了解它,并且了解什么是函数式编程语言让我不断回头。

总结:

  1. 有什么我没有得到的吗?(99% 是)

  2. 如果是,那是什么?

  3. 我应该如何阅读整个标准输入并在一个连续的表达式中处理它?(如果我只需要 1 行,我想无论解决方案是什么,它也可以与 getLine 一起使用,因为它基本上是getContents 的妹妹)

提前致谢!

【问题讨论】:

  • 是的。您将包装在 IO monad 中的值和简单值混合在一起。如果您执行m >>= f,则包裹在IO 中的内容 将传递给ff 具有签名 f :: a -> IO b,因此它会产生另一个(很可能相同)值,包装在 IO monad 中。所以getContents >>= (++ " hello") 没有意义,因为(++ "hello") 不会返回IO b。但是,您可以使用 getContents >>= putStrLn . (++ " hello")
  • @WillemVanOnsem 很抱歉,但我仍然不清楚。如果仅将 IO 中包装的内容作为参数传递,那么在getContents >>= (++ " hello") 中,String(或[Char])应该是传递给(++ " hello") 的内容,对吧?而(++ " hello") 的类型为:: [Char] -> [Char],所以如果将String 传递给(++ " hello"),有什么问题?
  • 我认为你关于 do 表示法的结论是错误的,它只是一个语法扩展。如果你不明白如何编写没有 do 符号的程序,那么你就不会理解它。
  • 您似乎在寻找interact 函数。 main = interact (++ " hello")
  • 未来可能对您有所帮助的一件事是用do 表示法编写您的函数,然后手动对其进行脱糖,或者考虑一下您使用>>= 手动编写的表达式会是什么样子do 符号。例如,您的错误表达式getContents >>= (++ " hello") >>= putStrLn 对应于do 表示法do { x <- getContents; y <- x ++ " hello"; putStrLn y },明确错误在哪里:您有y <-,因此右侧必须是IO 操作,而是它只是一个String。一种解决方案是将其包装在pure(或return)中以使其成为一个动作。

标签: haskell io


【解决方案1】:
  1. 有什么我没有得到的吗?(99% 是)

    是的。

  2. 如果是,那是什么?

    IO StringString 的概念完全不同。前者就像菜谱,后者就像一顿饭。
    在您觉得自己是 Haskeller 专家之前,您最好忘记有 unsafePerformIO 这样的东西。这是您在普通 Haskell 代码中永远不需要的东西,仅用于将 FFI 绑定到不纯的 C 代码或用于最后的优化。

  3. 我应该如何阅读整个标准输入并在一个连续的表达式中处理它们?

    main = getContents >>= putStrLn . (++ " hello")
    

    请注意,这里只有两个 IO 操作:getContentsputStrLn。因此,您只需要一个 bind operator 即可从一项操作获取信息到另一项操作。
    在这两者之间,你有纯粹的(++ " hello")。这不需要任何单子绑定,只需 function composition 即可传递信息。
    如果你觉得信息流的混合方向很难看,也可以使用翻转绑定:

    main = putStrLn . (++ " hello") =<< getContents
    

    或者,您可以使用单子绑定,但您首先需要masquerade the pure function as an IO action(该操作不利用任何副作用可能性):

    main = getContents >>= pure . (++ " hello") >>= putStrLn
    

    或者您也可以,而不是“将 putStrLn 转换为在它打印的内容之后添加 " hello"”,而是将“transform getContents 以在它获取的内容之前添加 " hello"”:

    main = (++ " hello")<$>getContents >>= putStrLn
    

    根据单子定律,所有这些都是等价的。

【讨论】:

  • 谢谢!!这太清楚了。如果可以的话,还有一个问题。 pure 函数有什么作用?文档只是说它“提升了价值”,这是什么意思?
  • purereturn 几乎相同,只是更通用一些,并且可以说命名更好。 (return在某些情况下类似于过程语言的 return 关键字,但有非常显着的区别。)两者都只是“包装”一个纯值,因此它具有单子动作实际上没有任何副作用。 “化装舞会”实际上是我想说的一个恰当的词。
【解决方案2】:

你没有考虑的主要事情似乎是&gt;&gt;=的类型:

(>>=) :: IO a -> (a -> IO b) -> IO b

换句话说,你不需要将IO String“解包”到String&gt;&gt;= 运算符已经将一个普通的 String 传递给它的右操作数(函数):

getContents >>= (\s -> ...)
--                ^ s :: String

getContents &gt;&gt;= (++ " hello") 失败的原因是&gt;&gt;= 要求函数返回一个IO ... 值,但(++ "hello") :: String -&gt; String

您可以通过将return :: a -&gt; IO a 添加到组合中来解决此问题:

getContents >>= (return . (++ "hello"))

整个表达式的类型为IO String。执行时,它将从stdin 读取数据,将"hello" 附加到它,并返回结果字符串。

因此,

getContents >>= (return . (++ "hello")) >>= putStrLn

应该可以正常工作。但这比必要的复杂。从概念上讲,returnIO 中包装了一个值,&gt;&gt;= 再次将其解包(有点)。

我们可以融合右边的return/&gt;&gt;=位:

getContents >>= (\s -> putStrLn (s ++ "hello"))

即而不是采用getContents :: IO String,将"hello"添加到它以形成一个新的IO String动作,然后将putStrLn :: String -&gt; IO ()附加到它,我们包装putStrLn以创建一个新的String -&gt; IO ()函数(将"hello"附加到在将事情交给putStrLn之前,它的论点。

现在,如果我们愿意,我们可以通过标准的免积分技巧摆脱 s

getContents >>= (putStrLn . (++ "hello"))

关于IO 的一般说明:要记住的是IO ... 是普通的Haskell 类型。这里没有魔法发生。 &gt;&gt;= 不执行任何操作;它只是结合了一个IO something 类型的值和一个函数来构造一个IO somethingelse 类型的新值。

您可以将 Haskell 视为一种纯粹的元语言,它将命令式程序(即要执行的指令列表)构造为内存中的数据结构。唯一实际执行的是绑定到Main.main 的值。也就是说,它就像一个命令式运行时运行您的 Haskell 代码以在 main :: IO () 中生成一个纯值。然后这个值的内容作为命令式指令执行:

main :: IO ()
main =
    putChar 'H' >>
    putChar 'i' >>
    putChar '\n'

main 绑定到表示命令式程序print 'H'; print 'i'; print newline 的数据结构。运行 Haskell 程序会构建这个数据结构,然后运行时执行它。

不过,这个模型并不完整:命令式运行时可以回调到 Haskell 代码中。 &gt;&gt;= 可用于在命令式代码中“嵌入”Haskell 函数,然后可以(在运行时)检查值、决定下一步做什么等。但所有这些都以纯 Haskell 代码的形式发生;只有x &gt;&gt;= f 中从f 返回的IO 值很重要(f 本身没有副作用)。

【讨论】:

  • 感谢使用急需的(对新手而言)括号,即使是无关紧要和不酷的。 :)
  • "f 本身没有副作用" ... 因为它纯粹构造了一个 IO 动作描述,然后将由命令式运行时运行,届时可能会有一些副作用,按照构造。
【解决方案3】:
  1. 有什么我没有得到的吗?(99% 是)

是的。 &gt;&gt;= 右侧的函数是一个 函数,其签名为 <b>a</b> -&gt; <b>m b</b>m 单子。

比喻:生日礼物

帮助我理解 monad(以及具有绑定函数的函数 &gt;&gt;=)的东西考虑 IO

您还可以将 monad(请注意,IO 只是众多 monad 之一)视为一个集合。比如Maybe a,这也是一个monad。

您可以将Maybe a 视为某种“盒子”。该框可以有一个对象(如果是Just x),也可以是一个“空框”(而不是Nothing)。

现在绑定运算符&gt;&gt;=在左侧有这样一个框,在另一侧有一个函数f :: a -&gt; Maybe b。想象f 是一个人,现在是他们的生日。他/她收到盒子的内容,并且必须将另一个礼物传递给日历上的下一个人。所以绑定运算符&gt;&gt;=打开盒子,将它传递给这个人,并期待一个新的礼物,然后它可以被进一步处理。

长话短说:你必须返回一个新的盒子。现在(++ " hello") 将字符串作为输入,但它确实将该内容放入一个新框中(因此没有派对:()。

但是,您可以自己将其包装到一个盒子中。为了做到这一点,有return 函数(这是一个函数,不是一个关键字)。所以你可以写成:

getContents &gt;&gt;= <b>return .</b> (++ " hello") &gt;&gt;= putStrLn

请注意,函数不必给出相同的“现在”,但“盒子”(monad)的类型必须相同。例如putStrLn 的类型为String -&gt; IO ()。所以putStrLn 是一个你可以对存在的字符串感到满意的人,并且该函数将返回一个带有() 实例的框(或一个空框,顺便说一下@987654342 只有一个值@:())。

因此我们可以简单地通过一个函数来处理字符串,例如:

getContents &gt;&gt;= putStrLn<b> . (++ " hello")</b>

所以如果你写一段像这样的代码:

a >>= b >>= c >>= d >>= e

这意味着a 将构建第一个框。绑定运算符将打开该框并将内容处理到b。基于内容b 将构造一个新框(其中包含他/她知道c 喜欢的对象类型)。绑定运算符打开盒子并将内容传递给c,依此类推。

这与 I/O 有什么关系

I/O 函数可以看作是生日礼物故事中的人物。他们返回一个“IO 盒子”,可以说里面有内容。 bind 操作符会打开“IO 盒”(实际上&gt;&gt;= 是少数知道如何打开 IO 盒的函数之一),并将内容传递给下一个函数。通过这样做,它将强制执行命令,因为如果前一个人没有首先检查他们的礼物并构造一个新的礼物,那么下一个人就无法处理礼物。

Maybe重访

I/O 是相当难以理解的 I/O 单子,因为它连接到计算机系统中。一个更容易理解的 monad 是 Maybe monad。正如我们已经讨论过的,Maybe 具有以下构造函数:

data Maybe a = Nothing | Just a

现在我们将Nothing 想象为:

  +---+
 /   /|
+---+ +
|   |/
+---+

Just x 为:

  +---+
 /   /|
+---+ +
| x |/
+---+

一个包含内容的盒子。现在当然您可能知道如何解开Just x 构造函数并获取x 值。但是想象一下,我们不允许这样做。只有&gt;&gt;= 操作员可以打开Maybe 框。

然后我们可以构造一个monad:

instance Monad Maybe where
    return x = Just x

    (>>=) Nothing _ = Nothing
    (>>=) (Just x) f = f x

我们看到的是一个return 函数,它将一个对象包装到一个Maybe“盒子”中,所以使用return,我们可以制作一个“礼物”。

绑定运算符将检查左侧的当前。万一发现盒子里什么都没有,也不会打扰右边的人,直接返回Nothing

如果盒子包含x,它将将该对象提供给f,但期望f 构造一个新的礼物。 &gt;&gt;= 因此打开了现在。他/她是“专业的生日礼物开瓶器”。

所以现在我们可以这样写:

Just 2 >>= Just . (+5) >>= Just . (*6)

它将返回Just 42。为什么?我们从包含2 的礼物开始。 &gt;&gt;= 解包礼物,并将内容处理到Just . (+2),因此我们评估(Just . (+2)) 2。请注意,Just 2Just 已消失。所以现在我们对此进行评估,因此链中的第二个项目将Just 7 处理给系统。

这个包再次打开,你猜怎么着,它包含一个7,现在7被处理到最后一个函数Just . (*6)所以最后一个函数将它的当前乘以6并再次包装它放进盒子里。

如果你写:

Just 2 &gt;&gt;= (+5) &gt;&gt;= Just . (*6)

这将失败。为什么?因为显然第二个函数心情不好,忘了把礼物包在盒子里。

【讨论】:

  • 谢谢!盒子类比确实有效。虽然这很奇怪,以至于没有教程或文档(至少我发现)提到您需要在下一次绑定之前重新包装该值,但有些人甚至暗示您解开了它,它是一帆风顺的。