Como posso obter o enésimo elemento de uma lista?

97

Como posso acessar uma lista por índice em Haskell, análogo a este código C?

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

Respostas:

154

Olha aqui , o operador usado é !!.

Ou seja, [1,2,3]!!1dá a você 2, já que as listas são indexadas a 0.

Phimuemue
fonte
86
Pessoalmente, não consigo compreender como um acessador no índice que não retorna um tipo Maybe é aceitável como Haskell idiomático. [1,2,3]!!6apresentará um erro de tempo de execução. Isso poderia ser facilmente evitado se !!tivesse o tipo [a] -> Int -> Maybe a. A própria razão de termos Haskell é evitar esses erros de tempo de execução!
worldsayshi
9
É uma troca. O símbolo que escolheram é provavelmente o símbolo mais alarmante que poderiam ter. Acho que a ideia era permitir isso para casos extremos, mas fazer com que se destacasse como não 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)). Observe que isso falhará catastroficamente em uma lista infinita.
djvs
2
!!é uma função parcial e, portanto, insegura. Dê uma olhada no comentário abaixo e use lens stackoverflow.com/a/23627631/2574719
goetzc
90

Não estou dizendo que haja algo errado com sua pergunta ou com a resposta dada, mas talvez você gostaria de saber sobre a ferramenta maravilhosa que é o Hoogle para economizar tempo no futuro: Com o Hoogle, você pode pesquisar funções de biblioteca padrão que correspondem a uma determinada assinatura. Portanto, não sabendo nada sobre !!, no seu caso, você pode pesquisar "algo que pega uma Inte uma lista de qualquer coisa e retorna um único tal qualquer", a saber

Int -> [a] -> a

Lo e eis que , com !!como o primeiro resultado (embora a assinatura de tipo na verdade tem dois argumentos na ordem inversa em relação ao que buscamos). Legal, hein?

Além disso, se o seu código depende da indexação (em vez de consumir do início da lista), as listas podem na verdade não ser a estrutura de dados adequada. Para acesso baseado em índice O (1), existem alternativas mais eficientes, como matrizes ou vetores .

gspr
fonte
4
Hoogle é absolutamente fantástico. Todo programador Haskell deve saber disso. Existe uma alternativa chamada Hayoo ( holumbus.fh-wedel.de/hayoo/hayoo.html ). Ele pesquisa enquanto você digita, mas não parece tão inteligente quanto o Hoogle.
musiKk
61

Uma alternativa ao uso (!!)é usar o pacote de lentes e sua elementfunção e operadores associados. A lente fornece uma interface uniforme para acessar uma ampla variedade de estruturas e estruturas aninhadas acima e além das listas. Abaixo, vou me concentrar em fornecer exemplos e ignorar tanto as assinaturas de tipo quanto a teoria por trás do pacote de lentes . Se você quiser saber mais sobre a teoria, um bom lugar para começar é o arquivo leia-me no repositório github .

Acessando listas e outros tipos de dados

Obtendo acesso ao pacote de lentes

Na linha 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


Acessando listas

Para acessar uma lista com o operador infixo

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

Ao contrário de, (!!)isso não lançará uma exceção ao acessar um elemento fora dos limites e, em Nothingvez disso, retornará . Geralmente, é recomendável evitar funções parciais como (!!)ou, headpois elas têm mais casos extremos e são mais propensas a causar um erro de tempo de execução. Você pode ler um pouco mais sobre por que evitar funções parciais nesta página wiki .

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

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

Você pode forçar a técnica da lente a ser uma função parcial e lançar uma exceção quando estiver fora dos limites usando o (^?!)operador em vez do (^?)operador.

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


Trabalhar com tipos diferentes de listas

No entanto, isso não se limita apenas a listas. Por exemplo, a mesma técnica funciona em árvores do pacote de contêineres padrão .

 > 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

Agora podemos acessar os elementos da árvore em ordem de profundidade:

> 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

