【问题标题】:How to write a Haskell function that determine if a number is even or odd?如何编写一个判断一个数是偶数还是奇数的 Haskell 函数?
【发布时间】:2021-02-09 02:47:13
【问题描述】:

我正在尝试理解 haskell 语言,但我对它很困惑,因为我有以下练习,甚至无法开始:

  1. evenNat :: Integer -> Bool,它将一个非负整数作为输入,并且 确定它是否是偶数。

(还有更多,但现在让我们专注于这一点)

在所有这些程序中,您都不能使用内置的乘法、除法或模函数。

我应该如何开始,你能给我一些如何学习这门语言的建议吗?谢谢!

【问题讨论】:

  • 我假设你应该使用递归。例如,对于evenNat,定义evenNat 0,然后对于任意(正)输入递归一个较小的数字。这基本上告诉你如何做第一个,我将把其他的留给你:)
  • 如果您可以访问某个名为substractLargestPossiblePowerOf2 的函数,您知道如何使用递归解决evenNat 吗?你读过this chapter 或类似的东西吗?
  • 愚蠢的问题:是否禁止按位运算?因为如果不是,evenNat 可以使用x & 0x1 == 0(或 Haskell 用于按位运算的任何语法)来实现。而其他的则以evenNat 的形式简单实现。

标签: function haskell integer boolean


【解决方案1】:

一个整数是偶数当且仅当它的低位是 0。它是奇数当且仅当它的低位是 1。您可以使用Data.Bits 中的一个简单操作来测试这一点。

import Data.Bits
import Prelude hiding (even, odd)

even, odd :: Integer -> Bool
even x = ??
odd x = ??

也就是说,我认为 Robin Zigmond 对本练习的意图可能是正确的。要更直接地了解该意图,您可以定义自己的数字。

import Prelude hiding (Integer, even, odd)

data PNat
  = One
  | Succ PNat

data Integer
  = Negative PNat
  | Zero
  | Positive PNat

现在定义

evenNat :: PNat -> Bool

等等。

