如何在Haskell中通过索引访问列表,类似于这个C代码?
int a[] = { 34, 45, 56 };
return a[1];
使用lens包及其element
函数和相关运算符是使用(!!)
的一种替代方法。 lens提供了一个统一的接口,用于访问各种结构和嵌套结构,超出了列表的范围。以下将重点提供示例,并略过lens包的类型签名和理论部分。如果您想了解更多关于理论的信息,可以在github存储库的自述文件中找到很好的起点。
在命令行上:
$ 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
这样的部分函数,因为它们有更多的边角情况,并且更容易导致运行时错误。您可以在这个维基页面上了解更多关于如何避免使用部分函数的信息。
> [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
这不仅仅适用于列表。例如,相同的技术可以用于标准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
我们也可以从containers包中访问序列:
> import qualified Data.Sequence as Seq
> Seq.fromList [1,2,3,4] ^? element 3
Just 4
我们可以从vector包中访问标准的int索引数组,从标准text包中访问文本,从标准bytestring包中访问字节串,以及许多其他标准数据结构。您可以通过将它们变成可遍历的类型类实例来扩展此标准访问方法,详见Lens文档中的更长示例列表。
使用lens 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
要求,您可以任意嵌套任意类型。 因此,访问树列表的文本序列毫不费力。
许多语言中的常见操作是在数组的索引位置上赋值。在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]
使用任意嵌套的 Traversable
,分配再次完美运行。
> [[1,2,3],[4,5,6]] & element 0 . element 1 .~ 9
[[1,9,3],[4,5,6]]
Data.Traversable
而不是在lens
中重新导出吗? - dfeuer!!
。zip ["foo","bar","baz"] [0..]
,你会得到一个新的列表,其中每个元素都与一个索引“附加”在一起形成一对:[("foo",0),("bar",1),("baz",2)]
,这通常正是你所需要的。!!
,但如果你想要递归地做到这一点,下面是一种方法:dataAt :: Int -> [a] -> a
dataAt _ [] = error "Empty List!"
dataAt y (x:xs) | y <= 0 = x
| otherwise = dataAt (y-1) xs
forall t. [t]
在实现上非常类似于经典的 C 链表,并且共享其基本属性。链表与数组非常不同。特别是,按索引访问是一种 O(n) 线性操作,而不是一种 O(1) 常量时间操作。Data.Array
。
!!
是一种不安全的部分定义函数,对于超出范围的索引会导致崩溃。请注意,标准库包含一些这样的部分函数(例如 head
、last
等)。为了安全起见,请使用一个选项类型 Maybe
或者 Safe
模块。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
在处理链表时,常常使用序数(ordinals)来方便地操作:
nth :: Int -> [a] -> Maybe a
nth _ [] = Nothing
nth 1 (x : _) = Just x
nth n (_ : xs) = nth (n - 1) xs
我知道这是一篇旧帖子...但它可能对某些人有用... 以"功能性"的方式...
import Data.List
safeIndex :: [a] -> Int -> Maybe a
safeIndex xs i
| (i> -1) && (length xs > i) = Just (xs!!i)
| otherwise = Nothing
!!
不同),并且由现有库(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
“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
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
[1,2,3]!!6
会导致运行时错误。如果!!
的类型是[a] -> Int -> Maybe a
,这种情况很容易避免。我们使用Haskell的一个非常重要的原因就是为了避免这种运行时错误! - worldsayshi!!
是一个部分函数,因此不安全。请查看下面的评论,并使用lens
库 https://dev59.com/Hm435IYBdhLWcg3wxDP_#23627631 - goetzMaybe
的原因。一些基本运算符作为部分函数表达更有用。当然,您应该以某种方式证明前提条件已满足。我同意,在Kotlin中像List.get
和List.getOrNull
那样同时拥有两个版本通常很有用。甚至可能会有一个额外的除法运算符,它可能返回DivideByZeroError
,这很酷。但是只有“安全”的那些...我不知道,我不喜欢那样。 - cubuspl42