1votos

Propuesta de Kata desde Aprendiendo TDD en Haskell

por josejuan hace 5 años

No hace falta inyección de dependencias, la inferencia de tipos de Haskell permite fijar la implementación concreta para cada alternativa a una abstracción (clase en Haskell o Interface en OOP). Se abstraen todos los aspectos del proceso requerido en las especificaciones.

Propuesta de Kata desde Aprendiendo TDD https://aprendiendotdd.wordpress.com/

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
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
{- 
 
  Las funcionalidades que debemos implementar son: 
  
    1. cargar un fichero 
     
    2. meterlo en una base de datos. 
     
    3. filtrar los datos de la base de datos 
  
    4. enviar por mail el resultado del filtro 
     
  Se pide implementarlo atendiendo a TDD y, en particular, 
  se hace referencia al uso de `mocks` para aislar 
  funcionalidades. 
 
  Así las cosas, ni vamos a definir el conjunto de datos 
  (no sea que cambie), ni vamos a leer el fichero (no sea que cambie), 
  ni vamos a escribir en la base de datos (no sea que cambie), ni 
  vamos a filtrar nada (no sea que cambie), ni vamos a enviar nada 
  por mail (no sea que cambie), ni vamos a seleccionar las pelis a 
  notificar (no sea que cambie). 
 
-} 
module Pelis where 
 
-- Abstraemos el conjunto real de datos a procesar (nos valdrá cualquiera) 
data Data a = Data a | Empty 
 
-- Abstraemos la fuente y forma en que se leen los datos (nos valdrá cualquiera) 
class Monad m => Readable m a where 
  read' :: Monad m => Data a -> m (Data a) 
 
-- Abstraemos el destino y forma en que se escriben los datos (nos valdrá cualquiera) 
class Monad m => Backendable m a where 
  writeDB :: Monad m => Data a -> m ()      -- añade datos (no están claros los requisitos) 
  readDB :: Monad m => Data a -> m (Data a) -- lee todos los datos del backend 
 
-- Abstraemos el tipo de filtro o transformación sobre los datos a notificar (n.v.c.) 
class Monad m => Filterable m a where 
  filter' :: Monad m => Data a -> m (Data a) 
 
-- Abstraemos la forma en que será notificado el filtrado (n.v.c.) 
class Monad m => Notifyable m a where 
  notify :: Monad m => Data a -> m () 
 
-- Así, toda nuestra funcionalidad totalmente abstraída y aislada (desacoplada) consiste en 
readWriteFilterNotify :: (Monad m, Readable m a, Backendable m a, Filterable m a, Notifyable m a) => Data a -> m () 
readWriteFilterNotify dataType = read' dataType >>= writeDB >> readDB dataType >>= filter' >>= notify 
 
 
 
 
 
 
 
 
-- ********************************************************************************************************* 
-- los ejemplos tienen un par de workarounds para que puedan estar todos en el 
-- mismo archivo y por eso son un poco más verboses de lo preciso 
 
{-# LANGUAGE NoMonomorphismRestriction          #-} 
{-# LANGUAGE FlexibleInstances                  #-} 
{-# LANGUAGE UndecidableInstances               #-} 
{-# LANGUAGE MultiParamTypeClasses              #-} 
 
import Control.Monad.Writer 
import Data.Spreadsheet 
 
... 
 
-- == EJEMPLO 1 ============================================================================ 
-- Un ejemplo "tonto" (sin persistencia) en que definimos `mocks` para cada abstracción 
 
data IntIO = IntIO [Int] 
 
dataIntIO :: Data IntIO 
dataIntIO = Empty 
 
instance Readable IO IntIO where 
  read' _ = return $ Data $ IntIO [1..10] 
 
instance Backendable IO IntIO where 
  writeDB (Data (IntIO ns)) = putStrLn $ "Data { [Int] := " ++ show ns ++ " }" 
  readDB _ = putStrLn "Reading..." >> return (Data $ IntIO [20..30]) 
 
instance Filterable IO IntIO where 
  filter' (Data (IntIO ns)) = return $ Data $ IntIO $ filter (\n -> n `mod` 2 == 0) ns 
 
instance Notifyable IO IntIO where 
  notify (Data (IntIO ns)) = putStrLn $ "Notificando... " ++ show ns 
 
-- Un ejemplo de ejecución. 
test1 = readWriteFilterNotify dataIntIO 
{- 
    *Pelis> test1 
    Data { [Int] := [1,2,3,4,5,6,7,8,9,10] } 
    Reading... 
    Notificando... [20,22,24,26,28,30] 
-} 
 
 
 
-- == EJEMPLO 2 ============================================================================= 
-- Para testear, usamos un Writer para la salida que deberá coincidir con lo esperado 
 
data IntWriter = IntWriter [Int] 
 
dataIntWriter :: Data IntWriter 
dataIntWriter = Empty 
 
instance MonadWriter String m => Readable m IntWriter where 
  read' _ = return $ Data $ IntWriter [1..10] 
 
instance MonadWriter String m => Backendable m IntWriter where 
  writeDB (Data (IntWriter ns)) = tell $ "Data { [Int] := " ++ show ns ++ " }" 
  readDB _ = tell "Reading..." >> return (Data $ IntWriter [20..30]) 
 
instance MonadWriter String m => Filterable m IntWriter where 
  filter' (Data (IntWriter ns)) = return $ Data $ IntWriter $ filter (\n -> n `mod` 2 == 0) ns 
 
instance MonadWriter String m => Notifyable m IntWriter where 
  notify (Data (IntWriter ns)) = tell $ "Notificando... " ++ show ns 
 
-- Un ejemplo de ejecución 
test2 = snd $ runWriter (readWriteFilterNotify dataIntWriter) 
{- 
    *Pelis> test2 
    "Data { [Int] := [1,2,3,4,5,6,7,8,9,10] }Reading...Notificando... [20,22,24,26,28,30]" 
-} 
 
-- Validar el test podría ser algo como 
test3 = test2 == "Data { [Int] := [1,2,3,4,5,6,7,8,9,10] }Reading...Notificando... [20,22,24,26,28,30]" 
{- 
    *Pelis> test3 
    True 
-} 
 
 
-- == EJEMPLO 3 ============================================================================= 
-- Usando directamente un archivo (haciendo bypass del backend) 
data Pelicula = Pelicula { idpeli    :: Int 
                         , titulo    :: String 
                         , director  :: String 
                         , cantidad  :: Int 
                         , precio    :: Double 
                         } deriving Show 
data DirectFile = DirectFile [Pelicula] deriving Show 
 
dataDirectFile :: Data DirectFile 
dataDirectFile = Empty 
 
instance Readable IO DirectFile where 
  read' _ = return dataDirectFile -- no lee nada, lo hará el backend 
 
instance Backendable IO DirectFile where 
  writeDB _ = return () -- no escribimos nada, leeremos directamente de disco 
  readDB _ = do 
    (nl, ok, err) <- readFile "pelis.txt" >>= return . parseRecords 
    putStrLn $ show nl ++ " líneas leidas." 
    mapM_ putStrLn err 
    return $ Data $ DirectFile ok 
    where   -- Helper para parsear cosas que están mal formadas 
            read' :: Read a => String -> Maybe a 
            read' s = case reads s of 
                          [(x, "")] -> Just x 
                          _         -> Nothing 
 
            -- Parsea una entrada, devolviendo los registros correctos y errores posibles indicando la línea 
            parseRecords = foldl parse (1, [], []) . fromStringSimple '\n' '|' 
              where parse (l, os, es) [i,t,d,c,p] = 
                      case (read' i, read' c, read' p) of 
                        (Just i', Just c', Just p') -> (l + 1, Pelicula i' t d c' p':os, es) 
                        _                           -> (l + 1, os, err l "formato incorrecto": es) 
                    parse (l, os, es) _           = (l + 1, os, err l "nº de campos incorrecto": es) 
                    err l msg                     = "Error en la línea " ++ show l ++ ": " ++ msg 
             
 
instance Filterable IO DirectFile where 
  filter' (Data (DirectFile ns)) = return $ Data $ DirectFile $ filter (\p -> director p == "James Cameron") ns 
 
instance Notifyable IO DirectFile where 
  notify (Data (DirectFile ns)) = putStrLn $ "Notificando... " ++ show ns 
 
-- Un ejemplo de ejecución. 
test4 = readWriteFilterNotify dataDirectFile 
{- 
    *Pelis> test4 
        32 líneas leidas. 
        Error en la línea 31: nº de campos incorrecto 
        Notificando... [Pelicula {idpeli = 17, titulo = "True Lies", director = "James Cameron", cantidad = 7, precio = 71.0},Pelicula {idpeli = 14, titulo = "Terminator 2: The Judgment day", director = "James Cameron", cantidad = 3, precio = 11.3}] 
 
-} 
 
 
 
{-- Otros usos ------------------------- 
 
lógicamente, otras versiones de `Notifyable` serían algo como 
 
    import Network.Mail.Mime 
     
    instance Notifyable IO CurrentImplementation where 
      notify (Data data) = simpleMail 
                                "you@example.com" 
                                "me@example.com" 
                                "Test" 
                                (show data) 
                                "" 
                                [] 
     
 
 
y para el backend otras como 
 
    instance Backendable IO UsingDataBase where 
      writeDB data = do 
          let config = SqliteConf $ getDataBaseContext -- que vendría de UsingDataBase 
          pool <- createPoolConfig config 
          runNoLoggingT $ runResourceT $ flip runSqlPool pool $ do 
            mapM_ insert_ data -- los datos son una lista del objeto de mapeo de persistencia 
      readDB _ = do 
          let config = SqliteConf $ getDataBaseContext -- que vendría de UsingDataBase 
          pool <- createPoolConfig config 
          runNoLoggingT $ runResourceT $ flip runSqlPool pool $ do 
            selectList [Asc title] 
     
 
-} 

Comenta la solución

Tienes que identificarte para poder publicar tu comentario.