0votos

Kata borrado de ficheros (con dobles de prueba) en Haskell

por josejuan hace 5 años

Una implementación genérica hace que puedan usarse normalmente valores de cualquier tipo para testear la función (obtener un representante y separar los repetidos). Aplicarlo a la eliminación de ficheros es sólo un caso de todos los posibles (por tanto, se admiten todos los "dobles de prueba" que se deseen).

Se desea borrar todos los ficheros duplicados de un directorio. Se considera que dos ficheros son iguales si tienen exactamente el mismo tamaño. El reto de esta kata es utilizar dobles de prueba para abstraernos de los detalles de las llamadas al sistema. Intenta resolver este ejercicio sin realizar ninguna llamada que tenga que ver con el sistema de ficheros.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
-- Esta función es genérica, sirve para cualquier tipo de dato. 
-- Permite por tanto aplicar tantos "dobles de prueba" como se desee. 
-- Separa representantes únicos de los repetidos. 
 
splitDups :: Eq a => [a] -> ([a], [a]) 
splitDups = foldl split ([], []) 
  where split (uniques, dups) x = if x `elem` uniques 
                                    then (uniques, dups ++ [x]) -- al final por conveniencia 
                                    else (uniques ++ [x], dups) 
 
 
 
-- Como caso de uso concreto está la aplicación de eliminar ficheros repetidos. 
 
-- Para ello, creamos un tipo... 
data FileInformation = FileInformation { path :: FilePath 
                                       , size :: FileOffset } deriving Show 
 
-- ...en el que dos ficheros son iguales si son iguales sus tamaños. 
instance Eq FileInformation where 
  a == b = size a == size b 
 
 
-- Un helper para convertir rutas en nuestro tipo FileInformation 
getFileInformation :: FilePath -> IO FileInformation 
getFileInformation path = getFileStatus path >>= return . FileInformation path . fileSize 
 
 
-- Entonces, un programa completo sería 
main = getArgs 
       >>= find always (fileType ==? RegularFile) . head 
       >>= mapM getFileInformation 
       >>= mapM_ print . snd . splitDups            -- cambiar `print` por `removeFile` 
 
 
 
 
 
 
 
 
 
-- El código completo con test, otros tipos aplicados a `splitDups` etc... 
import System.Directory 
import System.FilePath.Find (find, always, FileType(RegularFile), fileType, (==?)) 
import System.Posix 
import Control.Monad 
import System.Environment 
 
{- 
 
      La implementación genérica del proceso solicitado 
      tiene coste cuadrático. 
 
-} 
splitDups :: Eq a => [a] -> ([a], [a]) 
splitDups = foldl split ([], []) 
  where split (uniques, dups) x = if x `elem` uniques 
                                    then (uniques, dups ++ [x]) -- al final por conveniencia 
                                    else (uniques ++ [x], dups) 
 
 
 
 
{- 
 
      Para testearla, pueden usarse tantos tipos como 
      se deseen (la propia función de test es genérica). 
 
-} 
testDupExample :: Eq a => String -> [a] -> ([a], [a]) -> IO () 
testDupExample msg source mustBe = do 
 
      let result = if splitDups source == mustBe 
                        then "CORRECT" 
                        else "WRONG" 
 
      putStrLn $ "Testing " ++ msg ++ " was " ++ result ++ "." 
 
 
 
{- 
 
      Por ejemplo, podemos crear "dobles de prueba" de cualquier tipo 
 
-} 
tests1 = do 
 
      testDupExample "integers" [1,4,8,2,6,4,9,8] ([1,4,8,2,6,9],[4,8]) 
      testDupExample "booleans" [True, False, False, True] ([True, False], [False, True]) 
      testDupExample "integer tuples" [(1, "While"), (2, "For"), (1, "While")] ([(1, "While"), (2, "For")], [(1, "While")]) 
 
 
{- 
 
      Obviamente, como la idea es usarla en un proceso de clasificación más general 
      (archivos), por lo que podemos crear cualquier tipo de dato (siempre que 
      implemente la clase Eq). 
 
-} 
 
data FileInformation = FileInformation { path :: FilePath 
                                       , size :: FileOffset } deriving Show 
 
