如果我来自命令式编程背景,我如何围绕没有动态变量的想法来跟踪Haskell中的事物?

时间:2011-12-09 00:58:45

标签: variables haskell imperative

所以我正在努力教自己Haskell。我目前正处于Learn You a Haskell for Great Good的第11章,正在进行99 Haskell Problems以及Project Euler Problems

事情进展顺利,但每当我需要跟踪“变量”时,我发现自己经常做某事。我只是创建另一个函数,接受那些“变量”作为参数,并根据情况递归地提供不同的值。举一个例子来说明,这是我对Problem 7 of Project Euler, Find the 10001st prime的解决方案:

answer :: Integer
answer = nthPrime 10001

nthPrime :: Integer -> Integer
nthPrime n
  | n < 1 = -1
  | otherwise = nthPrime' n 1 2 []


nthPrime' :: Integer -> Integer -> Integer -> [Integer] -> Integer
nthPrime' n currentIndex possiblePrime previousPrimes
  | isFactorOfAnyInThisList possiblePrime previousPrimes = nthPrime' n currentIndex theNextPossiblePrime previousPrimes
  | otherwise = 
    if currentIndex == n 
      then possiblePrime 
      else nthPrime' n currentIndexPlusOne theNextPossiblePrime previousPrimesPlusCurrentPrime
        where currentIndexPlusOne = currentIndex + 1
              theNextPossiblePrime = nextPossiblePrime possiblePrime
              previousPrimesPlusCurrentPrime = possiblePrime : previousPrimes

我想你明白了。让我们也忽略这样一个事实,即这个解决方案可以更高效,我知道这一点。

所以我的问题是一个由两部分组成的问题。首先,我对Haskell的看法是错的吗?我是否陷入了强制性的编程思维模式,并没有像我一样接受Haskell?如果是这样,我觉得我是,如何避免这种情况?是否有一本书可以指出我可以帮助我更多地考虑Haskell?

非常感谢您的帮助,

-Asaf

6 个答案:

答案 0 :(得分:14)

  

我是否陷入了强制性的编程心态而不是拥抱   哈斯克尔应该如此?

你没有被卡住,至少我不希望如此。你经历的是绝对正常的。当你使用命令式语言时,你学习(可能不知道)从非常具体的角度看待编程问题 - 即van Neumann机器。

如果你有一个问题,比如说,制作一个包含一些数字序列的列表(假设我们想要前1000个偶数),你会立即想到:一个链表实现(可能来自你的标准库)编程语言),一个循环和一个你设置为起始值的变量,然后你会循环一段时间,通过添加2并将它放到列表的末尾来更新变量。

了解您最常想如何为机器提供服务?内存位置,循环等! 在命令式编程中,人们会考虑如何以特定顺序操纵某些存储器单元以始终到达的解决方案。 (顺便说一下,初学者很难找到学习(命令式)编程的一个原因。非程序员根本不习惯通过将问题简化为一系列记忆操作来解决问题。为什么要这样做?但是一旦你了解到了,你有权力 - 在命令式的世界。对于函数式编程,你需要忘掉它。)

在函数式编程中,特别是在Haskell中,您只需说明列表的构造规律。因为列表是递归数据结构,所以这个定律当然也是递归的。在我们的例子中,我们可以举例如下:

constructStartingWith n = n : constructStartingWith (n+2)

差不多完成了!要到达我们的最终列表,我们只需要说明从哪里开始以及我们想要多少:

result = take 1000 (constructStartingWith 0)

请注意,库中提供了更为通用的constructStartingWith版本,它被称为iterate,它不仅需要起始值,还需要使用当前下一个列表元素的函数之一:

iterate f n = n : iterate f (f n)
constructStartingWith = iterate (2+)   -- defined in terms of iterate

另一种方法是假设我们有另一个列表,我们的列表可以轻松制作。例如,如果我们有前n个整数的列表,我们可以通过将每个元素乘以2来轻松地将它放入偶数整数列表中。现在,Haskell中前1000个(非负)整数的列表就是< / p>

[0..999]

还有一个函数map通过将给定函数应用于每个参数来转换列表。我们想要的功能是将元素加倍:

double n = 2*n

因此:

result = map double [0..999]

稍后您将了解更多快捷方式。例如,我们不需要定义double,但可以使用以下部分:(2*)或者我们可以将列表直接编写为序列[0,2..1998]

