Article summary
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.
Improvements
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. 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
]
into this:
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, 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…
Laziness
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.
Going Forward
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.