Haskell中的语句排序?

3
我是一名有用的助手,可以为您进行文本翻译。以下是需要翻译的内容:

我正在尝试编译一个非常简单的Haskell程序。要么我不知道如何在Haskell中对语句(表达式)进行排序,要么就没有这样的方法。

我基本上想要一个形式为:

f = <do stuff 1>
    <do stuff 2>
    ...

以下内容旨在模拟一种模式。
<do stuff>
<tail recursion>

但编译失败:

go 0 = 0
go i =  case i of
            1 -> 2
            _ -> 3
        go (i-1)

这也不起作用(更简单,没有递归):
go i =  1
        2

上面的代码可以编译,但在运行时我收到了晦涩难懂的错误信息:

No instance for (Num (a0 -> t0))
  arising from a use of `go'
Possible fix: add an instance declaration for (Num (a0 -> t0))
In the expression: go 2
In an equation for `it': it = go 2

我是否存在缩进问题?或者是没有正确排序的问题?还是没有排序能力?如果没有排序,如何直接处理?

<do stuff>
<tail recursion>

感谢您的回复。

很难相信Haskell中的函数基本上只限于一个“表达式”或“语句”或您想要称呼它的东西。您将如何解决以下人为制造的玩具示例(使用Erlang编写,但可以轻松地直接转换为Prolog或其他许多语言):

count([], X) -> X;
count([_|B], X) ->
    Y = X+1,
    count(B, Y).

一个运行示例:

erlang> count([a,b,c,d,e],0).
5

第二个条款符合我之前描述的模式,即:
<do stuff>
<tail recursion>

我猜这个例子可以映射到Haskell中的“let...in”,就像这里描述的那样。但是如果需要进行10个“do stuffs”,或者20个,或更多呢?而且这个“let...in”是否保证是尾递归的(上面的例子在Erlang、Prolog、Scheme等语言中保证是尾递归)。
是否有一种方法可以简单地“串联”或“序列化”一堆语句或表达式?在Prolog或Erlang中,通过逗号实现序列化(参见上面的玩具示例)。在c/c++中,它是通过分号实现的。我认为Haskell运算符只是空格,但也许在这种语言中不这样做?

8
表达式序列化的作用是什么?先求出第一个表达式,然后丢弃它,再求出并得出第二个表达式的结果吗?这样做没有意义,对纯代码进行求值永远不会产生任何副作用。 - user395760
11
在你开始编写代码之前,我建议你稍微学习一下Haskell的语法。在Haskell中,语句是由“do”引入的,并且只在单子代码中相关。 - augustss
1
也许你正在寻找let ... in ...?(不是顺序:在后续表达式中引入变量然后使用它们)。如果不知道你的简单程序尝试做什么,我们就无法帮助你解决问题。 - dave4420
@delnan:在某些情况下这是有意义的(你刚刚描述了seq的作用),但在这里不是。 - hammar
2
如果你想要做的“事情”是引入新的绑定,那么let语句就足够了。你可以通过换行或分号将多个绑定引入到单个let语句中。此外,在非严格性的情况下,尾递归是毫无意义的;Haskell栈的工作方式与严格语言的栈完全不同。关于你的最后一个问题,我坚持我的原始答案:在Haskell中你组合。对于IO操作,这通常使用>>=实现。对于纯函数,.是规范的组合运算符。 - Dan Burton
显示剩余2条评论
7个回答

25
在Haskell中,你不会编写“执行操作”的函数,而是编写计算值的函数。从这个角度来看,按照你提出的方式构建一个函数是没有意义的:

必须的梗

f = <do stuff 1>
    <do stuff 2>

相反,你需要编写一个实际的、数学化的函数,它是一个仅依赖于特定输入的单个表达式:

f x y = blah x blah y blah

将其分成子表达式可能会更方便

f x y = res1 + res2
  where res1 = x * 3
        res2 = y + 4

但最终,它只是一个单独的表达式。


使用do符号写成的单子代码看起来可以“顺序地执行操作”:

f = do
  thing1
  thing2

但这只是一种假象。它也可以被简化为单个表达式:

