Haskell - Reproduce la remodelación de numpy

8

Al entrar en Haskell, estoy tratando de reproducir algo como la remodelación de Numpy con listas. Específicamente, dada una lista plana, reconfórmela en una lista n-dimensional:

import numpy as np

a = np.arange(1, 18)
b = a.reshape([-1, 2, 3])

# b = 
# 
# array([[[ 1,  2,  3],
#         [ 4,  5,  6]],
# 
#        [[ 7,  8,  9],
#         [10, 11, 12]],
# 
#        [[13, 14, 15],
#         [16, 17, 18]]])

Pude reproducir el comportamiento con índices fijos, por ejemplo:

*Main> reshape23 [1..18]
[[[1,2,3],[4,5,6]],[[7,8,9],[10,11,12]],[[13,14,15],[16,17,18]]]

Mi código es:

takeWithRemainder :: (Integral n) => n -> [a] -> ([a], [a])
takeWithRemainder _ [] = ([], [])
takeWithRemainder 0 xs = ([], xs)
takeWithRemainder n (x:xs) = (x : taken, remaining)
                                where (taken, remaining) = takeWithRemainder (n-1) xs

chunks :: (Integral n) => n -> [a] -> [[a]]
chunks _ [] = []
chunks chunkSize xs = chunk : chunks chunkSize remainderOfList
                        where (chunk, remainderOfList) = takeWithRemainder chunkSize xs

reshape23 = chunks 2 . chunks 3

Ahora, parece que no puedo encontrar una manera de generalizar esto a una forma arbitraria. Mi idea original era hacer un pliegue:

reshape :: (Integral n) => [n] -> [a] -> [b]
reshape ns list = foldr (\n acc -> (chunks n) . acc) id ns list

Pero, no importa cómo lo haga, siempre obtengo un error de tipo del compilador. Según tengo entendido, el problema es que en algún momento, accse infiere que el tipo para es idie a -> a, y no le gusta el hecho de que la lista de funciones en el pliegue tiene un tipo diferente (aunque compatible para la composición) firma. Me encuentro con el mismo problema tratando de implementar esto con recursividad yo mismo en lugar de un pliegue. Esto me confunde porque originalmente me había propuesto para el [b]de reshape's firma de tipo a ser un sustituto de 'otra, de tipo disociado' que podría ser cualquier cosa de [[a]]a [[[[[a]]]]].

¿Cómo me estoy equivocando sobre esto? ¿Hay alguna manera de lograr realmente el comportamiento que pretendía, o es simplemente incorrecto querer este tipo de comportamiento "dinámico" en primer lugar?

shost71
fuente

Respuestas:

10

Aquí hay dos detalles que son cualitativamente diferentes de Python, que en última instancia se derivan de la escritura dinámica frente a la estática.

El primero que te has dado cuenta: en cada paso de fragmentación, el tipo resultante es diferente del tipo de entrada. Esto significa que no puede usar foldr, porque espera una función de un tipo específico. Sin embargo, podría hacerlo a través de la recursión.

El segundo problema es un poco menos obvio: el tipo de retorno de su reshapefunción depende de cuál sea el primer argumento. Como, si el primer argumento es [2], el tipo de retorno es [[a]], pero si el primer argumento es [2, 3], entonces el tipo de retorno es [[[a]]]. En Haskell, todos los tipos deben conocerse en tiempo de compilación. Y esto significa que su reshapefunción no puede tomar el primer argumento definido en tiempo de ejecución. En otras palabras, el primer argumento debe estar en el nivel de tipo.

Los valores de nivel de tipo pueden calcularse mediante funciones de tipo (también conocidas como "familias de tipos"), pero debido a que no es solo el tipo (es decir, también tiene un valor para calcular), el mecanismo natural (¿o el único?) Para eso es un tipo clase.

Entonces, primero definamos nuestra clase de tipo:

class Reshape (dimensions :: [Nat]) from to | dimensions from -> to where
    reshape :: from -> to

La clase tiene tres parámetros: dimensionsde tipo [Nat]es una matriz de números a nivel de tipo, que representa las dimensiones deseadas. fromes el tipo de argumento y toes el tipo de resultado. Tenga en cuenta que, aunque se sabe que el tipo de argumento siempre es [a], tenemos que tenerlo como una variable de tipo aquí, porque de lo contrario nuestras instancias de clase no podrán coincidir correctamente aentre argumento y resultado.

