在Haskell中,为什么我需要在我的阻止之前放一个$?

Jon*_*han 11 haskell

[这个问题是由"真实世界Haskell"中的第9章推动的]

这是一个简单的功能(减少到必需品):

saferOpenFile path = handle (\_ -> return Nothing) $ do
      h <- openFile path ReadMode
      return (Just h)
Run Code Online (Sandbox Code Playgroud)

我为什么需要那个$

如果要处理的第二个参数不是do块,我不需要它.以下工作正常:

handle (\_ -> putStrLn "Error calculating result") (print x)
Run Code Online (Sandbox Code Playgroud)

当我尝试删除$编译失败.如果我明确添加parens,我可以让它工作,即

saferOpenFile path = handle (\_ -> return Nothing)  (do
      h <- openFile path ReadMode
      return (Just h))
Run Code Online (Sandbox Code Playgroud)

我可以理解这一点,但我想我希望当Haskell击中do它时应该想到"我正在开始阻止",我们不应该明确地把它放在$那里来解决问题.

我还想过将do块推到下一行,如下所示:

saferOpenFile path = handle (\_ -> return Nothing)  
  do
    h <- openFile path ReadMode
    return (Just h)
Run Code Online (Sandbox Code Playgroud)

但是如果没有parens也行不通.这让我感到困惑,因为以下工作:

add a b = a + b

addSeven b = add 7
   b
Run Code Online (Sandbox Code Playgroud)

我敢肯定,我只是达到了接受它的程度,"这就是你如何写出惯用的Haskell",但有没有人有任何观点可以给予?提前致谢.

Cal*_*lum 10

这是由于Haskell的运营顺序.功能应用程序绑定最紧密,这可能是混乱的来源.例如:

add a b = a + b
x = add 1 add 2 3
Run Code Online (Sandbox Code Playgroud)

Haskell将其解释为:函数add应用于1,函数add.这可能不是程序员的意图.Haskell会抱怨为第二个参数预期Num,但是会得到一个函数.

有两种解决方案:

1)表达式可以括起来:

x = add 1 (add 2 3)
Run Code Online (Sandbox Code Playgroud)

哪个Haskell将解释为:函数add应用于1,然后add 2的值3.

但是如果嵌套太深,这会让人感到困惑,因此第二种解决方案.

2)$运算符:

x = add 1 $ add 2 3
Run Code Online (Sandbox Code Playgroud)

$是一个将函数应用于其参数的运算符.Haskell将其读作:将函数(add 1)应用于add 2的值3.请记住,在Haskell中,函数可以部分应用,因此(add 1)是一个参数的完全有效的函数.

$运算符可以多次使用:

x = add 1 $ add 2 $ add 3 4
Run Code Online (Sandbox Code Playgroud)

您选择哪种解决方案将取决于您认为在特定上下文中更具可读性.

  • 谢谢,卡勒姆.我猜想($)主要是关于语法,当我试图写出它的定义并提出($)fx = f x.从我有限的资料来看,似乎Haskellers真的希望在可能的情况下避免使用parens.我想这很难来自其他语言,我擅长阅读添加(1,添加(3,4)).特别是当使用组合器时 (3认同)

Rei*_*ton 10

这实际上并不是特定于do注释.你也不能写像print if x then "Yes" else "No"print let x = 1+1 in x*x.

您可以从Haskell报告第3章开头的语法定义中验证这一点:do表达式或条件或let表达式是lexp,但函数应用程序的参数是aexp,而lexp通常无效为aexp.

当然,这并没有告诉你为什么在报告中做出这样的选择.如果我不得不猜测,我可能会猜测它是将有用的规则"功能应用程序比其他任何东西更紧密地"扩展到绑定,比如do和它的块之间.但我不知道这是否是最初的动机.(我发现被拒绝的表单print if x then ...很难阅读,但这可能只是因为我习惯于阅读Haskell接受的代码.)


Dan*_*ton 6

根据Haskell 2010报告,dodesugars像这样:

do {e;stmts} = e >>= do {stmts}

do {p <- e; stmts} = let ok p = do {stmts}
                         ok _ = fail "..."
                     in e >>= ok
Run Code Online (Sandbox Code Playgroud)

对于第二种情况,将它写成这样的desugaring是相同的(并且更容易用于演示目的):

do {p <- e; stmts} = e >>= (\p -> do stmts)

所以假设你写这个:

-- to make these examples shorter
saferOpenFile = f
o = openFile

f p = handle (\_ -> return Nothing) do
    h <- o p ReadMode
    return (Just h)
Run Code Online (Sandbox Code Playgroud)

它不知道这个:

f p = handle (\_ -> return Nothing) (o p ReadMode) >>= (\h -> return (Just h))
Run Code Online (Sandbox Code Playgroud)

与此相同:

f p = (handle (\_ -> return Nothing) (o p ReadMode)) >>= (\h -> return (Just h))
Run Code Online (Sandbox Code Playgroud)

即使你可能意味着这意味着:

handle (\_ -> return Nothing) ((o p ReadMode) >>= (\h -> return (Just h)))
Run Code Online (Sandbox Code Playgroud)

tl; dr - 什么时候语法是desugared,它没有像你认为的那样括起整个语句(从Haskell 2010开始).

这个答案只有AFAIK和

  1. 可能不正确
  2. 如果它是正确的,有一天可能会过时

注意:如果你对我说"但实际的脱糖使用'让’语句应该组e >>= ok,对吗?",然后我会回答一样TL;博士做陈述的:它不括起/组喜欢你认为它确实如此.

  • 这不是这个答案.去糖总是适用于完成的抽象语法树,这意味着它需要"完全括号"输入并产生"完全括号"输出.它不会更改有效输入的定义,也不会更改需要括号的定义.(它的定义更像Lisp宏而不是像Lisp读取器宏或C的令牌序列宏). (2认同)