Indexación de una estructura de datos de Haskell para consultas

Tengo una Data.Vector of Dog registros que identifican cada uno House donde vive dicho perro. Necesitaré una rutina de búsqueda para encontrar todos los perros que viven en una casa vagamente como la siguiente, pero necesito búsquedas de tiempo constantes, que esta primera versión no puede proporcionar.

dogs_by_houses dogs h = [ d | d <- Vec.toList dogs, h == house d ]

Según tengo entendido, una regla central para optimizar el código Haskell es que el compilador solo calcula cada expresión una vez dentro de su expresión lambda adjunta. Por lo tanto, debo construir una tabla de búsqueda para este particular dogs dentro de dogs_by_houses dogs expresión antes de vincular el h, ¿sí?

Presumo que Data.Vector es la mejor herramienta para esta tarea, aunque aparentemente no puede reducirlos como lo haría con los vectores C ++. Implementaría esto aproximadamente de la siguiente manera:

dogs_by_houses :: Vec.Vector Dog -> Int -> [Dog]
dogs_by_houses dogs = let {
        dog_house = house_id . house ;
        v0 = Vec.replicate (maximum . map dog_house $ Vec.toList dogs) [] ;
        f v d = let { h = dog_house d } in v // [(h,d:v!h)] ;
        dbh = Vec.foldl' f v0 dogs
   } in (dbh !)

¿Hay algo tremendamente tonto aquí en cuanto a optimización? Supongo que las etiquetas de rigor en variables como dbh no ayudará mucho ya que por definición dogs debe ser atravesado antes dbh tiene sentido.

¿Hay alguna gran ventaja en hacer esto con un MVector y create en su lugar se pliega devolviendo vectores inmutables modificados? Todos mis intentos de usar MVector y create han salido hasta ahora deben ser menos concisos, varias capas de dos o fold (>>) como construcciones o lo que sea. Supongo que el compilador debería simplemente compilar dbh en su lugar incluso sin que se le haya dado explícitamente una MVector.

¿Es este algoritmo imposible de lograr con listas? Ocasionalmente ves a personas construyendo listas infinitas perezosas de primos y luego seleccionando el n-ésimo número primo con primes !! n. Asumiría que recuperar el enésimo número primo de esa manera requiere atravesar los primeros n números primos de la lista cada vez que lo haga. Por el contrario, he notado que GHC almacena cadenas como cadenas C, no listas. ¿El compilador simplemente representaría los elementos de lista conocidos como una matriz en lugar de volver a recorrer la lista para cada uno?

Actualización:

He empleado las respuestas de Paul Johnson y Louis Wasserman para crear una función para indexar un vector arbitrario de esta manera porque debo hacerlo en función de varias funciones de indexación diferentes.

vector_indexer idx vec = \i -> (Vec.!) t i
  where m = maximum $ map idx $ Vec.toList vec
        t = Vec.accumulate (flip (:)) (Vec.replicate m []) 
               $ Vec.map (\v -> (idx v, v)) vec
dogs_by_houses = vector_indexer (house_id . house)

Aún no he perfilado esto, pero finalmente. Esperaría que uno deba escribir my_d_by_h = dogs_by_houses my_dogs y llama my_d_by_h para beneficiarse de la indexación.

preguntado el 30 de enero de 12 a las 19:01

GHC solo hace eso con cadenas conocidas en tiempo de compilación. -

2 Respuestas

Una vez me atraparon con un desagradable atrapado haciendo algo como esto. Estaba usando Data.Map.Map como una tabla de búsqueda, pero el principio era el mismo. Mi función tomó una lista de pares clave-valor, construyó un mapa y devolvió la función de búsqueda. Fue algo como ésto:

makeTable :: [(Key, Value)] -> Key -> Value
makeTable pairs = ((fromList pairs) !)

Me parecía obvio que luego podría escribir algo como

myTable = makeTable [("foo", fooValue), ("bar", barValue)  ... and so on]

Entonces podría tener una búsqueda O (log N) diciendo

v = myTable "foo"

Sin embargo, lo que realmente hizo GHC fue reconstruir el mapa completo de la lista para cada llamada. Cuando crea una aplicación parcial de esta manera, GHC no intenta averiguar qué valores puede derivar de los argumentos que tiene, solo almacena los argumentos sin procesar y hace la función completa para cada llamada. Comportamiento perfectamente razonable, pero no el que yo quería.

Lo que tuve que escribir en su lugar fue esto:

makeTable pairs = \k -> table ! k
   where table = fromList pairs

Imagino que tendrás que hacer lo mismo.

Respondido el 31 de enero de 12 a las 01:01

Ahh, se refieren a la expresión lambda literalmente cuando dicen "una vez por cada expresión lambda adjunta", ¡gracias! - Jeff Burdges

No estoy seguro de qué hubiera pasado si hubiera escrito "makeTable pairs = (table!) Where ..., que es lo que creo que tuviste. Usar" trace "en el primer argumento cuando lo llames podría ser esclarecedor. - Paul Johnson

Construiría la mesa con

Vec.accumulate (:) (Vec.replicate maxHouse []) 
  (Vec.map (\ d -> (dog_house d, d)) dogs)

que definitivamente asignará como máximo un vector intermedio, y sospecho que podría ser lo suficientemente inteligente como para no asignar ningún vector intermedio.

Respondido el 31 de enero de 12 a las 00:01

Vaya, me había perdido por completo las funciones de acumulación al leer Data.Vector, ¡Gracias! - Jeff Burdges

Sí. Este es el truco tradicional para el tipo de indexación que está haciendo aquí. - Louis Wasserman

No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas or haz tu propia pregunta.