Además, la clase tiene una dependencia funcional dimensions from -> topara indicar que si conozco ambos dimensionsy frompuedo determinarlo sin ambigüedades to.

A continuación, el caso base: cuando dimentionses una lista vacía, la función simplemente se degrada a id:

instance Reshape '[] [a] [a] where
    reshape = id

Y ahora la carne: el caso recursivo.

instance (KnownNat n, Reshape tail [a] [b]) => Reshape (n:tail) [a] [[b]] where
    reshape = chunksOf n . reshape @tail
        where n = fromInteger . natVal $ Proxy @n

Primero realiza la llamada recursiva reshape @tailpara fragmentar la dimensión anterior, y luego fragmenta el resultado de eso utilizando el valor de la dimensión actual como tamaño de fragmentación.

Tenga en cuenta también que estoy usando la chunksOffunción de la bibliotecasplit . No hay necesidad de redefinirlo usted mismo.

Probémoslo:

λ reshape @ '[1] [1,2,3]          
[[1],[2],[3]]                     

λ reshape @ '[1,2] [1,2,3,4]        
[[[1,2]],[[3,4]]]                   

λ reshape @ '[2,3] [1..12]              
[[[1,2,3],[4,5,6]],[[7,8,9],[10,11,12]]]

λ reshape @ '[2,3,4] [1..24]                                                      
[[[[1,2,3,4],[5,6,7,8],[9,10,11,12]],[[13,14,15,16],[17,18,19,20],[21,22,23,24]]]]

Como referencia, aquí está el programa completo con todas las importaciones y extensiones:

{-# LANGUAGE
    MultiParamTypeClasses, FunctionalDependencies, TypeApplications,
    ScopedTypeVariables, DataKinds, TypeOperators, KindSignatures,
    FlexibleInstances, FlexibleContexts, UndecidableInstances,
    AllowAmbiguousTypes
#-}

import Data.Proxy (Proxy(..))
import Data.List.Split (chunksOf)
import GHC.TypeLits (Nat, KnownNat, natVal)

class Reshape (dimensions :: [Nat]) from to | dimensions from -> to where
    reshape :: from -> to

instance Reshape '[] [a] [a] where
    reshape = id

instance (KnownNat n, Reshape tail [a] [b]) => Reshape (n:tail) [a] [[b]] where
    reshape = chunksOf n . reshape @tail
        where n = fromInteger . natVal $ Proxy @n
Fyodor Soikin
fuente
¿En qué (..)parte está import Data.Proxy (Proxy(..))?
Micha Wiedenmann
@MichaWiedenmann (..)significa importar todos los constructores de tipos de datos y posiblemente los campos de registro. Como Proxysolo tiene un constructor, es equivalente aProxy(Proxy))
lehins
6

La respuesta de @Fyodor Soikin es perfecta con respecto a la pregunta real. Excepto que hay un pequeño problema con la pregunta en sí. Las listas de listas no son lo mismo que una matriz. Es un error común pensar que Haskell no tiene matrices y que se ve obligado a lidiar con listas, que no podrían estar más lejos de la realidad.

Debido a que la pregunta está etiquetada arrayy hay una comparación numpy, me gustaría agregar una respuesta adecuada que maneje esta situación para las matrices multidimensionales. Hay un par de bibliotecas de matrices en el ecosistema de Haskell, una de las cuales esmassiv

Una reshapefuncionalidad similar numpyse puede lograr mediante la resize'función:

λ> 1 ... (18 :: Int)
Array D Seq (Sz1 18)
  [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18 ]
λ> resize' (Sz (3 :> 2 :. 3)) (1 ... (18 :: Int))
Array D Seq (Sz (3 :> 2 :. 3))
  [ [ [ 1, 2, 3 ]
    , [ 4, 5, 6 ]
    ]
  , [ [ 7, 8, 9 ]
    , [ 10, 11, 12 ]
    ]
  , [ [ 13, 14, 15 ]
    , [ 16, 17, 18 ]
    ]
  ]
lehins
fuente
2
Tenga en cuenta que aquí, al igual que en mi respuesta, el argumento de las dimensiones está en el nivel de tipo
Fyodor Soikin