¿Cómo puedo obtener el n-ésimo elemento de una lista?

97

¿Cómo puedo acceder a una lista por índice en Haskell, análogo a este código C?

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

Respuestas:

154

Mire aquí , el operador utilizado es !!.

Es decir , [1,2,3]!!1te da 2, ya que las listas están indexadas en 0.

phimuemue
fuente
86
Personalmente, no puedo comprender cómo un descriptor de acceso en el índice que no devuelve un tipo Maybe es aceptable como Haskell idiomático. [1,2,3]!!6le dará un error de tiempo de ejecución. Podría evitarse muy fácilmente si !!tuviera el tipo [a] -> Int -> Maybe a. ¡La verdadera razón por la que tenemos Haskell es para evitar tales errores de tiempo de ejecución!
worldsayshi
9
Es una compensación. El símbolo que eligieron es probablemente el símbolo más alarmante que podrían tener. Entonces creo que la idea era permitirlo para casos extremos, pero hacer que se destaque como no idiomático.
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)). Tenga en cuenta que esto fallará catastróficamente en una lista infinita.
djvs
2
!!es una función parcial y por lo tanto insegura. Eche un vistazo al comentario a continuación y use lens stackoverflow.com/a/23627631/2574719
goetzc
90

No estoy diciendo que haya nada malo con su pregunta o la respuesta dada, pero tal vez le gustaría saber acerca de la maravillosa herramienta que es Hoogle para ahorrar tiempo en el futuro: con Hoogle, puede buscar funciones de biblioteca estándar que coinciden con una firma determinada. Por lo tanto, sin saber nada sobre !!, en su caso, podría buscar "algo que tome una Inty una lista de lo que sea y devuelva un solo lo que sea", es decir

Int -> [a] -> a

He aquí , con !!como primer resultado (aunque la firma de tipo en realidad tiene los dos argumentos al revés en comparación con lo que buscamos). Limpio, ¿eh?

Además, si su código se basa en la indexación (en lugar de consumir desde el principio de la lista), es posible que las listas no tengan la estructura de datos adecuada. Para el acceso basado en índices O (1) hay alternativas más eficientes, como matrices o vectores .

gspr
fuente
4
Hoogle es absolutamente genial. Todo programador de Haskell debería saberlo. Existe una alternativa llamada Hayoo ( holumbus.fh-wedel.de/hayoo/hayoo.html ). Busca mientras escribe, pero no parece ser tan inteligente como Hoogle.
musiKk
61

Una alternativa al uso (!!)es usar el paquete de lentes y su elementfunción y operadores asociados. La lente proporciona una interfaz uniforme para acceder a una amplia variedad de estructuras y estructuras anidadas por encima y más allá de las listas. A continuación, me centraré en proporcionar ejemplos y pasaré por alto tanto las firmas de tipos como la teoría detrás del paquete de lentes . Si desea saber más sobre la teoría, un buen lugar para comenzar es el archivo Léame en el repositorio de github .

Accediendo a listas y otros tipos de datos

Obtener acceso al paquete de lentes

En la línea de 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


Accediendo a listas

Para acceder a una lista con el operador infijo

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

A diferencia de (!!)esto, esto no generará una excepción al acceder a un elemento fuera de los límites y en su Nothinglugar regresará . A menudo se recomienda evitar funciones parciales como (!!)o, headya que tienen más casos de esquina y es más probable que causen un error de tiempo de ejecución. Puede leer un poco más sobre por qué evitar funciones parciales en esta página wiki .

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

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

Puede forzar la técnica de la lente para que sea una función parcial y lanzar una excepción cuando esté fuera de los límites utilizando el (^?!)operador en lugar del (^?)operador.

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


Trabajar con tipos distintos a las listas

Sin embargo, esto no se limita a las listas. Por ejemplo, la misma técnica funciona en árboles del paquete de contenedores estándar .

 > 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

Ahora podemos acceder a los elementos del árbol en orden de profundidad:

> 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

También podemos acceder a secuencias desde el paquete de contenedores :

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

