【问题标题】:Is h a handle or a lambda function (or both)?h 是句柄还是 lambda 函数(或两者兼而有之)?
【发布时间】:2019-04-15 01:08:51
【问题描述】:

我正在查看来自the Haskell Wikibook 的简单 IO 程序。该页面上显示的结构工作得很好,但我试图理解“如何”。

下面的writeChar 函数接受文件路径(作为字符串)和字符,并将字符写入给定路径的文件。该函数使用bracket 来确保文件正确打开和关闭。在括号中运行的三个计算中,“中间运行的计算”——据我所知——是一个 lambda 函数,它返回 hPutChar h c 的结果。

现在,hPutChar 本身有一个 hPutChar :: Handle -> Char -> IO () 的声明。这就是我迷路的地方。我似乎将h 作为hPutChar 的句柄传递。我希望一个句柄以某种方式引用以fp 打开的文件,但它似乎是递归调用 lambda 函数\h。我不明白这个调用自身的 lambda 函数如何知道将c 写入fp 的文件。

我想了解为什么这个函数的最后一行不应该是(\h -> hPutChar fp c)。尝试以这种方式运行它会导致“无法将类型‘[Char]’与‘Handle’匹配”,考虑到 hPutChar 需要 Handle 数据类型而不是字符串,我认为这是明智的。

import Control.Exception
writeChar :: FilePath -> Char -> IO ()
writeChar fp c =
    bracket
      (openFile fp WriteMode)
      hClose
      (\h -> hPutChar h c)

【问题讨论】:

  • fp 是一个 FilePath(实际上只是一个字符串)。 lambda 表达式中的hHandle,它位于openFile 返回的IO monad 中。见hackage.haskell.org/package/base-4.12.0.0/docs/System-IO.html
  • \h 本身并不是一个真正的实体;它只是 lambda 函数 \h -> hPutChar fp c 的一部分。
  • 我误导自己认为同一行上的h\h 指的是同一个实体!
  • 在 cmets 中保持参与和交谈的重要性:感谢您的上述评论,您的误解(“\h”本身就是一个实体)终于变得清晰并且可以解决! /++

标签: haskell lambda syntax functional-programming io-monad


【解决方案1】:

让我们看看bracket 的类型(引用它出现在您的Haskell Wiki 链接中):

bracket :: IO a        -- computation to run first ("acquire resource")
        -> (a -> IO b) -- computation to run last ("release resource")
        -> (a -> IO c) -- computation to run in-between
        -> IO c

在您的用例中,第一个参数openFile fp WriteMode 是一个IO Handle 值,该计算产生与fp 路径相对应的句柄。第三个参数\h -> hPutChar h c 是一个函数,它接受一个句柄并返回一个写入它的计算。这个想法是,您作为第三个参数传递的函数指定了如何使用第一个参数生成的资源。

【讨论】:

    【解决方案2】:

    这里没有递归。 h 确实是 Handle。如果您使用 C 进行编程,粗略的等价物是 FILE。句柄由文件描述符、缓冲区以及在附加文件/管道/终端/任何其他内容上执行 I/O 所需的任何其他内容组成。 openFile 获取路径,打开请求的文件(或设备),并提供可用于操作请求的文件的句柄。

    bracket
      (openFile fp WriteMode)
      hClose
      (\h -> hPutChar h c)
    

    这会打开文件以生成句柄。该句柄被传递给第三个函数,该函数将其绑定到h 并将其传递给hPutChar 以输出一个字符。然后最后bracket将句柄传递给hClose关闭文件。

    如果不存在异常,您可以像这样实现bracket

    bracket
      :: IO resource
      -> (resource -> IO x)
      -> (resource -> IO a)
      -> IO a
    bracket first last middle = do
      resource <- first
      result <- middle resource
      last resource
      pure result
    

    但是bracket 实际上必须安装一个异常处理程序来承受last 即使发生异常也会被调用。

    【讨论】:

      【解决方案3】:
      hPutChar :: Handle -> Char -> IO ()
      

      是一个纯 Haskell 函数,给定两个参数 h :: Handlec :: Char,产生一个 IO () 类型的纯 Haskell 值,即“IO 动作”:

               h :: Handle      c :: Char
      ---------------------------------------------
      hPutChr  h                c          :: IO ()
      

      这个“动作”只是一个 Haskell 值,但是当它出现在 main 下的 IO monad do 块中时,它会由 Haskell 运行时系统执行,并且然后它实际上执行 I/O 操作,将字符 c 放入由句柄 h 引用的文件系统实体中。

      对于 lambda 函数,实际明确的语法是

      (\ h -> ... ) 
      

      \h 之间的空格是可选的,整个 (.......) 表达式是 lambda 表达式。所以有没有\h实体”:

      • (\ ... -&gt; ... ) 是 lambda 表达式语法;
      • \ h -&gt;中的h是lambda函数的参数,
      • (\ h -&gt; ... ) 中的 ... 是 lambda 函数的主体。

      bracket调用(\h -&gt; hPutChar h c)lambda函数,结果由(openFile fp WriteMode)I/O产生计算,即fp引用的文件名句柄,根据打开到WriteMode模式。

      关于 Haskell monadic IO 的主要理解是“计算”不是一个函数:它是执行实际文件打开的实际(这里是 I/O)计算——它产生句柄——然后运行时系统使用它来调用 纯 Haskell 函数 (\ h -&gt; ...)

      这个分层(纯粹的 Haskell“世界”和不纯的 I/O“世界”)是 .... 的精髓,是的,Monad。一个 I/O 计算做某事,找到一些值,用它来调用一个纯 Haskell 函数,它创建一个新的计算,然后运行,将 its 结果提供给下一个纯函数,等等。等等

      因此,我们在 Haskell 中只通过谈论不纯的东西来保持我们的纯洁性。说不做。

      是吗?

      【讨论】:

      • "h in \ h -> is the lambda function's parameter" -- 这就是我所缺少的,以及 bracket 将其第一次计算的结果输入到中间的理解计算(@dfeuer 总结得很好)。我错误地将匿名函数的参数读取为匿名函数的“名称”。 ?
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-09-22
      • 2011-09-26
      • 1970-01-01
      • 2014-07-20
      • 1970-01-01
      • 2011-10-16
      相关资源
      最近更新 更多