In one of my previous posts, I described an implementation of miniKanren in Haskell. I have since been improving the implementation.
I am currently working on the third iteration of my logic library. The first one was described by my previous post. The second is in a GitHub repository. I have yet to settle on a design that I really like, but I thought I’d post something about what I’ve been working on.
The key improvements in the second iteration were the ability to easily add custom data types and the addition of a `match` function. The custom data type addition was pretty straightforward.
The `match` function was an attempt to replicated the functionality of the `match` function in [William Byrd’s thesis](https://scholarworks.iu.edu/dspace/bitstream/handle/2022/8777/Byrd_indiana_0093A_10344.pdf). I *think* what I have does the same thing; if not, it still gave me the idea.
The type of my `match` function is as follows:
match :: (MonadKanren m, Matchable a, Unifiable b) => (a -> m (LVar b)) -> m (LVar a) -> m (LVar b)
Think of the type `m (LVar a)` as a relational monadic computation that returns a logic variable.
`match` turns code that looks like this:
logic_append a b ab = do conde [ do (h, t, res) <- fresh Cons h t === a Cons h res === ab ab === logic_append t b res , do Nil === a b === ab ]
logic_append a b = match (logic_append_match b) a where -- Note that the order of the arguments was swapped logic_append_match b Nil = b logic_append_match b (Cons x xs) = conso x (logic_append xs b)
It lets me take some advatage of Haskell's built-in pattern matching and cleans things up quite a bit.
Hey look! I found a Monad!
One of the _really_ interesting things about `match` that I didn't notice at first is that
it, has a _very_ similar type signature to the monad bind operator: `=<<`
match :: (a -> m (LVar b)) -> m (LVar a) -> m (LVar b) =<< :: (a -> m b) -> m a -> m b
Is it possible to build a monad around match? It turns out: yes! This is the core idea behind my [third iteration](https://github.com/jvranish/MiniKanrenT/tree/relational), which is starting to look a lot less like miniKanren.
In this implementation, `match` is implicit in monadic bind `>>=`. The main interesting consequence of this is that logic variable are abstracted away. A given piece of code in the resulting monad cannot tell the difference between a logic variable and a plain old
regular value. And in fact, if it could, that would mess up everything.
I’m still working on fleshing out the implications of this. I honestly don’t know if this is a really cool idea or a really dumb idea. Perhaps both?
The most obvious implication is that it saves a lot of wrapping and unwrapping to convert between logic variables and computations that produce logic variable. Though at first glance it doesn’t look like it grants much more than just some extra syntactic sugar for the ‘match’ function. But…
One thing that appears to be a more significant implication is laziness.
In my previous iterations, when I defined customs datatypes to be used by the logic library, I put the logic variables right in the datatype like so:
data List a = Cons (LVar a) (LVar (List a)) | Nil
Which worked fine, but it meant that I had to run all the computations that would produce those logic variables before I constructed an instance of my datatype.
In the new system, the list type looks like this:
data List f m = Cons (m (f m)) (m (List f m)) | Nil
Which, first of all looks _terrible_ (and I’ll talk more about that later), but secondly it lets me construct my list with just the _computation_ that will produce the logic variable (or maybe just a value) rather than the logic variable itself. The computation is only run when the value is needed.
I’m still not sure if all this is a really cool idea or just a really dumb idea, but I’m learning a ton about relational/logic programming in the process.
My goal for my next iteration is to clean up the super ugly datastructures. Currently a custom datatype in the new system must be a higher-kinded type with higher-kinded parameters, which is not just ugly, but also a real pain. Defining your own datastructures and converting back a forth between the logic and regular variants of a datastructure is currently the biggest pain point in the new system. Fortunately, I think I can fix it.