-- Son iguales si son iguales sus tamaños 
instance Eq FileInformation where 
  a == b = size a == size b 
 
 
-- Para testear se puede hacer lógicamente 
tests2 = do 
 
      testDupExample "file information" [ FileInformation "A.txt" 12 
                                        , FileInformation "B.txt" 10 
                                        , FileInformation "C.txt" 12 
                                        , FileInformation "D.txt" 10 ] 
 
                                        ( [ FileInformation "A.txt" 12 
                                          , FileInformation "B.txt" 10 ] 
                                        , [ FileInformation "C.txt" 12 
                                          , FileInformation "D.txt" 10 ] ) 
 
 
 
-- Una vez que tenemos perfectamente testeada nuestra función, podemos usarla normalmente 
 
-- Una para convertir rutas en FileInformation 
getFileInformation :: FilePath -> IO FileInformation 
getFileInformation path = getFileStatus path >>= return . FileInformation path . fileSize 
 
 
-- Entonces, un programa completo sería 
main = getArgs 
       >>= find always (fileType ==? RegularFile) . head 
       >>= mapM getFileInformation 
       >>= mapM_ print . snd . splitDups            -- cambiar `print` por `removeFile` 
3 comentarios
0votos

Escrito por Javier J. hace 5 años

Josejuan, tiene una pinta genial. Además veo que, en este caso, creo el doble de prueba lo has hecho tú a mano y no has usado ninguna herramienta ya generada, que también está muy bien.

No se si Haskell tiene algo parecido asserts para verificar que se seleccionan los ficheros adecuados en vez e mandarlos por la consola.

¿Sería posible hacer un doble que reciba llamadas a removeFile (o que encapsule estas llamadas) y así poder verificar si se llama con los archivos correctos?

Gracias !!!!
0votos

Escrito por josejuan hace 5 años

Como el problema solicitaba básicamente (o eso he interpretado yo, claro :D) aislar el "computo" principal de las llamadas al sistema, he abstraído con la función splitDups, que hace todo lo que se solicita.

Dicha función sirve para cualquier tipo de dato, por lo que podemos hacer test sintéticos (aunque yo he puesto algunos hardcoded).

Del resto (llamadas al sistema), tenemos a getFileInformation de la que lógicamente habría que hacer test, pero no pueden abstraerse, porque su única función es una llamada al sistema (obtener el tamaño de un archivo), por tanto, yo ahí pondría un test sobre archivos reales en disco, algo como:

testIO = do
   assert "Check zero file" (0 `equalTo` (size $ getFileInformation "zero.txt"))
   assert "Check prime size" (3559 `equalTo` (size $ getFileInformation "prime.txt"))
   etc...


El operador de igualdad (la implementación de la clase Eq por el tipo FileInformation) sí admitiría hacer algún test, pero a simple vista parece correcta XD XD

La función main que es lo que queda del programa, son tres procesos de sistema con algo de pegamento, ese pegamento podría abstraerse y testearse (un ejemplo de abstracción de ese pegamento tan acoplado creo que podría ser la solución Propuesta de Kata desde Aprendiendo TDD), pero está tan acoplado a las llamadas del sistema y es tan pequeño que yo recuriría otra vez a test funcionales, clonando varios directorios ya preparados, lanzando el programa sobre ellos y verificando que han quedado como deben.

A mí lo que más me ha gustado de esta Kata, es que el proceso principal (buscar duplicados, dejar un represetante y eliminar el resto) es una función tan general como pueda ser sort, groupBy, ... y que el enunciado de la Kata a forzado a ver esa separación (cuando, efectivamente, muchos habríamos codificado un popurrí mezclandolo todo XD XD).
0votos

Escrito por Javier J. hace 5 años

"A mí lo que más me ha gustado de esta Kata, es que el proceso principal (buscar duplicados, dejar un represetante y eliminar el resto) es una función tan general como pueda ser sort, groupBy, ... y que el enunciado de la Kata a forzado a ver esa separación (cuando, efectivamente, muchos habríamos codificado un popurrí mezclandolo todo XD XD)"

Sííííí. ¡¡ Gracias y felicidades por verlo !! De nuevo un trabajo excelente, aunque me cuesta horrores entender Haskell ;)

Comenta la solución

Tienes que identificarte para poder publicar tu comentario.