但不知道这些技巧不应该让你感觉不好!你现在面临的主要挑战是开发一种心态,你构建前1000个偶数列表的问题是两个阶段:a)定义所有偶数列表的方式数字看起来像和b)采取该列表的某一部分。一旦你开始这样思考即使你仍然使用手写版iteratetake

,你也会完成

回到欧拉问题:在这里我们可以使用自上而下的方法(以及一些应该确实知道的基本列表操作函数:headdropfilter,{ {1}})。首先,如果我们已经有了素数列表,我们可以放弃前1000个并取其余的头来获得第100个:

any

我们知道在从无限列表中删除任意数量的元素之后,仍然会有一个非空的列表来从中选择result = head (drop 1000 primes) ,因此,head的使用在这里是合理的。如果您不确定是否有超过1000个素数,您应该写下类似的内容:

head

现在是困难的部分。不知道如何继续,我们可以编写一些伪代码:

result = case drop 1000 primes of
    [] -> error "The ancient greeks were wrong! There are less than 1001 primes!"
    (r:_) -> r

我们肯定知道2是第一个素数,基本情况,可以这么说,因此我们可以把它写下来。未填充的部分给了我们一些思考的东西。例如,列表应该从某个值开始,因为显而易见的原因,该值大于2。因此,精炼:

primes = 2 : {-an infinite list of numbers that are prime-}

现在,这就是出现一种人们需要学会识别的模式的地步。这肯定是由谓词编写的primes = 2 : {- something like [3..] but only the ones that are prime -} 列表,即素数(我们不知道如何检查质量,逻辑结构并不重要)是重要的一点。(而且,我们可以肯定,质量测试是可能的!))。这允许我们编写更多代码:

filter

请参阅?我们差不多完成了。在3个步骤中,我们以这样一种方式减少了一个相当复杂的问题,即所有留下来写的都是一个非常简单的谓词。 同样,我们可以写伪代码:

primes = 2 : filter isPrime [3..]

并且可以改进它。由于这几乎已经是haskell,所以太容易了:

isPrime n = {- false if any number in 2..n-1 divides n, otherwise true -}

请注意,我们尚未进行优化。例如,我们可以构建要立即过滤的列表以仅包含奇数,因为我们知道偶数不是素数。更重要的是,我们希望减少我们在isPrime中尝试的候选人数量。在这里,需要一些数学知识(当然,如果用C ++或Java编程,也是如此),这告诉我们检查我们测试的isPrime n = not (any (divides n) [2..n-1]) divides n p = n `rem` p == 0 是否可被任何素数整除就足够了数字,并且我们不需要通过方数大于n的素数来检查可除性。幸运的是,我们已经定义了素数列表,可以从那里挑选一组候选人!我把它当作练习。

您稍后将学习如何使用标准库和句法糖,如部分,列表推导等等,您将逐渐放弃编写自己的基本功能。

即使以后,当你必须再次使用命令式编程语言时,如果没有infinte列表,更高阶函数,不可变数据等,你会发现它很难生存。 这与从C回到汇编程序一样困难。

玩得开心!

答案 1 :(得分:12)

一开始就有必要的心态。随着时间的推移,您将更加习惯并开始看到可以拥有更多功能性程序的地方。实践是完美的。

至于使用可变变量,如果你遵循将变量转换为函数参数并迭代到尾递归的经验法则,你现在可以保留它们。

答案 2 :(得分:4)

脱离我的头顶:

答案 3 :(得分:3)

我认为从代码到代码更多类似代码的重大变化是更好地使用更高阶函数,模式匹配和懒惰。例如,您可以像这样编写nthPrime函数(使用与您所做的类似的算法,再次忽略效率):

nthPrime n = primes !! (n - 1) where
  primes = filter isPrime [2..]
  isPrime p = isPrime' p [2..p - 1]
  isPrime' p [] = True
  isPrime' p (x:xs) 
    | (p `mod` x == 0) = False
    | otherwise = isPrime' p xs

例如nthPrime 4返回7.有几点需要注意:

  • isPrime'函数使用模式匹配来实现函数,而不是依赖于if语句。
  • primes值是所有素数的无限列表。由于haskell是懒惰的,这是完全可以接受的。
  • 使用
  • filter而不是使用递归重新实现该行为。

