Come posso ottenere l'ennesimo elemento da una lista?


97

Come posso accedere a un elenco per indice in Haskell, analogo a questo codice C?

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

Risposte:


154

Guarda qui , l'operatore utilizzato è !!.

Cioè [1,2,3]!!1ti dà 2, poiché le liste sono indicizzate 0.


86
Personalmente non riesco a capire come una funzione di accesso all'indice che non restituisce un tipo Forse sia accettabile come idiomatica Haskell. [1,2,3]!!6ti darà un errore di runtime. Potrebbe essere facilmente evitato se !!avesse il tipo [a] -> Int -> Maybe a. Il vero motivo per cui abbiamo Haskell è evitare tali errori di runtime!
worldsayshi

9
È un compromesso. Il simbolo che hanno scelto è probabilmente il simbolo più allarmante che potrebbero avere. Quindi penso che l'idea fosse di permetterlo per casi limite, ma farlo risaltare come non idiomatico.
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)). Nota, questo fallirà catastroficamente su un elenco infinito.
djvs

2
!!è una funzione parziale e quindi non sicura. Date un'occhiata al commento qui sotto e l'uso lens stackoverflow.com/a/23627631/2574719
goetzc

90

Non sto dicendo che ci sia qualcosa di sbagliato nella tua domanda o nella risposta data, ma forse ti piacerebbe conoscere il meraviglioso strumento che è Hoogle per risparmiare tempo in futuro: con Hoogle, puoi cercare le funzioni di libreria standard che corrispondono a una data firma. Quindi, non sapendo nulla in merito !!, nel tuo caso potresti cercare "qualcosa che prende un Inte un elenco di qualsiasi cosa e restituisce una singola cosa del genere", vale a dire

Int -> [a] -> a

Ed ecco , con !!come primo risultato (sebbene la firma del tipo abbia effettivamente i due argomenti al contrario rispetto a quello che abbiamo cercato). Perfetto, eh?

Inoltre, se il codice si basa sull'indicizzazione (invece di consumare dall'inizio dell'elenco), gli elenchi potrebbero in realtà non essere la struttura dati corretta. Per l'accesso basato su indice O (1) sono disponibili alternative più efficienti, come gli array o vettori .


4
Hoogle è assolutamente fantastico. Ogni programmatore Haskell dovrebbe saperlo. C'è un'alternativa chiamata Hayoo ( holumbus.fh-wedel.de/hayoo/hayoo.html ). Cerca durante la digitazione ma non sembra essere intelligente come Hoogle.
musiKk

61

Un'alternativa all'uso (!!)è usare il pacchetto lenti e la sua elementfunzione e gli operatori associati. L' obiettivo fornisce un'interfaccia uniforme per accedere a un'ampia varietà di strutture e strutture nidificate al di sopra e al di là degli elenchi. Di seguito mi concentrerò sulla fornitura di esempi e sorvolerò sia sulle firme del tipo che sulla teoria alla base del pacchetto dell'obiettivo . Se vuoi saperne di più sulla teoria, un buon punto di partenza è il file readme in repository github .

Accesso a elenchi e altri tipi di dati

Accesso al pacchetto dell'obiettivo

Alla riga di comando:

$ 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


Accesso agli elenchi

Per accedere a una lista con l'operatore infisso

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

A differenza di (!!)questo non genererà un'eccezione quando si accede a un elemento fuori dai limiti e restituirà Nothinginvece. Si consiglia spesso di evitare funzioni parziali come (!!)o headpoiché hanno più casi d'angolo e hanno maggiori probabilità di causare un errore di runtime. Puoi leggere un po 'di più sul perché evitare le funzioni parziali in questa pagina wiki .

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

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

È possibile forzare la tecnica dell'obiettivo in modo che sia una funzione parziale e generare un'eccezione quando fuori dai limiti utilizzando l' (^?!)operatore invece (^?)dell'operatore.

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


Lavorare con tipi diversi dalle liste

Tuttavia, questo non è limitato solo agli elenchi. Ad esempio, la stessa tecnica funziona sugli alberi del pacchetto contenitori standard .

 > 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

Ora possiamo accedere agli elementi dell'albero in profondità al primo ordine:

> 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