f = thing1 >> thing2

这个表达式说明f是一个值,它是由thing1thing2组合而成的。现在,IO代码可能会编译成按顺序“执行操作”的代码,但在Haskell中并不是这个意思。在Haskell中,你不能简单地“执行操作”。相反,你需要进行组合


6
对于一个不错的解释并巧妙地利用了互联网迷因,我给一个赞。 - user395760
你的Haskell代码似乎是顺序执行的,因为在计算res1+res2之前已经计算出了res1和res2。那么你能用do实现同样的功能吗? - X Y Z
5
@XYZ: res1res2不一定在表达式之前计算。(在这种情况下,它们是如此计算的,但并不仅仅因为它们在while语句中被定义。)因为Haskell是惰性的,编译器可以自由地重新排列这些计算。你没有指定执行顺序;你只是指定了每当需要计算res1res2时应该如何计算它们。 - Louis Wasserman
例如,如果是 f x y = if res2 /= 0 then res1 else 0 where ...,那么除非 res2 不为零,否则 res1 不一定会被计算。 - Louis Wasserman

5
非常棒,你能够提供一个Erlang代码示例来说明你所说的“做事情”的意思。之前有些模糊不清。只要讨论的是严格的语言,对于这一点有些模糊也没关系,因为规则相当统一并且大家都理解。
另一方面,我们正在谈论Haskell,这是一种具有“惰性”评估的语言,因此非常重要明确“做事情”的含义,否则由于错误的假设可能会导致混淆。
以下是您提供的Erlang函数在Haskell中的相对直接的翻译:
count ([],x) = x
count (_:b, x) =
    let y = x + 1
    in count (b, y)

如您所见,它的结构与Erlang版本几乎完全相同。因此,您可能会想知道有什么了不起的?如果在Haskell中“做事情”如此容易,为什么每个人都觉得它很复杂?
解释其重要性的一种方法是指出上述Haskell函数等效于以下函数:
count ([],x) = x
count (_:b, x) = count (b, x + 1)

此外还有这个:
count ([],x) = x
count (_:b, x) = count (b, y) where y = x + 1

这是因为Haskell计算表达式的顺序不取决于这些表达式的词法顺序。一般来说,Haskell将会延迟表达式的计算直到最后一刻。
现在,当你考虑将这种惰性评估方案与副作用(读写文件、向屏幕发送输出等)组合起来时,就有点模糊了。副作用发生的顺序通常非常重要。幸运的是,在处理副作用的Haskell函数中一般不会执行实际的副作用操作。它们会生成一个“action”值,用于描述副作用。然后有其他函数可以以尊重顺序的方式组合这些“action”值。然后你只需要一个实际执行由“action”值描述的副作用的东西...

2
基本上,没有办法做到。Haskell是一种纯函数式语言,既没有语句的改变也没有顺序。模拟序列的一种方式是使用单子。单子在互联网上有很多教程,我个人建议您阅读Learn You A Haskell For Great Good!Real World Haskell中的单子章节。另外一件事是:在Haskell中,顺序操作并不是很惯用。你最好从其他方面入手。

3
我强烈反对“没有办法做到这件事”。相反,我会说,“有一种通用的方法可以做到,你可以用它来统一表达所有你所谓的‘排序’所能意味的东西”(要始终保持积极!) - Alexandre C.
@AlexandreC 这就是我想通过“模拟序列的一种方法…”来暗示的。你的措辞可能更好,随意调整我的回答。 - fuz

1
当你说“做事情”时,你指的是什么?
  • 计算某些东西,转换我们已经拥有的数据?
  • 与用户交互,读/写文件,从网络下载某些东西?

在第一种情况下,请使用let...in

quadraticRoots a b c = let discriminant = b * b - 4 * a * c
                           root = sqrt discriminant
                       in case compare discriminant 0 of
                               LT -> []
                               EQ -> [-b / (2 * a)]
                               GT -> [(-b + root) / (2 * a), (-b - root) / (2 * a)]