凭借更多的经验,您会发现您将编写更多惯用的haskell代码 - 它会随着经验自动发生。所以不要担心,只是继续练习,并阅读其他人的代码。

答案 4 :(得分:2)

另一种方法,只是为了多样化!强烈使用懒惰......

module Main where

nonmults :: Int -> Int -> [Int] -> [Int]
nonmults n next [] = []
nonmults n next l@(x:xs)
   | x < next = x : nonmults n next xs
   | x == next = nonmults n (next + n) xs
   | otherwise = nonmults n (next + n) l

select_primes :: [Int] -> [Int]
select_primes [] = []
select_primes (x:xs) = 
  x : (select_primes $ nonmults x (x + x) xs)

main :: IO ()
main = do
  let primes = select_primes [2 ..]
  putStrLn $ show $ primes !! 10000 -- the first prime is index 0 ...

答案 5 :(得分:1)

我想尝试在不使用任何函数式编程或数学的情况下回答您的问题,不是因为我认为您不会理解它,而是因为您的问题很常见,也许其他人会从心态中受益我会尝试描述。我将通过任何方式说我不是Haskell 专家来作为序言,但我已经通过实现以下内容而超越了你所描述的心理障碍:

1。 Haskell是简单

Haskell和其他我不熟悉的函数式语言肯定与你的“普通”语言有很大的不同,比如C,Java,Python等。不幸的是,我们的心灵工作方式,人类过早地得出结论如果有什么不同,那么A)他们不理解它,B)它比他们已经知道的更复杂。如果我们非常客观地看待Haskell,我们会发现这两个猜想是完全错误的:

“但我不明白:(”

其实你这样做。 Haskell和其他函数语言中的所有内容都是根据逻辑和模式定义的。如果你能回答一个简单的问题,如果“如果所有的Meep都是Moops,并且所有的Moops都是Moors,都是Meeps Moors?”,那么你可以自己编写Haskell Prelude。为了进一步支持这一点,请考虑Haskell lists are defined in Haskell terms, and are not special voodoo magic

“但这很复杂”

实际上恰恰相反。它的简单性是如此的赤裸裸,以至于我们的大脑起初很难弄清楚该怎么做。与其他语言相比,Haskell实际上具有相当少的“特性”和更少的语法。当您阅读Haskell代码时,您会注意到几乎所有的函数定义在风格上看起来都相同。这与例如Java有很大的不同,它有像Classes,Interfaces,for循环,try / catch块,匿名函数等构造......每个都有自己的语法和习语。

您再次提到$.,只需记住它们的定义与任何其他Haskell函数一样,并且不一定需要使用。但是,如果你没有这些,那么随着时间的推移,当你发现它们有多方便时,你可能会自己实现这些功能。

2。

没有任何 Haskell版本

这实际上是一个伟大的事物,因为在Haskell中,我们可以自由地定义完全我们想要的东西。大多数其他语言提供了人们串联到程序中的构建块。在用它构建之前,Haskell首先要让你首先定义构建块是什么。

许多初学者都会问“如何在Haskell中进行For循环?”之类的问题。只是试图提供帮助的无辜的人会给出一个不幸的答案,可能涉及辅助函数和额外的Int参数,并且尾部递归直到你达到0.当然,这个构造可以计算类似for循环的东西,但是没有方式是for循环,它不是for循环的替代品,如果考虑执行流程,它绝不会与for循环类似。类似的是状态monad模拟状态。它可以用来完成类似静态变量在其他语言中所做的事情,但绝不是一回事。当大多数人回答这些问题时,大多数人都忽略了最后一点,我认为这只会让人们更加困惑,直到他们自己意识到这一点。

3。 Haskell是一个逻辑引擎,而不是编程语言

这可能是我想要做的最不真实的一点,但是听我说。在命令式编程语言中,我们关注的是使我们的机器执行操作,执行操作,更改状态等等。在Haskell中,我们尝试定义事物是什么,以及它们应该如何表现。我们通常不关心在任何特定时间正在做什么。这肯定有利有弊,但事实就是这样。这与大多数人在谈到“编程语言”时的想法非常不同。

这就是我如何留下必要的思维模式并转向更具功能性的思维方式。实现Haskell的合理性将帮助您不再看待自己的代码。希望以这些方式思考Haskell将有助于您成为一个更高效的Haskeller。