【讨论】:

    【解决方案2】:
    1. evenNat :: Integer -> Bool,以非负整数为输入,判断是否为偶数。

    只要写下你知道的:

    evenNat :: Integer -> Bool
    evenNat n | n < 0 = error "forbidden call to evenNat"
    evenNat 0 = True
    evenNat 1 = False
    evenNat 2 = True
    evenNat 3 = .....
    .....
    

    这是一个非常好的开始,但我们不希望明确列举所有必要的情况,以涵盖所有可能的输入。

    但是让我们停下来。我们将如何完成evenNat 3 = ... 的定义?我们当然可以把明确的答案放在那里,False。但我们也可以注意到False == not True 和True == evenNat 2,所以

    evenNat 3 = False 
              = not True
              = not (evenNat 2)
              = not (evenNat (3 - 1))
    

    事实上,

    evenNat 2 = True 
              = not False
              = not (evenNat 1)
              = not (evenNat (2 - 1))
    

    完全遵循相同的模式。

    你有它!剩下的就是,泛化!通过将具体的数据转化为变量,这样我们就可以得到一个更多的——最终的——子句,

    evenNat n = not (...... ( ... - ... ))
    

    就是这样。

    【讨论】:

      【解决方案3】:

      对效率的适度尝试:

      让我们接受 Robin 的前提,即递归是这里的实际主题。

      递归就是用一个更简单的问题,或者用一个更简单的问题列表替换一个相对困难的问题。

      例如,evenNat 43 是evenNat 42 的否定。如果我们沿着这条路线走,经过 40 多步后,我们会到达 evenNat 0,也就是众所周知的 True。该方案有效,但对于大量数据可能会有点慢。

      一种更快的方法是在每一步从手头的数字中减去 2 的最大可能幂。这样的减法不会改变数字的偶数字符。所以 43 是偶数当且仅当 (43-32) = 11 是偶数。接下来,我们考虑 (11-8) = 3。所以它看起来肯定是一种更快但更复杂的方法。

      首先,我们需要 2 的幂的序列,直到手头的数字。当然,这可以递归地完成:

      powersOf2UpToN :: Integer -> [Integer]
      powersOf2UpToN n =
          let  -- defining auxiliary function sq:
               sq m pwrs = if (head pwrs > m)    -- if reached big enough
                               then  (tail pwrs) -- then stop and return result
                               else -- augment:
                                    let  hps = head pwrs  in  sq m ((hps+hps):pwrs)
          in  (sq n [2])
      

      请注意,我使用的是(hps+hps) 而不是(2*hps),因为练习规则禁止本地乘法。

      基本上,只要我们保持在手头的数字以下,我们就会在幂序列中添加越来越大的 2 幂。回想一下在 Haskell 中,将元素添加到列表中是一种廉价的操作。

      在ghci解释器下测试:

      $ ghci
       λ> 
       λ> :load q64544005.hs
       Ok, one module loaded.
       λ> 
       λ> powersOf2UpToN 337
       [256,128,64,32,16,8,4,2]
       λ> 
      

      接下来,我们必须实现减去 2 的最大可能幂的函数。这样的函数将采用 (number, powers of 2) 对,并返回一个“改进的”对,即具有较小的数字和缩短的功率列表。类型签名如下:

      reduce2 :: (Integer, [Integer]) -> (Integer, [Integer])`
      

      整体逻辑是:

      reduce2 (n, pwrs) =
          if (null pwrs)     -- did we just run out of powers of 2 ?
              then  (n, [])  -- nothing left to do
              else  if ((head pwrs) <= n)  -- return a lower n if possible
                    ...
      

      请在此处暂停阅读,看看是否可以填空。


      解决方案:如果可能,我们减去 2 的最大剩余幂,它恰好位于(递减)列表的头部。

      reduce2 (n, pwrs) =
          if (null pwrs)
              then  (n, [])  -- nothing left to do
              else  if ((head pwrs) <= n)  -- return a lower n if possible
                        then  reduce2  ((n - (head pwrs)), tail pwrs)
                        else  reduce2  (n, tail pwrs)
      

      reduce2 的机制是关键。因此,为了帮助理解,让我们利用 Haskell 方便的小型跟踪工具。

      在下面的代码中,表达式DT.trace msg res 的计算结果仅为res,并具有打印msg 的副作用。是的,Haskell 中通常禁止副作用,但跟踪工具享有特殊特权。

      所以这是我们reduce2函数的跟踪版本:

      import qualified  Debug.Trace  as  DT
      type DebugMsg = String
      
      traceReduce2 :: DebugMsg -> (Integer, [Integer]) -> (Integer, [Integer])
      traceReduce2 msg (n, pwrs) =
          let  res = if (null pwrs)
                         then  (n, [])
                         else  let  msg1 = (show n) ++ "  " ++ (show pwrs)
                                    rem  = n - (head pwrs)
                               in   if ((head pwrs) <= n)
                                        then  traceReduce2 msg1 (rem, tail pwrs)
                                        else  traceReduce2 msg1 (n,   tail pwrs)
          in  DT.trace msg res
      
      

      测试我们的跟踪版本:

       λ> 
       λ> traceReduce2 "TOP" (337, (powersOf2UpToN 337))
       TOP
       337  [256,128,64,32,16,8,4,2]
       81  [128,64,32,16,8,4,2]
       81  [64,32,16,8,4,2]
       17  [32,16,8,4,2]
       17  [16,8,4,2]
       1  [8,4,2]
       1  [4,2]
       1  [2]
       (1,[])
       λ> 
      
      
      

      所以我们差不多完成了。在处理完 2 的所有幂之后,0 或 1 的值保留为该对的左侧。我们可以继续编写我们的evenNat 函数:

      evenNat :: Integer -> Bool
      evenNat n = 
          let  (remainder, ys) = reduce2 (n, powersOf2UpToN n)
          in
               -- here, remainder is set to 0 if and only if n is even
               ...
      
      

      其余的应该很容易。

      【讨论】:

      • 我会在一些 RL 内容后不久通读这篇文章,但这是个好主意。我也有这个想法——连续 2 个nots 与 4 个相同,等等,所以,以某种方式跳过它,每次呼叫/尝试逐渐加倍距离......跨度>
      • 但显然你的方式也行得通。 :)(您可以使用模式匹配而不是显式的头尾来获得更清晰的代码,但这只是细节......)
      • @WillNess - 感谢您的建议。此外,也有可能将半数保持在进位状态内。从功率列表中弹出 512,将行中的下一个功率 256 添加到半数。所以我们可以使用相同的想法来获得一些 splitNat 函数,它将 21 映射到 (1,10)。如果我们想扩展这个简洁的练习,它可以用来实现基于递归加法的乘法。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-09-06
      • 2015-05-31
      • 1970-01-01
      • 1970-01-01
      • 2014-07-26
      • 1970-01-01
      相关资源
      最近更新 更多