Context
I'm trying to write a function that drops a list of tables from a database (e.g., tables A, B and C). This function has the following type, it returns a list of booleans indicating whether each table was successfully dropped:
dropTables :: (IConnection conn) => conn -> [String] -> IO [Bool]
Now, imagine table C depends on table B, i.e., table B cannot be dropped while table C exists. The function will be able to drop tables A and C, but not B, and so it will return [True, False, True]
.
So, I wanted to write a function that takes a list of tables, and calls dropTables
. If any of the operations fail, it calls dropTables
again but this time only with the tables that could not be dropped in the first iteration. And so on, until either all tables have been dropped, or progression halts (i.e. all operations in a given iteration fail, due to e.g. a connection being closed)
This was my first stab at it:
dropTablesRec :: IConnection conn => conn -> [String] -> IO ()
dropTablesRec conn tables = do
results <- dropTables conn tables
let failures = map fst $ filter (not . snd) $ zip tables results
unless (null failures) $
if length failures == length tables
then print $ "Failed to drop the following tables: " ++ intercalate ", " failures
else dropTablesRec conn failures
I then tried to generalize the core algorithm, and remove all references to tables and database connections:
-- Repeats a given action for a set of elements, until it succeeds for all elements, or fails for all elements.
-- Elements for which the operation succeeds are not passed onto the next iteration.
-- Returns the elements for which the action failed.
repeatUntilAll :: Monad m => [a] -> ([a] -> m [Bool]) -> m [a]
repeatUntilAll xs f =
let
go [] = return []
go failures
| length failures == length xs = return failures -- halt
| otherwise = repeatUntilAll failures f
in
do
results <- f xs
let failures = map fst $ filter (not . snd) $ zip xs results
go failures
dropTablesRec
can now be reduced to:
dropTablesRec' :: IConnection conn => conn -> [String] -> IO ()
dropTablesRec' conn tables = do
failures <- repeatUntilAll tables (dropTables conn)
unless (null failures) $ print $ "Failed to drop the following tables: " ++ intercalate ", " failures
I was wondering if there's a simpler way of implementing repeatUntilAll
.
I had a look around Control.Monad.Loops
to see if there was anything I could use, but no such luck. Being a Haskell novice doesn't help either, I'm only familiar with a very small part of the core libraries.
Other improvements (naming, conventions, greater generalization) are also welcome.