如何从列表中获取第n个元素?


97

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

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

Answers:


154

这里,所用的运算符是!!

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


86
就我个人而言,我无法理解不返回Maybe类型的索引访问器如何作为惯用的Haskell可以接受的。[1,2,3]!!6会给您一个运行时错误。如果!!有类型,可以很容易地避免它[a] -> Int -> Maybe a。我们拥有Haskell的根本原因是为了避免此类运行时错误!
worldsayshi

9
这是一个权衡。他们选择的符号可能是他们可能拥有的最令人震惊的符号。因此,我认为该想法是允许在极端情况下使用,但使其脱颖而出。
cdosborn

3
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))。请注意,这将在无限列表上造成灾难性的失败。
djvs

2
!!是部分功能,因此不安全。看看下面的评论,并使用lens stackoverflow.com/a/23627631/2574719
goetzc

90

我并不是说您的问题或给出的答案有什么问题,但也许您想了解一下Hoogle这个很棒的工具,它可以在将来节省您的时间:使用Hoogle,您可以搜索标准库函数与给定签名相匹配。因此,!!如果您不了解,则可能会搜索“带有Int和的清单,并返回一个诸如此类的清单的清单”,即

Int -> [a] -> a

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

另外,如果您的代码依赖于索引(而不是从列表的开头使用),则列表实际上可能不是正确的数据结构。对于基于索引的O(1)访问,有更有效的替代方法,例如数组向量


4
Hoogle绝对很棒。每个Haskell程序员都应该知道这一点。还有一个替代方法,称为Hayoo(holumbus.fh-wedel.de/hayoo/hayoo.html)。它在您键入时进行搜索,但似乎不如Hoogle聪明。
musiKk'3

61

使用的替代方法(!!)是使用 镜头包装及其element功能和相关的操作员。该 透镜提供了用于访问各种超出列表结构和嵌套结构的一个统一的接口。在下文中,我将重点提供示例,并对镜头包装的类型签名和理论进行介绍 。如果您想了解更多有关该理论的知识,那么不错的起点是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


存取清单

使用infix运算符访问列表

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

(!!)this 不同,在超出范围访问元素时不会抛出异常,Nothing而是返回。通常建议避免使用诸如(!!)或之类的部分函数,head因为它们具有更多的极端情况,并且更有可能导致运行时错误。您可以在此Wiki页面上阅读更多有关为什么要避免使用部分功能的信息。

> [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


使用列表以外的类型

但是,这不仅限于列表。例如,相同的技术适用于标准 容器包装中的树木

 > 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

我们还可以从 容器包中访问序列

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

我们可以从vector包访问标准的int索引数组,从standard 文本包访问 文本,从 标准 bytestring包访问字节串,以及许多其他标准数据结构。通过使它们成为类型类Taversable的实例,可以将这种标准的访问方法扩展到您的个人数据结构,请参见Lens文档中的示例Traversables的更长列表


嵌套结构

用镜头劈开挖掘嵌套结构很简单。例如,访问列表列表中的元素:

> [[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]

镜头包给出与此功能(.~)操作。尽管与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只是功能而(&)操作员,是镜头包装的一部分 ,只是反向功能应用。这是更常见的功能应用程序。

> (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]]

3
我可以建议链接到Data.Traversable而不是再出口lens吗?
dfeuer

@dfeuer-我在基础中添加了指向Data.Traversable的链接。我还保留了旧的链接,并指出在Lens文档中有更多的示例traverable列表。谢谢你的建议。
Davorak 2015年

11

直接答案已经给出:Use !!

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


2
您还需要注意那里的类型。大多数情况下,您不想以索引为慢整数而不是快速机器整数来结束。根据函数的功能以及键入的明确程度,Haskell可能会将[0 ..]的类型推断为[Integer]而不是[Int]。
chrisdb 2011年

4

您可以使用!!,但是如果您想递归地使用它,那么下面是一种方法:

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

4

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

这些函数分别对负整数和非正整数永远递归。
Bjartur Thorlacius
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.