在第二种情况下,使用do符号:
main = do args <- getArgs
          case map read args of
               [a, b, c] -> putStrLn $ case quadraticRoots a b c of
                                            [] -> "no real roots"
                                            [x] -> "single root: " ++ show x
                                            [x1, x2] -> unwords ["two real roots:", show x1,
                                                                 "and", show x2]
               _ -> hPutStrLn stderr "Please provide three coefficients on the command line"

6
我建议使用 sqrt 而不是 (** 0.5),它更快且更具描述性。 - augustss

1

更新: 注意不要在 Haskell 中编写 Scheme 程序。累积参数在惯用的 Haskell 中很少出现,它使用惰性而不是尾递归。

例如,在 Haskell 中定义 count 的方式将类似于

count [] = 0
count (x:xs) = 1 + count xs

你可能会反对 length的源代码看起来更像Scheme-ish

-- | /O(n)/. 'length' returns the length of a finite list as an 'Int'.
-- It is an instance of the more general 'Data.List.genericLength',
-- the result type of which may be any kind of number.
length                  :: [a] -> Int
length l                =  len l 0#
  where
    len :: [a] -> Int# -> Int
    len []     a# = I# a#
    len (_:xs) a# = len xs (a# +# 1#)

但这是低级、非惯用的代码。引用的genericLength与上面的count具有相同的结构,并且与length相比具有更广泛的适用性。

genericLength           :: (Num i) => [b] -> i
genericLength []        =  0
genericLength (_:l)     =  1 + genericLength l

在 Haskell 中,“先做这个,然后做那个”被表达为纯代码中的函数组合。例如,要计算最长子列表的长度,我们将计算子列表的长度,然后计算这些长度的最大值。在 Haskell 中,这是这样表达的:
Prelude> :t maximum . map length
maximum . map length :: [[a]] -> Int

或者

Prelude> :m + Data.List 
Prelude Data.List> :t maximum . map genericLength
maximum . map genericLength :: (Ord c, Num c) => [[b]] -> c

注意:懒惰性增加了细微差别,但总体观点仍然成立。
即使在IO内部的“命令式”代码中,
main :: IO ()
main = do
  putStr "Hello "
  purStrLn "world!"

函数组合是“在覆盖下”的,因为它具有与之相同的语义。

main :: IO ()
main = putStr "Hello " >>= \_ -> putStrLn "world!"

也许使用列表单子的示例会更加清晰。假设我们想要找到所有勾股三元组 (a,b,c),使得没有一个分量大于某个最大值 n
import Control.Monad (guard)

triples :: Integer -> [(Integer,Integer,Integer)]
triples n = do
  a <- [1 .. n]
  b <- [a .. n]
  c <- [b .. n]
  guard $ a*a + b*b == c*c
  return (a,b,c)

正如你所看到的,我们必须首先选择a的候选值,然后是b,依此类推。

编译器会将此代码转换为显式使用单子绑定组合器>>=

-- indented to show nested scopes
triples_bind :: Integer -> [(Integer,Integer,Integer)]
triples_bind n =
  [1 .. n] >>= \a ->
    [a .. n] >>= \b ->
      [b .. n] >>= \c ->
        (guard $ a*a + b*b == c*c) >>= \_ ->
          return (a,b,c)

在列表单子里,>>=concatMap,所以上述代码等同于:
triples_stacked :: Integer -> [(Integer,Integer,Integer)]
triples_stacked n =
  concatMap (\a ->
    concatMap (\b ->
      concatMap (\c ->
        concatMap (\_ ->
          [(a,b,c)])
          (guard $ a*a + b*b == c*c))
        [b .. n])
      [a .. n])
    [1 .. n]

缩进显示结构,给人以赏心悦目的印象,因为它呈现了Haskell标志的组合λ和>>=
表达相同语义的另一种方式是:
triples_cm n = concatMap step2 [1 .. n]  -- step 1
  where step2 a = concatMap step3 [a .. n]
          where step3 b = concatMap step4 [b .. n]
                  where step4 c = concatMap step5 (guard $ a*a + b*b == c*c)
                          where step5 _ = [(a,b,c)]