Também podemos acessar sequências do pacote de contêineres :

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

Podemos acessar os arrays indexados int padrão do pacote vetorial , texto do pacote de texto padrão , cadeias de bytes do pacote de cadeias de bytes padrão e muitas outras estruturas de dados padrão. Este método padrão de acesso pode ser estendido às suas estruturas de dados pessoais, tornando-as uma instância da typeclass Taversable ; consulte uma lista mais longa de exemplos de Traversables na documentação do Lens. .


Estruturas aninhadas

Cavar em estruturas aninhadas é simples com o hackage da lente . Por exemplo, acessando um elemento em uma 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

Essa composição funciona mesmo quando as estruturas de dados aninhadas são de tipos diferentes. Por exemplo, se eu tivesse uma lista de árvores:

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

Você pode aninhar profundamente com tipos arbitrários, desde que atendam ao Traversablerequisito. Portanto, acessar uma lista de árvores de sequências de texto é fácil.


Alterar o enésimo elemento

Uma operação comum em muitos idiomas é atribuir a uma posição indexada em uma matriz. Em python, você pode:

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

O pacote de lentes oferece essa funcionalidade com o (.~)operador. Embora ao contrário do python, a lista original não sofra mutação, mas uma nova lista é retornada.

> 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é apenas uma função e o (&)operador, parte do pacote de lentes , é apenas a aplicação da função reversa. Aqui está a aplicação de função mais comum.

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

A atribuição novamente funciona perfeitamente bem com aninhamento arbitrário de Traversables.

> [[1,2,3],[4,5,6]] & element 0 . element 1 .~ 9
[[1,9,3],[4,5,6]]
Davorak
fonte
3
Posso sugerir um link para em Data.Traversablevez de reexportar em lens?
dfeuer
@dfeuer - adicionei um link para Data.Traversable na base. Eu também mantive o link antigo e indiquei que havia uma lista mais longa de traversíveis de exemplo na documentação do Lens. Obrigado pela sugestão.
Davorak
11

A resposta direta já foi dada: Use !!.

No entanto, os novatos geralmente tendem a usar esse operador em excesso, o que é caro em Haskell (porque você trabalha com listas de links simples, não com matrizes). Existem várias técnicas úteis para evitar isso, a mais fácil é usar o zip. Se você escrever zip ["foo","bar","baz"] [0..], obterá uma nova lista com os índices "anexados" a cada elemento em um par:, [("foo",0),("bar",1),("baz",2)]que geralmente é exatamente o que você precisa.

Landei
fonte
2
Você também precisa ter cuidado com seus tipos lá. Na maioria das vezes, você não deseja que os índices sejam inteiros lentos, em vez de Ints de máquina rápidos. Dependendo do que exatamente sua função faz e quão explícita sua digitação é, Haskell pode inferir o tipo de [0 ..] como [Integer] em vez de [Int].
chrisdb
4

Você pode usar !!, mas se quiser fazer isso recursivamente, a seguir está uma maneira de fazer:

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

O tipo de dados de lista padrão de Haskell forall t. [t]na implementação se assemelha muito a uma lista vinculada C canônica e compartilha suas propriedades essenciais. Listas vinculadas são muito diferentes de arrays. Mais notavelmente, o acesso por índice é um O (n) linear-, em vez de uma O (1) operação de tempo constante.

Se você precisar de acesso aleatório frequente, considere o Data.Arraypadrão.

!!é uma função parcialmente definida insegura, provocando um travamento para índices fora do intervalo. Esteja ciente de que a biblioteca padrão contém algumas dessas funções parciais ( head, last, etc.). Por segurança, use um tipo de opção Maybeou o Safemódulo.

Exemplo de uma função de indexação total robusta e razoavelmente 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

Trabalhando com listas vinculadas, geralmente os ordinais são convenientes:

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

fonte
Essas funções têm recorrência contínua para Ints negativos e não positivos, respectivamente.
Bjartur Thorlacius