Làm cách nào để lấy phần tử thứ n từ danh sách?


97

Làm cách nào để truy cập danh sách theo chỉ mục trong Haskell, tương tự như mã C này?

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

Câu trả lời:


154

Nhìn ở đây , toán tử được sử dụng là !!.

Tức là [1,2,3]!!1cung cấp cho bạn 2, vì danh sách được lập chỉ mục 0.


86
Cá nhân tôi không thể hiểu làm thế nào một trình truy cập tại chỉ mục không trả về loại Có thể được chấp nhận như Haskell thành ngữ. [1,2,3]!!6sẽ cung cấp cho bạn một lỗi thời gian chạy. Nó có thể rất dễ dàng tránh được nếu !!có loại [a] -> Int -> Maybe a. Lý do chính chúng tôi có Haskell là để tránh các lỗi thời gian chạy như vậy!
worldayshi

9
Đó là một sự đánh đổi. Biểu tượng họ chọn có lẽ là biểu tượng đáng báo động nhất mà họ có thể có. Vì vậy, tôi nghĩ ý tưởng là để cho phép nó cho các trường hợp phức tạp, nhưng làm cho nó nổi bật như không phải thành ngữ.
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)). Lưu ý, điều này sẽ thất bại thảm hại trong danh sách vô hạn.
djv

2
!!là một chức năng một phần và do đó không an toàn. Hãy nhìn vào những nhận xét dưới đây và sử dụng lens stackoverflow.com/a/23627631/2574719
goetzc

90

Tôi không nói rằng có điều gì sai với câu hỏi của bạn hoặc câu trả lời được đưa ra, nhưng có thể bạn muốn biết về công cụ tuyệt vời là Hoogle để tiết kiệm thời gian cho chính bạn trong tương lai: Với Hoogle, bạn có thể tìm kiếm các chức năng thư viện tiêu chuẩn phù hợp với một chữ ký nhất định. Vì vậy, không biết bất cứ điều gì !!, trong trường hợp của bạn, bạn có thể tìm kiếm "thứ gì đó lấy một Intvà một danh sách các whatevers và trả về một cái gì đó như vậy bất cứ thứ gì", cụ thể là

Int -> [a] -> a

Lo và kìa , với !!kết quả đầu tiên (mặc dù chữ ký kiểu thực sự có hai đối số ngược lại so với những gì chúng tôi đã tìm kiếm). Gọn gàng hả?

Ngoài ra, nếu mã của bạn dựa vào lập chỉ mục (thay vì sử dụng từ phía trước danh sách), thực tế danh sách có thể không phải là cấu trúc dữ liệu thích hợp. Đối với truy cập dựa trên chỉ mục O (1), có nhiều lựa chọn thay thế hiệu quả hơn, chẳng hạn như mảng hoặc vectơ .


4
Hoogle hoàn toàn tuyệt vời. Mọi lập trình viên Haskell nên biết điều đó. Có một giải pháp thay thế được gọi là Hayoo ( holumbus.fh-wedel.de/hayoo/hayoo.html ). Nó tìm kiếm khi bạn nhập nhưng có vẻ không thông minh như Hoogle.
musiKk

61

Một thay thế cho việc sử dụng (!!)là sử dụng gói ống kínhelementchức năng của nó và các nhà khai thác liên quan. Các ống kính cung cấp một giao diện thống nhất để truy cập vào một loạt các cấu trúc và cấu trúc lồng nhau trên và vượt ra ngoài danh sách. Dưới đây tôi sẽ tập trung vào việc cung cấp các ví dụ và sẽ giải thích về cả chữ ký loại và lý thuyết đằng sau gói ống kính . Nếu bạn muốn biết thêm về lý thuyết, một nơi tốt để bắt đầu là tệp readme tại github repo .

Truy cập danh sách và các kiểu dữ liệu khác

Tiếp cận với gói ống kính

Tại dòng lệnh:

$ 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


Truy cập danh sách

Để truy cập danh sách bằng toán tử infix

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

Không giống như (!!)điều này sẽ không đưa ra một ngoại lệ khi truy cập một phần tử ngoài giới hạn và Nothingthay vào đó sẽ trả về . Bạn thường nên tránh các chức năng từng phần như (!!)hoặc headvì chúng có nhiều trường hợp góc hơn và có nhiều khả năng gây ra lỗi thời gian chạy hơn. Bạn có thể đọc thêm một chút về lý do tại sao phải tránh các chức năng một phần tại trang wiki này .

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

> [1,2,3] ^? element 9
Nothing

Bạn có thể buộc kỹ thuật thấu kính là một chức năng riêng phần và đưa ra một ngoại lệ khi vượt ra ngoài giới hạn bằng cách sử dụng (^?!)toán tử thay vì (^?)toán tử.

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


Làm việc với các loại khác với danh sách

Tuy nhiên, điều này không chỉ giới hạn trong danh sách. Ví dụ, kỹ thuật tương tự hoạt động trên cây từ gói container tiêu chuẩn .

 > 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

Bây giờ chúng ta có thể truy cập các phần tử của cây theo thứ tự sâu nhất:

> 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

Chúng tôi cũng có thể truy cập các chuỗi từ gói vùng chứa :

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

Chúng ta có thể truy cập các mảng được lập chỉ mục int tiêu chuẩn từ gói vector , văn bản từ gói văn bản tiêu chuẩn , bytestrings từ gói bytestring tiêu chuẩn và nhiều cấu trúc dữ liệu tiêu chuẩn khác. Phương pháp truy cập tiêu chuẩn này có thể được mở rộng cho các cấu trúc dữ liệu cá nhân của bạn bằng cách biến chúng thành một phiên bản của Kiểu có thể lật ngang , hãy xem danh sách dài hơn về các cấu trúc Có thể chuyển ngang mẫu trong tài liệu Ống kính. .


Cấu trúc lồng nhau

Việc đào sâu vào các cấu trúc lồng nhau rất đơn giản với ống kính hack . Ví dụ truy cập một phần tử trong danh sách các danh sách:

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

Thành phần này hoạt động ngay cả khi cấu trúc dữ liệu lồng nhau thuộc các kiểu khác nhau. Ví dụ: nếu tôi có một danh sách các cây:

> :{
 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

Bạn có thể lồng sâu tùy ý với các loại tùy ý miễn là đáp ứng được Traversable yêu cầu. Vì vậy, việc truy cập một danh sách các cây gồm các chuỗi văn bản là không tốn kém.


Thay đổi phần tử thứ n

Một thao tác phổ biến trong nhiều ngôn ngữ là gán cho một vị trí được lập chỉ mục trong một mảng. Trong python, bạn có thể:

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

Các ống kính gói cho chức năng này với các (.~)nhà điều hành. Mặc dù không giống như trong python, danh sách ban đầu không bị đột biến, thay vào đó, một danh sách mới được trả về.

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

element 3 .~ 9chỉ là một chức năng và người (&)vận hành, một phần của ống kính gói , chỉ là ứng dụng chức năng đảo ngược. Đây là ứng dụng chức năng phổ biến hơn.

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

Phép gán lại hoạt động hoàn toàn tốt với việc lồng các Traversables tùy ý .

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

3
Tôi có thể đề xuất liên kết đến Data.Traversablethay vì tái xuất vào lenskhông?
dfeuer

@dfeuer - Tôi đã thêm một liên kết đến Data.Traversable trong cơ sở. Tôi cũng giữ liên kết cũ và chỉ ra rằng có một danh sách dài hơn về các thiết bị kéo ví dụ trong tài liệu về Ống kính. Cám ơn vì sự gợi ý.
Davorak

11

Câu trả lời thẳng thắn đã được đưa ra: Sử dụng !! .

Tuy nhiên, người mới thường có xu hướng lạm dụng toán tử này, toán tử này đắt tiền trong Haskell (vì bạn làm việc trên danh sách liên kết đơn, không phải trên mảng). Có một số kỹ thuật hữu ích để tránh điều này, cách dễ nhất là sử dụng zip. Nếu bạn viết zip ["foo","bar","baz"] [0..], bạn sẽ nhận được một danh sách mới với các chỉ số được "đính kèm" cho mỗi phần tử trong một cặp : [("foo",0),("bar",1),("baz",2)], thường chính xác là những gì bạn cần.


2
Bạn cũng cần phải cẩn thận về các loại của bạn ở đó. Hầu hết thời gian bạn không muốn kết thúc với các chỉ số là Số nguyên chậm thay vì Kiến máy nhanh. Tùy thuộc vào chức năng chính xác của bạn làm gì và cách nhập của bạn rõ ràng như thế nào, Haskell có thể suy ra loại [0 ..] là [Integer] thay vì [Int].
chrisdb

4

Bạn có thể sử dụng !!, nhưng nếu bạn muốn làm điều đó một cách đệ quy thì dưới đây là một cách để làm điều đó:

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

4

Kiểu dữ liệu danh sách chuẩn của Haskell forall t. [t] triển khai gần giống với danh sách liên kết C chuẩn và chia sẻ các thuộc tính cơ bản của nó. Danh sách được liên kết rất khác với mảng. Đáng chú ý nhất, truy cập theo chỉ mục là một O (n) tuyến tính-, thay vì một O (1) hoạt động thời gian không đổi.

Nếu bạn yêu cầu truy cập ngẫu nhiên thường xuyên, hãy xem xét Data.Arraytiêu chuẩn.

!!là một chức năng được xác định một phần không an toàn, gây ra sự cố cho các chỉ số nằm ngoài phạm vi. Hãy nhận biết rằng các thư viện chuẩn chứa một số chức năng một phần như vậy ( head, last, vv). Để đảm bảo an toàn, hãy sử dụng một loại tùy chọn MaybehoặcSafe mô-đun.

Ví dụ về hàm lập chỉ mục tổng hiệu quả, mạnh mẽ (đối với chỉ số ≥ 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

Làm việc với danh sách được liên kết, thứ tự thường rất thuận tiện:

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

Các hàm này lặp lại mãi mãi cho các Kiến âm và không dương tương ứng.
Bjartur Thorlacius
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.