2

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

dhu
  • 718
  • 6
  • 19
  • Preliminary advice: I would define `Q` without a type variable, as `data Q = QAtom Atom | QList [Q]`. This allows you to make lists such as `QList [QAtom (D 1.0), QAtom (I 2), QList [QAtom (I 3), QAtom (I 4), QAtom (I 5)]]`, like in your Q example; you can’t represent such a structure with your current type. But beyond this I don’t know; I’ll have to look a bit more at your code and see if I find anything else to give advice on. **EDIT:** I was wrong, see next comment – bradrn Feb 11 '20 at 04:17
  • Actually, no, never mind: I misread your constructor `QAtom a` as `QAtom (Atom a)`. Your current definition of `Q` is actually fine! Sorry for the confusion. – bradrn Feb 11 '20 at 04:21
  • As for your `Traversable` instance: I think you should definitely make this an instance of `Foldable` etc., but as you have already noted your `Traversable` instance is a bit funny. A better way to implement that case would be to avoid pattern-matching and instead use the `Traversable []` instance: `sequenceA (QList fl) = fmap QList $ sequenceA $ fmap sequenceA fl`. (You could think of this as sequencing every individual element of the `QList`, then sequencing the whole list of actions which that gives you.) But I find it much easier to implement `traverse` rather than `sequenceA`. – bradrn Feb 11 '20 at 04:34
  • The statement "Lists are ordered collections that are build from atoms" is misleading. It is true in some trivial sense because all types in KDB are ultimately built from atoms, but only simple lists are build directly from atoms. Anyway, for better understanding of how q data types are represented in KDB you may want to have a look at the KDB header file for C interface (k.h, can be found on code.kx.com). Another useful resource is the collection of interafaces for KDB for Erlang, Java, C# and Python at https://github.com/exxeleron, which express KDB/q data types in those languages. – Slawomir K. Feb 11 '20 at 09:29

1 Answers1

7

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
  • 5
    I don't think it's true that every type has at most one traversable instance. I can think of at least two for `data Pair a = Pair a a` – luqui Feb 11 '20 at 06:25