【问题标题】:How can I get nth element from a list?如何从列表中获取第 n 个元素?
【发布时间】:2011-03-07 07:52:55
【问题描述】:

如何在 Haskell 中按索引访问列表,类似于此 C 代码?

int a[] = { 34, 45, 56 };
return a[1];

【问题讨论】:

    标签: haskell


    【解决方案1】:

    here,使用的运算符是!!

    [1,2,3]!!1 为您提供 2,因为列表是 0 索引的。

    【讨论】:

    • 就我个人而言,我无法理解不返回 Maybe 类型的索引访问器如何作为惯用的 Haskell 接受。 [1,2,3]!!6 会给你一个运行时错误。如果!! 的类型为[a] -> Int -> Maybe a,则很容易避免这种情况。我们拥有 Haskell 的真正原因是避免此类运行时错误!
    • 这是一个权衡。他们选择的符号可能是他们可能拥有的最令人震惊的符号。所以我认为这个想法是允许它用于边缘情况,但让它脱颖而出,因为它是非惯用的。
    • itemOf :: Int -> [a] -> Maybe a; x `itemOf` xs = let xslen = length xs in if ((abs x) > xslen) then Nothing else Just (xs !! (x `mod` xslen))。请注意,这将在无限列表上灾难性地失败。
    • !! 是部分函数,​​因此不安全。看看下面的评论并使用lensstackoverflow.com/a/23627631/2574719
    • @worldsayshi 除法运算符不返回Maybe 的原因相同。一些基本的运算符表示为偏函数很有用。当然,你应该以某种方式证明前提条件得到满足。我同意,both 版本通常会很有用,例如 Kotlin 中的 List.getList.getOrNull。即使是可能返回DivideByZeroError 的额外除法运算符也会很酷。但是只有“安全”的……我不知道,我不喜欢那样。
    【解决方案2】:

    我并不是说您的问题或给出的答案有什么问题,但也许您想了解 Hoogle 这个出色的工具,以节省您将来的时间:使用 Hoogle,您可以搜索与给定签名匹配的标准库函数。因此,对!! 一无所知,在您的情况下,您可能会搜索“采用Int 和任何东西的列表并返回一个这样的东西的东西”,即

    Int -> [a] -> a
    

    Lo 和behold!! 作为第一个结果(尽管与我们搜索的相比,类型签名实际上有两个参数相反)。整洁吧?

    此外,如果您的代码依赖于索引(而不是从列表的前面使用),列表实际上可能不是正确的数据结构。对于基于 O(1) 索引的访问,有更有效的替代方案,例如 arraysvectors

    【讨论】:

    • Hoogle 绝对很棒。每个 Haskell 程序员都应该知道它。有一个替代方案叫 Hayoo (holumbus.fh-wedel.de/hayoo/hayoo.html)。它会在您键入时进行搜索,但似乎不如 Hoogle 聪明。
    【解决方案3】:

    使用(!!) 的替代方法是使用 lens 包及其 element 函数和相关运算符。这 lens 提供了一个统一的接口,用于访问列表之外的各种结构和嵌套结构。下面我将专注于提供示例,并将掩盖类型签名和背后的理论 lens 包。如果您想了解更多有关该理论的信息,最好从github repo 处的自述文件开始。

    访问列表和其他数据类型

    获取镜头包

    在命令行:

    $ cabal install lens
    $ ghci
    GHCi, version 7.6.3: http://www.haskell.org/ghc/  :? for help
    Loading package ghc-prim ... linking ... done.
    Loading package integer-gmp ... linking ... done.
    Loading package base ... linking ... done.
    > import Control.Lens
    


    访问列表

    使用中缀运算符访问列表

    > [1,2,3,4,5] ^? element 2  -- 0 based indexing
    Just 3
    

    (!!) 不同,这在访问越界元素时不​​会引发异常,而是返回Nothing。通常建议避免使用像 (!!)head 这样的部分函数,​​因为它们有更多的极端情况并且更有可能导致运行时错误。您可以在 this wiki page 阅读更多关于为什么要避免使用偏函数的信息。

    > [1,2,3] !! 9
    *** Exception: Prelude.(!!): index too large
    
    > [1,2,3] ^? element 9
    Nothing
    

    您可以通过使用(^?!) 运算符而不是(^?) 运算符将镜头技术强制为部分函数并在超出范围时引发异常。

    > [1,2,3] ^?! element 1
    2
    > [1,2,3] ^?! element 9
    *** Exception: (^?!): empty Fold
    


    使用列表以外的类型

    然而,这不仅限于列表。例如,相同的技术适用于标准中的trees containers 包。

     > import Data.Tree
     > :{
     let
      tree = Node 1 [
           Node 2 [Node 4[], Node 5 []]
         , Node 3 [Node 6 [], Node 7 []]
         ]
     :}
    > putStrLn . drawTree . fmap show $tree
    1
    |
    +- 2
    |  |
    |  +- 4
    |  |
    |  `- 5
    |
    `- 3
       |
       +- 6
       |
       `- 7
    

    我们现在可以按深度优先顺序访问树的元素:

    > tree ^? element 0
    Just 1
    > tree ^? element 1
    Just 2
    > tree ^? element 2
    Just 4
    > tree ^? element 3
    Just 5
    > tree ^? element 4
    Just 3
    > tree ^? element 5
    Just 6
    > tree ^? element 6
    Just 7
    

    我们也可以通过以下方式访问sequences containers包:

    > import qualified Data.Sequence as Seq
    > Seq.fromList [1,2,3,4] ^? element 3
    Just 4
    

    我们可以从 vector 包,文字来自标准 text 包,来自标准的字节串 bytestring 包,以及许多其他标准数据结构。这种标准的访问方法可以扩展到您的个人数据结构,方法是让它们成为类型类 Taversable 的实例,请参阅更长的示例列表 Traversables in the Lens documentation.


    嵌套结构

    使用镜头hackage 可以轻松挖掘嵌套结构。例如访问列表列表中的元素:

    > [[1,2,3],[4,5,6]] ^? element 0 . element 1
    Just 2
    > [[1,2,3],[4,5,6]] ^? element 1 . element 2
    Just 6
    

    即使嵌套数据结构的类型不同,这种组合也有效。例如,如果我有一个树列表:

    > :{
     let
      tree = Node 1 [
           Node 2 []
         , Node 3 []
         ]
     :}
    > putStrLn . drawTree . fmap show $ tree
    1
    |
    +- 2
    |
    `- 3
    > :{
     let 
      listOfTrees = [ tree
          , fmap (*2) tree -- All tree elements times 2
          , fmap (*3) tree -- All tree elements times 3
          ]            
     :}
    
    > listOfTrees ^? element 1 . element 0
    Just 2
    > listOfTrees ^? element 1 . element 1
    Just 4
    

    只要满足Traversable 的要求,您就可以对任意类型进行任意深度嵌套。所以访问文本序列树的列表并不困难。


    更改第 n 个元素

    许多语言中的一个常见操作是分配给数组中的索引位置。在 python 中你可能会:

    >>> a = [1,2,3,4,5]
    >>> a[3] = 9
    >>> a
    [1, 2, 3, 9, 5]
    

    lens 包通过 (.~) 运算符提供此功能。虽然与 python 不同,原始列表没有发生变化,而是返回一个新列表。

    > let a = [1,2,3,4,5]
    > a & element 3 .~ 9
    [1,2,3,9,5]
    > a
    [1,2,3,4,5]
    

    element 3 .~ 9 只是一个函数,(&) 运算符是 lens 包,只是反向功能应用。这里是比较常见的函数应用。

    > (element 3 .~ 9) [1,2,3,4,5]
    [1,2,3,9,5]
    

    Traversables 的任意嵌套下,赋值再次完美运行。

    > [[1,2,3],[4,5,6]] & element 0 . element 1 .~ 9
    [[1,9,3],[4,5,6]]
    

    【讨论】:

    • 我可以建议链接到Data.Traversable,而不是在lens 中重新导出吗?
    • @dfeuer - 我在 base 中添加了指向 Data.Traversable 的链接。我还保留了旧链接,并指出 Lens 文档中有更长的示例可遍历列表。感谢您的建议。
    【解决方案4】:

    已经给出了直接的答案:使用!!

    然而新手经常倾向于过度使用这个运算符,这在 Haskell 中是昂贵的(因为你在单链表上工作,而不是在数组上)。有几种有用的技术可以避免这种情况,最简单的一种是使用 zip。如果你写zip ["foo","bar","baz"] [0..],你会得到一个新列表,其中索引“附加”到一对中的每个元素:[("foo",0),("bar",1),("baz",2)],这通常正是你所需要的。

    【讨论】:

    • 你还需要注意你的类型。大多数时候,您不希望索引是慢整数而不是快速机器整数。根据您的函数的具体功能以及您的输入的明确程度,Haskell 可能会推断 [0..] 的类型为 [Integer] 而不是 [Int]。
    【解决方案5】:

    你可以使用!!,但如果你想递归地做,下面是一种方法:

    dataAt :: Int -> [a] -> a
    dataAt _ [] = error "Empty List!"
    dataAt y (x:xs)  | y <= 0 = x
                     | otherwise = dataAt (y-1) xs
    

    【讨论】:

      【解决方案6】:

      Haskell 的标准列表数据类型forall t. [t] 在实现中非常类似于规范的 C 链表,并共享其本质属性。链表与数组非常不同。最值得注意的是,按索引访问是 O(n) 线性操作,而不是 O(1) 恒定时间操作。

      如果您需要频繁的随机访问,请考虑Data.Array 标准。

      !! 是一个不安全的部分定义函数,会导致超出范围的索引崩溃。请注意,标准库包含一些这样的部分函数(headlast 等)。为安全起见,请使用选项类型 MaybeSafe 模块。

      合理高效、稳健的总计(对于索引 ≥ 0)索引函数的示例:

      data Maybe a = Nothing | Just a
      
      lookup :: Int -> [a] -> Maybe a
      lookup _ []       = Nothing
      lookup 0 (x : _)  = Just x
      lookup i (_ : xs) = lookup (i - 1) xs
      

      使用链表,通常序数很方便:

      nth :: Int -> [a] -> Maybe a
      nth _ []       = Nothing
      nth 1 (x : _)  = Just x
      nth n (_ : xs) = nth (n - 1) xs
      

      【讨论】:

      • 这些函数分别对负整数和非正整数永远递归。
      【解决方案7】:

      我知道这是一篇旧帖子......但它可能对某人有用...... 以“功能性”的方式...

      import Data.List
      
      safeIndex :: [a] -> Int -> Maybe a
      safeIndex xs i 
              | (i> -1) && (length xs > i) = Just (xs!!i)
              | otherwise = Nothing
      

      【讨论】:

        【解决方案8】:

        “Maybe-way”是一种合理的方法。

        只是想出一个替代方案,您需要获得一个可以事先确定的默认值。

        atDefault :: a -> Integer -> [a] -> a
        atDefault aDef _ [] = aDef                        -- case: is empty anyway
        atDefault _ 0  (a:_) = a                          -- case: index is 0 -> take it
        atDefault aDef nIndex  (a:la) 
            | nIndex > 0 = atDefault aDef (nIndex - 1) la -- case: index is positive
            | otherwise  = aDef                           -- case: index is negative
        

        一个用例可能如下:

        您想通过使用八位位组列表来表示无穷无尽的数字集。假设列表总是有一组给定的 n 个元素,假设 n > 0 - 但元素也可能在您通常认为它超出索引范围(索引 >= n 或索引

        例子:

        module Main where
        
        import qualified Data.Word as W
        import qualified Data.Bits as Bts
        import Data.Bits ((.|.))
        import qualified Data.List as L
        
        main :: IO ()
        main = do
            print $ atDefault 0x00 (-1) myOctet
            print $ atDefault 0x00 0 myOctet
            print $ atDefault 0x00 1 myOctet
            print $ atDefault 0x00 2 myOctet
            print $ atDefault 0x00 3 myOctet
            print $ atDefault 0x00 4 myOctet
        
        myOctet = toOctets (0xA4B3C2D1 :: W.Word32)
        
        atDefault :: a -> Integer -> [a] -> a
        atDefault aDef _ [] = aDef                        -- case: is empty anyway
        atDefault _ 0  (a:_) = a                          -- case: index is 0 -> take it
        atDefault aDef nIndex  (a:la) 
            | nIndex > 0 = atDefault aDef (nIndex - 1) la -- case: index is positive
            | otherwise  = aDef                           -- case: index is negative
        
        class Octetable w where
            toOctets :: w -> [W.Word8]
        
        instance Octetable W.Word32 where
            toOctets w32 = 
                [ fromIntegral (w32 `Bts.shiftR` 24)
                , fromIntegral (w32 `Bts.shiftR` 16)
                , fromIntegral (w32 `Bts.shiftR` 8)
                , fromIntegral w32
                ]
        

        输出

        0
        164
        179
        194
        209
        0
        

        【讨论】:

          【解决方案9】:

          另一个选项是使用genericIndex :: Integral i => [a] -> i -> a 函数,它在索引类型参数中的限制比(!!) :: [a] -> Int -> a 的限制要小。在某些情况下可以派上用场。

          【讨论】:

            【解决方案10】:

            以下函数似乎很理想,因为它不是部分的(与 !! 不同),并且由现有库实现(relude):

            (!!?) :: [a] -> Int -> Maybe a
            

            https://hackage.haskell.org/package/relude-1.0.0.1/docs/Relude-List.html#v:-33--33--63-

            Prelude> import Relude.List
            Prelude Relude.List> ['a'..'f'] !!? 0
            Just 'a'
            Prelude Relude.List> ['a'..'f'] !!? 100
            Nothing
            

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 2015-02-10
              • 1970-01-01
              • 2011-03-19
              • 2019-09-05
              • 2023-03-08
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多