I am trying to model the kdb/q "atoms and lists" through Haskell type system.
In kdb/q all data is built from atoms. An atom is an irreducible value of a specific data type. Int, boolean and char are examples of atoms. Lists are ordered collections that are build from atoms. Since q is a vector language, most of the built-in operations are atomic, and therefore it recurses into the argument structure until it gets to atoms.
For example:
(1;2;3) is a simple list of integers 1, 2, 3
(1.0;2;(3;4;5)) is a general list of 1.0(float), 2(int), and a simple int list (3;4;5)
neg is a function that negates one number. For example:
neg 1 yields -1
neg -1.0 yields 1f
neg (1.0;2;(3;4;5)) yields (-1f;-2;(-3;-4;-5)).
This is what inspired me to try to model this behavior in Haskell types. The data type should consist of atom type, and a list.
The following is a simplified version of what I have so far. And I also went further to try to make it an instance of Foldable and Traversable.
data Atom = I Int
| C Char
| D Double
deriving Show
data Q a = QAtom a
| QList [Q a]
deriving Show
instance Functor Q where
fmap f (QAtom a) = QAtom (f a)
fmap f (QList qs) = QList $ fmap (fmap f) qs
instance Foldable Q where
foldMap f (QAtom a) = f a
foldMap f (QList qs) = mconcat $ fmap (foldMap f) qs
instance Traversable Q where
sequenceA (QAtom fa) = fmap QAtom fa
sequenceA (QList []) = pure $ QList []
sequenceA (QList (qfa:qfas)) = concatL <$> (sequenceA qfa) <*> (sequenceA (QList qfas))
where
concatL (QAtom a) (QList qas) = QList ((QAtom a):qas)
This is what I have and it compiles but I don't particularly like the concatL function that doesn't cover all the patterns according to the type. Once I start adding a new value constructor QDict [(Q Atom, Q a)] to Q, this just gets even worse.
Did I model the original data correctly? Should I even try to make it Traversable? However I thought Traversable is necessary if I need to use the data type with Maybe or Either to model errors.
Any advice is appreciated.
EDIT: edited q code formatting
The compiler knows how to automatically derive a Traversable
instance for your types. If you do :set -ddump-deriv -dsuppress-all -XDeriveTraversable -XStandaloneDeriving
and then deriving instance Traversable Q
, you can see the "right" answer. If you take that knowledge and apply it to your instance, you'd get this:
instance Traversable Q where
sequenceA (QAtom fa) = fmap QAtom fa
sequenceA (QList qfas) = fmap QList (traverse sequenceA qfas)
Or if you want to avoid traverse
in favor of sequenceA
:
instance Traversable Q where
sequenceA (QAtom fa) = fmap QAtom fa
sequenceA (QList qfas) = fmap QList (sequenceA (fmap sequenceA qfas))
The key is that lists themselves are Traversable
, so you can call sequenceA
on them without having to re-wrap pieces of them in your own type.
Side note, in your Foldable
instance, instead of chaining mconcat
and fmap
, just use foldMap
again, since lists are Foldable
too:
instance Foldable Q where
foldMap f (QAtom a) = f a
foldMap f (QList qs) = foldMap (foldMap f) qs