Haskell的模式匹配已经在幕后执行了case匹配。与其这样,
go 0 = 0
go i =  case i of
            1 -> 2
            _ -> 3
        go (i-1)

完成你开始的模式。

go 0 = 0
go 1 = go (2 - 1)
go _ = go (3 - 1)

请记住,Haskell 中的变量是不可变的:它们没有像 C 或 Java 中那样的破坏性更新。您只有一次机会定义变量的值,然后就必须使用它。您可以将 go 定义为:

go 0 = 0
go x = let i = case x of 1 -> 2
                         _ -> 3
       in go (i - 1)

或者

go 0 = 0
go x | x == 1    = go (2 - 1)
     | otherwise = go (3 - 1)

以上示例都经过了类型检查,但存在一个共同的大问题,即go仅在其参数为零时终止。你没有提供足够的信息来帮助我们解决这个问题。告诉我们你想做什么,我们可以针对你的情况提供具体的建议。

是的,我已经知道Haskell在幕后进行模式匹配/案例匹配,并且玩具示例可以转换为您的示例。重点只是创建一个“做事情”的表达式,因此使用了case语句(它可以是任何其他语句/表达式)。请参见我的帖子顶部的编辑,其中包含计数函数,这不是一个“玩具”示例(尽管仍然是一个玩具示例)。重点不在于关注任何细节,而是“如何在一个函数中有几个表达式或语句(即‘序列化’),或者是否可能?” - X Y Z
@XYZ 请查看更新的答案。尽量不要用命令式语调来使用Haskell。是的,你可以在IO中表达显式顺序,但这应该是例外。让大部分代码保持在愉快的纯函数世界中,只有最薄的命令式皮肤与外部世界进行交互。 - Greg Bacon
不知道我是否在说"命令式"语言。你认为我发布的 Erlang 代码示例是命令式的吗?它有"序列化",但我想大多数人会称其为"函数式"风格(顺便说一句,当我说"序列化"时,我并不一定是指 IO,我只是指"一系列表达式"),而且它没有破坏性地更新。无论如何,非常感谢您深入的解释!非常有见地! - X Y Z
@XYZ:你的计数示例将变为count [] x = x; count (_:bs) x = count bs (1 + x)--或者你可以写成count [] x = x; count (_:bs) x = let y = x + 1 in count bs x,或等价于count [] x = x; count (_:bs) x = count bs y where y = x + 1。你正在寻找一个let或where子句来为你创建这些局部名称,但这些不是有序的效果,let并不会使事情发生,只允许你引用共享的值/计算。 - Edward Kmett

0
一个函数在数学上是由其从输入到输出的唯一映射定义的。序列是通过将输出转发到新函数来创建的。因此,在Haskell中处理序列的自然方式是函数组合... f2 . f1。有时,函数返回的内容超过了下一步所需的内容。因此,您需要使用letwhere提取要转发到下一步的内容。

转发仅指从输出到输入。从输入到输出,它是不同变量之间的映射(一个函数)。

没有任何东西被改变。没有副作用。

do符号也是函数组合。您可能认为某些东西(Monad)是通过函数处理并可能更改的,但实际上它没有被更改。相反,构造了一个修改后的副本。好吧,理想情况下是这样,因为类似于IO的单子会对外部世界进行更改。


0

我不确定你的函数需要做什么。也许你需要像这样的东西:

go :: Int -> Int
go 0 = 0
go i = go (i-1)

如果该函数需要参数,例如'3',它会计算go 3 -> go 2 -> go 1 -> go 0 -> 0并得到答案'0'

现在,如果您想要一些排序,可以使用关键字'do',但我确定这不是您所需的。如果您正在尝试创建某种“变量”,请查看“let”和“in”语句:

someInput = 7

gogogo 0  = 0
gogogo n = 
    let
        gavk = n + 1
        kravk = gavk * 2
        out = kravk + 1
    in 
        out

main = 
   do
       putStrLn "this is sample do statement"
       putStrLn "gogogo:"
       putStrLn $ gogogo someInput 

更新:修正了打字错误。


imputinputgogogo是什么? - Andre
2
你不能使用 in 作为变量名。 - augustss

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接