Possiamo anche accesso sequenze dei contenitori pacchetto:

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

Possiamo accedere agli array indicizzati int standard dal pacchetto vettoriale , al testo dal pacchetto di testo standard , a bytestrings dal pacchetto standard bytestring ea molte altre strutture dati standard. Questo metodo di accesso standard può essere esteso alle strutture dei dati personali rendendole un'istanza della classe di tipo Taversable , vedere un elenco più lungo di Traversables di esempio nella documentazione di Lens. .


Strutture annidate

Scavare in strutture annidate è semplice con l' hackage dell'obiettivo . Ad esempio l'accesso a un elemento in un elenco di elenchi:

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

Questa composizione funziona anche quando le strutture dati nidificate sono di tipi diversi. Quindi, ad esempio, se avessi un elenco di alberi:

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

È possibile nidificare arbitrariamente in profondità con tipi arbitrari purché soddisfino i Traversablerequisiti. Quindi accedere a un elenco di alberi di sequenze di testo non è un problema.


Modifica dell'ennesimo elemento

Un'operazione comune in molte lingue consiste nell'assegnare a una posizione indicizzata in un array. In Python potresti:

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

Il pacchetto di lenti offre questa funzionalità (.~)all'operatore. Sebbene a differenza di Python l'elenco originale non sia stato modificato, viene invece restituito un nuovo elenco.

> 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è solo una funzione e l' (&)operatore, parte del pacchetto dell'obiettivo , è solo un'applicazione di funzione inversa. Eccolo con l'applicazione delle funzioni più comune.

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

L'assegnazione funziona di nuovo perfettamente con l'annidamento arbitrario di Traversables.

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

3
Posso suggerire di collegarmi a Data.Traversablepiuttosto che riesportare in lens?
dfeuer

@dfeuer - Ho aggiunto un collegamento a Data.Traversable in base. Ho anche mantenuto il vecchio collegamento e ho sottolineato che c'era un elenco più lungo di esempi di traverables nella documentazione di Lens. Grazie per il suggerimento.
Davorak

11

La risposta diretta era già stata data: Usa !! .

Tuttavia, i neofiti spesso tendono a fare un uso eccessivo di questo operatore, che è costoso in Haskell (perché lavori su elenchi concatenati singoli, non su array). Esistono diverse tecniche utili per evitarlo, la più semplice è usare zip. Se scrivi zip ["foo","bar","baz"] [0..], ottieni una nuova lista con gli indici "attaccati" ad ogni elemento in una coppia:, [("foo",0),("bar",1),("baz",2)]che spesso è esattamente quello che ti serve.


2
Devi anche stare attento ai tuoi tipi lì. La maggior parte delle volte non vuoi finire con gli indici che sono interi lenti piuttosto che Ints macchina veloce. A seconda di quello che fa esattamente la tua funzione e di quanto sia esplicita la tua digitazione, Haskell potrebbe dedurre che il tipo di [0 ..] sia [Integer] invece di [Int].
chrisdb

4

Puoi usarlo !!, ma se vuoi farlo in modo ricorsivo, di seguito è riportato un modo per farlo:

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

4

Tipo di dati elenco standard di Haskell forall t. [t] nell'implementazione assomiglia molto a un elenco collegato C canonico e ne condivide essenzialmente le proprietà. Gli elenchi collegati sono molto diversi dagli array. In particolare, l'accesso per indice è un'operazione lineare O (n), invece di un'operazione a tempo costante O (1).

Se hai bisogno di un accesso casuale frequente, considera lo Data.Arraystandard.

!!è una funzione parzialmente definita non sicura, che provoca un arresto anomalo per indici fuori intervallo. Essere consapevoli del fatto che la libreria standard contiene alcune tali funzioni parziali ( head, last, ecc). Per sicurezza, utilizzare un tipo di opzione Maybeo il fileSafe modulo.

Esempio di una funzione di indicizzazione del totale (per indici ≥ 0) ragionevolmente efficiente e robusta:

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

Lavorando con elenchi collegati, spesso gli ordinali sono convenienti:

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

Queste funzioni ricorrono all'infinito rispettivamente per Ints negativo e non positivo.
Bjartur Thorlacius
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.