Podemos acceder a las matrices indexadas int estándar desde el paquete de vectores , el texto del paquete de texto estándar , las cadenas de bytes del paquete de cadenas de bytes estándar y muchas otras estructuras de datos estándar. Este método estándar de acceso puede extenderse a sus estructuras de datos personales convirtiéndolas en una instancia de la clase de tipos Taversable , consulte una lista más larga de ejemplos de Traversables en la documentación de Lens. .


Estructuras anidadas

Profundizar en estructuras anidadas es simple con el truco de la lente . Por ejemplo, acceder a un elemento en una lista de listas:

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

Esta composición funciona incluso cuando las estructuras de datos anidadas son de diferentes tipos. Entonces, por ejemplo, si tuviera una lista de árboles:

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

Puede anidar arbitrariamente profundamente con tipos arbitrarios siempre que cumplan con el Traversablerequisito. Así que acceder a una lista de árboles de secuencias de texto es fácil.


Cambiar el enésimo elemento

Una operación común en muchos idiomas es asignar a una posición indexada en una matriz. En Python podrías:

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

El paquete de lentes brinda esta funcionalidad con el (.~)operador. Aunque a diferencia de Python, la lista original no está mutada, sino que se devuelve una nueva lista.

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

element 3 .~ 9es solo una función y el (&)operador, parte del paquete de lentes , es solo una aplicación de función inversa. Aquí está con la aplicación de función más común.

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

La asignación nuevamente funciona perfectamente bien con anidamientos arbitrarios de Traversables.

> [[1,2,3],[4,5,6]] & element 0 . element 1 .~ 9
[[1,9,3],[4,5,6]]
Davorak
fuente
3
¿Puedo sugerir un enlace en Data.Traversablelugar de la reexportación en lens?
dfeuer
@dfeuer: agregué un enlace a Data.Traversable en la base. También mantuve el enlace anterior y señalé que había una lista más larga de ejemplos de traverables en la documentación de Lens. Gracias por la sugerencia.
Davorak
11

Ya se dio la respuesta directa: Use !!.

Sin embargo, los novatos a menudo tienden a abusar de este operador, que es caro en Haskell (porque trabaja en listas vinculadas individuales, no en matrices). Existen varias técnicas útiles para evitar esto, la más sencilla es usar zip. Si escribe zip ["foo","bar","baz"] [0..], obtiene una nueva lista con los índices "adjuntos" a cada elemento de un par:, [("foo",0),("bar",1),("baz",2)]que a menudo es exactamente lo que necesita.

Landei
fuente
2
También debe tener cuidado con sus tipos allí. La mayoría de las veces no querrá terminar con índices enteros lentos en lugar de Ints de máquina rápidos. Dependiendo de lo que haga exactamente su función y cuán explícito sea su escritura, Haskell podría inferir que el tipo de [0 ..] es [Integer] en lugar de [Int].
chrisdb
4

Puede usar !!, pero si desea hacerlo de forma recursiva, a continuación se muestra una forma de hacerlo:

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

Tipo de datos de lista estándar de Haskell forall t. [t] en implementación se parece mucho a una lista enlazada de C canónica y comparte sus propiedades esenciales. Las listas enlazadas son muy diferentes de las matrices. En particular, el acceso por índice es una operación de tiempo constante O (n) lineal, en lugar de O (1).

Si necesita un acceso aleatorio frecuente, considere el Data.Arrayestándar.

!!es una función insegura parcialmente definida, que provoca un bloqueo de los índices fuera de rango. Tenga en cuenta que la biblioteca estándar contiene algunas de estas funciones parciales ( head, last, etc.). Por seguridad, utilice un tipo de opción Maybeo elSafe módulo.

Ejemplo de una función de indexación total robusta y razonablemente eficiente (para índices ≥ 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

Al trabajar con listas vinculadas, a menudo los ordinales son convenientes:

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

fuente
Estas funciones son recurrentes para siempre para Ints negativos y no positivos respectivamente.
Bjartur Thorlacius