【发布时间】:2011-03-07 07:52:55
【问题描述】:
如何在 Haskell 中按索引访问列表,类似于此 C 代码?
int a[] = { 34, 45, 56 };
return a[1];
【问题讨论】:
标签: haskell
如何在 Haskell 中按索引访问列表,类似于此 C 代码?
int a[] = { 34, 45, 56 };
return a[1];
【问题讨论】:
标签: haskell
看here,使用的运算符是!!。
即[1,2,3]!!1 为您提供 2,因为列表是 0 索引的。
【讨论】:
[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
Maybe 的原因相同。一些基本的运算符表示为偏函数很有用。当然,你应该以某种方式证明前提条件得到满足。我同意,both 版本通常会很有用,例如 Kotlin 中的 List.get 和 List.getOrNull。即使是可能返回DivideByZeroError 的额外除法运算符也会很酷。但是只有“安全”的……我不知道,我不喜欢那样。
我并不是说您的问题或给出的答案有什么问题,但也许您想了解 Hoogle 这个出色的工具,以节省您将来的时间:使用 Hoogle,您可以搜索与给定签名匹配的标准库函数。因此,对!! 一无所知,在您的情况下,您可能会搜索“采用Int 和任何东西的列表并返回一个这样的东西的东西”,即
Int -> [a] -> a
Lo 和behold,!! 作为第一个结果(尽管与我们搜索的相比,类型签名实际上有两个参数相反)。整洁吧?
此外,如果您的代码依赖于索引(而不是从列表的前面使用),列表实际上可能不是正确的数据结构。对于基于 O(1) 索引的访问,有更有效的替代方案,例如 arrays 或 vectors。
【讨论】:
使用(!!) 的替代方法是使用
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 的要求,您就可以对任意类型进行任意深度嵌套。所以访问文本序列树的列表并不困难。
许多语言中的一个常见操作是分配给数组中的索引位置。在 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 中重新导出吗?
已经给出了直接的答案:使用!!。
然而新手经常倾向于过度使用这个运算符,这在 Haskell 中是昂贵的(因为你在单链表上工作,而不是在数组上)。有几种有用的技术可以避免这种情况,最简单的一种是使用 zip。如果你写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
【讨论】:
Haskell 的标准列表数据类型forall t. [t] 在实现中非常类似于规范的 C 链表,并共享其本质属性。链表与数组非常不同。最值得注意的是,按索引访问是 O(n) 线性操作,而不是 O(1) 恒定时间操作。
如果您需要频繁的随机访问,请考虑Data.Array 标准。
!! 是一个不安全的部分定义函数,会导致超出范围的索引崩溃。请注意,标准库包含一些这样的部分函数(head、last 等)。为安全起见,请使用选项类型 Maybe 或 Safe 模块。
合理高效、稳健的总计(对于索引 ≥ 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
【讨论】:
我知道这是一篇旧帖子......但它可能对某人有用...... 以“功能性”的方式...
import Data.List
safeIndex :: [a] -> Int -> Maybe a
safeIndex xs i
| (i> -1) && (length xs > i) = Just (xs!!i)
| otherwise = 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
一个用例可能如下:
您想通过使用八位位组列表来表示无穷无尽的数字集。假设列表总是有一组给定的 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
【讨论】:
另一个选项是使用genericIndex :: Integral i => [a] -> i -> a 函数,它在索引类型参数中的限制比(!!) :: [a] -> Int -> a 的限制要小。在某些情况下可以派上用场。
【讨论】:
以下函数似乎很理想,因为它不是部分的(与 !! 不同),并且由现有库实现(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
【讨论】: