5
\$\begingroup\$

After failing miserably to get SQL to work with Haskell, I decided to try making my own system. It's fairly simple, but works well for simple jobs (I made it to handle highscores for a number guessing game). Its biggest issue is that the "tables" are only 1-dimensional currently, so it's quite limited in that regard.

Besides the obvious issue of it only being 1-dimensional, I'm looking for general feedback. Anything related to good/bad practices would be appreciated.

I'm also wondering about the tableIsInDB function. I need to do the pre-check for an empty list being passed to it (because if any receives an empty list, it will always output true), but that forces the second definition to have 2 parameters (otherwise I get a "functions definitions have a different number of arguments" error), which prevents it from being written point-free. This isn't a big deal, but I've noticed that functions being written point-free are generally preffered. Is there any way of writing it point-free if it has 2 definitions?

import Data.Binary
import Data.List
import System.Directory

type TableDatum = String
type TableName  = String
type TableData  = [TableDatum]
type Table      = (TableName,TableData)
type Database   = [Table]

type Path = String

loadDB :: Path -> IO Database
loadDB path = do
    fileExists <- doesFileExist path
    if fileExists
        then decodeFile path
    else return []

saveDB :: Path -> Database -> IO ()
saveDB path db = do
    let tempPath = (path ++ "Temp")
    encodeFile tempPath db
    renameFile tempPath path

tableName :: Table -> TableName
tableName = fst

tableData :: Table -> TableData
tableData = snd

tableIsInDB :: TableName -> Database -> Bool
tableIsInDB _ [] = False
tableIsInDB name db = any (== True) $ map (\table ->
    tableName table == name) db

getTableNames :: Database -> [TableName]
getTableNames = map fst

addTable :: TableName -> Database -> Database
addTable name = ((name,[]):)

addData :: TableDatum -> Table -> Table
addData dat (tName,tDat) = (tName,dat:tDat)

addDataTo :: TableName -> TableDatum -> Database -> Database
addDataTo name dat db
    | tableIsInDB name db = map (\table ->
        if tableName table == name
            then addData dat table
            else table) db
    | otherwise =
        addDataTo name dat (addTable name db)

getTestDB :: Database
getTestDB = 
    addTable "Third" . addTable "Second" $ addTable "First" []

getTable :: TableName -> Database -> Table
getTable _ [] = ([],[])
getTable name (cTable : rest)
    | tableName cTable == name = cTable
    | otherwise = getTable name rest

showTable :: Table -> String
showTable t = tableName t ++ ":\n" ++
    concatMap (\datum -> "\t" ++ datum ++ "\n") (tableData t)

showDB :: Database -> String
showDB db = concatMap showTable db

overwriteTable :: Table -> Database -> Database
overwriteTable _ [] = []
overwriteTable t db =
    map (\table ->  if tableName table == tableName t
                        then t
                        else table) db

sortTable :: Table -> Table
sortTable (tName,tData) = (tName,sort tData)

cutTableTo :: Int -> Table -> Table
cutTableTo limit (tName,tData) = (tName, take limit tData)

sortAndCutTableTo :: Int -> Table -> Table
sortAndCutTableTo limit = cutTableTo limit . sortTable
\$\endgroup\$
3
  • \$\begingroup\$ I guess for my question, I could use guards. \$\endgroup\$ Commented Aug 17, 2014 at 4:35
  • 1
    \$\begingroup\$ "because if any receives an empty list, it will always output True" -- this is not correct. any id [] === False. So you don't need empty list check. \$\endgroup\$ Commented Aug 17, 2014 at 15:39
  • \$\begingroup\$ @max taldykin Whoops. I was thinking of all. Scratch my second question. Thanks. \$\endgroup\$ Commented Aug 17, 2014 at 15:42

1 Answer 1

2
\$\begingroup\$

I suggest you to use more advanced data structures than plain list. As your Database is actually a map from TableName to TableData it is better to represent it as Map.

To use Map we need some imports (please be sure to import it qualified, otherwise some names may clash).

 import Data.Binary
 import Data.List
+import Data.Map (Map)
+import qualified Data.Map as Map
 import System.Directory

It is really nice that you provided domain-specific type aliases. This makes type signatures more readable and allows to easily change underlying data representation as in this case.

-type Database   = [Table]
+type Database   = Map TableName TableData

Path type is redundant as Prelude already provides FilePath alias to String.

-type Path = String

Fortunately Data.Map is instance of Binary, so loadDB and saveDB need only small changes.

-loadDB :: Path -> IO Database
+loadDB :: FilePath -> IO Database
 loadDB path = do
     fileExists <- doesFileExist path
     if fileExists
         then decodeFile path
-    else return []
+    else return Map.empty

-saveDB :: Path -> Database -> IO ()
+saveDB :: FilePath -> Database -> IO ()

Most of your DB-manipulating functions have twins in Data.Map module:

 tableIsInDB :: TableName -> Database -> Bool
 tableIsInDB = Map.member

 getTableNames :: Database -> [TableName]
 getTableNames = Map.keys

 addTable :: TableName -> Database -> Database
 addTable name = Map.insert name []

 addDataTo :: TableName -> TableDatum -> Database -> Database
 addDataTo name dat = Map.insertWith (++) name [dat]

 getTable :: TableName -> Database -> Table
 getTable name db = case Map.lookup name db of
   Nothing  -> ("",[])
   Just dat -> (name, dat)

 overwriteTable :: Table -> Database -> Database
 overwriteTable = uncurry Map.insert

Empty database can be represented as empty map.

 getTestDB :: Database
 getTestDB = 
-    addTable "Third" . addTable "Second" $ addTable "First" []
+    addTable "Third" . addTable "Second" $ addTable "First" Map.empty

Converting from Map database to the old list-based is easy with Map.toList:

 showDB :: Database -> String
-showDB db = concatMap showTable db
+showDB = concatMap showTable . Map.toList

Also have to note that I like your coding style: domain-specific type aliases and short functions with descriptive names make code easy to read.

\$\endgroup\$
5
  • \$\begingroup\$ Thank you. I don't know why I didn't just use Map in retrospect. I loved Maps in c++ -_- (I haven't done much with them in Haskell though). And I had a mini-epiphany last night on a better way to set everything up (with column headers, and a SQL 'execute'-like syntax), so I'm not sure if a Map will still be applicable. If it does end up just being an association list, I might as well use Map; no point it reinventing the wheel. And thank you for your last point; that's a new habit. I'm finding that it definitely makes it easier to work with, even if it lengthens some names. \$\endgroup\$ Commented Aug 17, 2014 at 15:51
  • \$\begingroup\$ My plan isn't working as well as I would have hoped :/. execute (which takes a string command that's parsed) needs to have multiple return types, depending on what the command is, so I'm forced to use an ADT to represent all possible types; which will have to be dissected after to retrieve the actual result. \$\endgroup\$ Commented Aug 17, 2014 at 18:12
  • \$\begingroup\$ You can use continuation passing style interpreter, in that case command's return type will be encapsulated into continuation. \$\endgroup\$ Commented Aug 17, 2014 at 18:59
  • \$\begingroup\$ I've never heard of that technique before. It's called "continuous passing style" if I wanted to look it up? \$\endgroup\$ Commented Aug 17, 2014 at 19:02
  • \$\begingroup\$ Wow those look overly complicated. I think for this purpose, just deconstructing an ADT would be cleaner. \$\endgroup\$ Commented Aug 17, 2014 at 19:09

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.