The Demo

Here’s the payoff. Read below for what the hell this is.


WhAT iS ThIS

The algebra of programming is an approach to making programming algebraic.

This means bringing the act of programming or analysis of programs to a place where it feels similar to manipulating polynomials or doing an integral. Make things point-free, in other words use combinators or make it categorical. This simplifies the equational reasoning.

An important side effect of this is that programs become perhaps offputtingly concise. This is important if you intend to manipulate a program with pencil and paper, because you will need to copy a significant portion of it for every new line of your deduction. There is something also to sheer syntactic mass being an obstruction to thought. It is good or at least interesting to consider what happens when you can fit an entire complex program on a line or screen. It changes the game. I remember Aaron Hsu saying this about APL, a bug that has not yet bit me. Combinators may take inspiration from traditional list combinators like fold, combinator calculus like SKI, or category theory. Depends on your backwards and predilections.

A second aspect that is interesting is to extend functional programming to relational specification. One has a sense that the concept of a mathematical function is a relatively nice fitting abstraction to the computers we’ve got. Functions are a special case of relations that are completely defined over the domain and deterministic. General relations feel in some sense farther from an implementation, but the extra oomph you get for tight concise specification of a problem is worth it, which can then be refined into a functional implementation.

Minikanren also operates relationally, in a way that can actually execute, so there is a natural question if these two things can be mixed. Minikanren is a relational or logic programming language similar in some respects to prolog. A crucial difference here is that it has a complete search strategy, which is at the least very convenient. It achieves it’s relational character essentially from the algorithm of unification, which is a kind of bidirectional pattern matching, or a method for solving a system of equations between terms with variables in them.

I fixed up a standard microkanren implementation to work on Biwa Scheme, which is a scheme that operates within javascript and hence the browser. The bits I needed to fix up were that Biwa does not support the suite of modern macro constructions, so I needed to convert the Microkanren macros to use unhygienic macros, something that is possible I did correctly, but who knows.

It’s also kind of fun that Repl.it supports Biwa Scheme, which was an ok dev environment. Many of my minikanren queries were infinite loops however, which it wasn’t so happy about, and I wasn’t super confident it was saving correctly every time.

I used something at least close to Haskell naming conventions for my operators. I knew I could do this minikanren relation algebra thing in haskell about two years ago, but it was too much of a project to do so and keep it well typed.

So I have a tendency when doing these point free things to shoot for the categorical combinators first. The identity relation is the same as the minikanren == relation and compose is kind of a standin for fresh. This is more or less the standard existential definition of relation composition, which can be thought of as a database join if you like.

(define (ido in out)
  (== in out)
)

(define (compo f g in out)
     (fresh (b) (f in b) (g b out))
)

Then I like to think about the cartesian combinators for dealing with pairs. forko is our first higher order relational combinator. There is a difference between the parameters f/g and in/out. f/g are relations, which are scheme lambdas whereas in and out are minikanren logic variables. More on this later.

(define (fsto in out)
  (fresh (a b) (== in `(Pair ,a ,b)) (== out a))
)

(define (sndo in out)
  (fresh (a b) (== in `(Pair ,a ,b)) (== out b))
)

; forko requires a higher order program call to interpo though? Nope
(define (forko f g in out)
      (fresh (a b c)
              (== in a)
              (== out `(Pair ,b ,c))
              (f a b)
              (g a c)
              )
)

A combinator derivable from these is duplicate. dup = fork id id

(define (dup in out)
  (== out `(Pair ,in ,in))
)

Then similarly we can deal with Either, aka the Sum type

(define (inj1o in out)
      (== out `(Left ,in))
)
(define (inj2o in out)
      (== out `(Right ,in))
)
; to curry or not curry. That is thy question
; Is that how we can metaprogram?
; 
(define (splito f g in out)
  (fresh (x)
        (conde
          ((== in `(Left ,x))  (f x out))
          ((== in `(Right ,x)) (g x out))
        )
  )
)

A combinator that revels we’re not in Kansas anymore is the converse combinator. So simple, and yet in some sense is solving the very difficult problem of inverting functions.

(define (convo f in out)
  (f out in)
)

I started very lightly down the road of recursion schemes, which is a point-free style for writing recursive programs with nice equational properties. It is recursion that is both a gift and a curse and for which the minikanren complete search is a great boon. As part of defining recursion schemes, you need to define fmap.

We can define a metainterpreter in minikanren for these relations very easily. What makes this interesting is that it opens the door to program synthesis and other cute tricks. As compared to other styles of making relational interpreters in minikanren, we avoid having to think about binding issues and contexts, which seems very nice. Maybe you pay for it though?

(define (interpo prog)
  (lambda (in out)
  (conde 
    ((== prog 'dump) (dumpo in out))
    ((== prog 'id) (ido in out))
    ((== prog 'fst) (fsto in out))
    ((== prog 'snd) (sndo in out))
    ((== prog 'distr) (distr in out))
    ((fresh (f g) (== prog `(fork ,f ,g))
                      ((forko (interpo f)
                             (interpo g))
                             in out 
                      )))
    ((== prog 'inj1) (inj1o in out))
    ((== prog 'inj2) (inj2o in out))
    ((fresh (f g) (== prog `(split ,f ,g))
              ((splito (interpo f) (interpo g)) in out)
              ))
    ((fresh (f g) (== prog `(comp ,f ,g)) (compo (interpo f) (interpo g) in out)))
    ;((fresh (f)  (== prog `(conv ,f)) (convo (interpo f) in out)))
    ((fresh (f) (== prog `(cata ,f))  ((cata (interpo f)) in out)))
    ((fresh (f) (== prog `(fmap ,f))  (fmapo (interpo f) in out)))

  )
))

With the converse operation is is very easy to derive the top relation that contains everything. (converse snd) . snd for example. I combatted this annoyance by just removing converse for the moment from the interpeter. A better solution may be to extend my microkanren with disequality operations, which the main minkanren already has, and include some anti-examples.

Bits and Bobbles

I feel my energy flagging on this, so let’s put it on the shelf by getting a blog post out and perhaps return to it someday.

You can synthesize programs by giving a bunch of examples. It is similar to barliman in a sense. https://github.com/webyrd/Barliman . Or perhaps you can write down a relational spec and insist that the outputs match for a pile of inputs. Or do some kind of cegar thing altenrating looking for bad inputs and good programs.

Something like

(define in '(1 2 3 4 5 6)) ; really we should conj over a list of possible inputs
(define spec ((compo sndo fsto))) ;; or some more interesting spec.
(run (f out) (conj (spec in out) ((interp f) in out)))

Even inferring a binary boolean operation based off it’s truth table was pretty slow. Then again I am using a funky javascript scheme. Me dunno.

I defined fmap in kind of a closed way. One can do it openly with imperative relation extension operations.

Is this a metainterpeter? Even if I get enough combinators to be equivalent to minikanren there is a slight difference of flavor here. I dunno.

Can I modify minikanren or make some kind of query to determine if a given relation is a subset of another one? AOP heavily dicsussed relational inclusion, which is part of what makes it particularly interesting

How do you get this to scale in the slightest? I need to actually read the minikanren quine interpreter paper http://io.livecode.ch/learn/gregr/icfp2017-artifact-auas7pp

Staging the interpeter? Nada Amin. Lisp conference 2021 keynote.

egraphs for aop.

Combinators for curry uncurry and apply. Makes it a closed catergory

I had forgotten about distr. It’s important mostly in the abscence of the curry combinators