package OCanren-ppx

  1. Overview
  2. Docs
Implementation of miniKanren relational (logic) EDSL: PPX extensions

Install

Dune Dependency

Authors

Maintainers

Sources

OCanren-ppx-0.2.0.tbz
sha256=77dd4b9d2b41f908ee4871612e78f455f9f0200a8c70321fcc82ebb53c86b4f0
sha512=c5dab91f59060f5dd5aecef5665e9235d8317cf071603070b0eef84ba6dc3a6ac027a8cf8aa37262109edb309b83c05f355a53c6718e43438164740c6b8715d4

Description

PPX rewriter for writing relational programs more confenient.

fresh (x...) goal 

expands into

Fresh.numeral (fun x ... -> goal)

Published: 05 Mar 2021

README

README.md

Branch dune
OCanren

Table of Contents

Introduction

OCanren is a strongly-typed embedding of relational programming language miniKanren into OCaml. Nowadays, the implementation of OCanren strongly reminds faster-miniKanren. Previous implementation was based on microKanren with disequality constraints.

What is miniKanren

miniKanren is an embedded language for constraint/logic/relational programming.

Prolog miniKanren OCanren
app([], X, X).
app([Y|Z], X, [Y,T]) :-
   app (Z, X, T).
(define appendo
 (lambda (l s ls)
   (conde
    [(== '() l) (== s ls)]
     [(fresh (a d res)
       (== `(,a . ,d) l)
       (== `(,a . ,res) ls)
       (appendo d s res))])))
let rec appendo x y z =
ocanren {
  x == [] & y == z |
  fresh h, t, ty in
    x == h :: t  &
    z == h :: ty &
    appendo t y ty
}

OCanren vs. miniKanren

The correspondence between original miniKanren and OCanren constructs is shown below:

miniKanren OCanren
#u success
#f failure
((==) a b) (a === b)
((=/=) a b) (a =/= b)
(conde (a b ...) (c d ...) ...) conde [a &&& b &&& ...; c &&& d &&& ...; ...]
(fresh (x y ...) a b ... ) fresh (x y ...) a b ...

In addition, OCanren introduces explicit disjunction (|||) and conjunction (&&&) operators for goals.

Injecting and Projecting User-Type Data

To make it possible to work with OCanren, user-type data have to be injected into logic domain. In the simplest case (non-parametric, non-recursive) the function

val inj  : ('a, 'b) injected -> ('a, 'b logic) injected
val lift : 'a ->  ('a, 'a) injected

can be used for this purpose:

inj @@ lift 1
inj @@ lift true
inj @@ lift "abc"

If the type is a (possibly recursive) algebraic type definition, then, as a rule, it has to be abstracted from itself, and then we can write smart constructor for constructing injected values,

type tree = Leaf | Node of tree * tree

is converted into

module T = struct
  type 'self tree = Leaf | Node of 'self * 'self

  let fmap f = function
  | Leaf -> Leaf
  | Node (l, r) -> Node (f l, f r)
end
include T
module F =  Fmap2(T)
include F

let leaf    ()  = inj @@ distrib @@ T.Leaf
let node   b c  = inj @@ distrib @@ T.Node (b,c)

Using fully abstract type we can construct type of ground (without logic values) trees and type of logic trees -- the trees that can contain logic variables inside.

Using this fully abstract type and a few OCanren builtins we can construct reification procedure which translates ('a, 'b) injected into it's right counterpart.

type gtree = gtree T.t
type ltree = ltree X.t logic
type ftree = (rtree, ltree) injected

Using another function reify provided by the functor application we can translate (_, 'b) injected values to 'b type.

val reify_tree : ftree -> ltree
let rec reify_tree eta = F.reify LNat.reify reify_tree eta

And using this function we can run query and get lazy stream of reified logic answers

let _: Tree.ltree RStream.t =
  run q (fun q  -> q === leaf ())
        (fun qs -> qs#reify Tree.reify_tree)

Bool, Nat, List

There is some built-in support for a few basic types --- booleans, natural numbers in Peano form, logical lists. See corresponding modules.

The following table summarizes the correspondence between some expressions on regular lists and their OCanren counterparts:

Regular lists OCanren
[] nil
[x] !< x
[x; y] x %< y
[x; y; z] x % (y %< z)
x::y::z::tl x % (y % (z % tl))
x::xs x % xs

Syntax Extensions

There are two constructs, implemented as syntax extensions: fresh and defer. The latter is used to eta-expand enclosed goal ("inverse-eta delay").

However, neither of them actually needed. Instead of defer (g) manual expansion can be used:

(fun st -> Lazy.from_fun (fun () -> g st))

To get rid of fresh one can use Fresh module, which introduces variadic function support by means of a few predefined numerals and a successor function. For example, instead of

fresh (x y z) g

one can write

Fresh.three (fun x y z -> g)

or even

(Fresh.succ Fresh.two) (fun x y z -> g)

Run

The top-level primitive in OCanren is run, which can be used in the following pattern:

run n (fun q1 q2 ... qn -> g) (fun a1 a2 ... an -> h)

Here n stands for numeral (some value, describing the number of arguments, q1, q2, ..., qn --- free logic variables, a1, a2, ..., an --- streams of answers for q1, q2, ..., qn respectively, g --- some goal, h --- a handler (some piece of code, presumable making use of a1, a2, ..., an).

There are a few predefined numerals (q, qr, qrs, qrst etc.) and a successor function, succ, which can be used to "manufacture" greater numerals from smaller ones.

Sample

We consider here a complete example of OCanren specification (relational binary search tree):

open Printf
open GT
open OCanren
open OCanren.Std

module Tree = struct
  module X = struct
    (* Abstracted type for the tree *)
    @type ('a, 'self) t = Leaf | Node of 'a * 'self * 'self with gmap,show;;
    let fmap eta = GT.gmap t eta
  end
  include X
  include Fmap2(X)

  @type inttree = (int, inttree) X.t with show
  (* A shortcut for "ground" tree we're going to work with in "functional" code *)
  @type rtree = (LNat.ground, rtree) X.t with show

  (* Logic counterpart *)
  @type ltree = (LNat.logic, ltree) X.t logic with show

  type ftree = (rtree, ltree) injected

  let leaf    () : ftree = inj @@ distrib @@ X.Leaf
  let node a b c : ftree = inj @@ distrib @@ X.Node (a,b,c)

  (* Injection *)
  let rec inj_tree : inttree -> ftree = fun tree ->
     inj @@ distrib @@ GT.(gmap t nat inj_tree tree)

  (* Projection *)
  let rec prj_tree : rtree -> inttree =
    fun x -> GT.(gmap t) LNat.to_int prj_tree x

end

open Tree

(* Relational insert into a search tree *)
let rec inserto a t' t'' = conde [
  (t' === leaf ()) &&& (t'' === node a (leaf ()) (leaf ()) );
  fresh (x l r l')
    (t' === node x l r)
    Nat.(conde [
      (t'' === t') &&& (a === x);
      (t'' === (node x l' r  )) &&& (a < x) &&& (inserto a l l');
      (t'' === (node x l  l' )) &&& (a > x) &&& (inserto a r l')
    ])
]

(* Top-level wrapper for insertion --- takes and returns non-logic data *)
let insert : int -> inttree -> inttree = fun a t ->
  prj_tree @@ RStream.hd @@
  run q (fun q  -> inserto (nat a) (inj_tree t) q)
        (fun qs -> qs#prj)

(* Top-level wrapper for "inverse" insertion --- returns an integer, which
   has to be inserted to convert t into t' *)
let insert' t t' =
  LNat.to_int @@ RStream.hd @@
  run q (fun q  -> inserto q (inj_tree t) (inj_tree t'))
        (fun qs -> qs#prj)

(* Entry point *)
let _ =
  let insert_list l =
    let rec inner t = function
    | []    -> t
    | x::xs ->
      let t' = insert x t in
      printf "Inserting %d into %s makes %s\n%!" x (show_inttree t) (show_inttree t');
      inner t' xs
    in
    inner Leaf l
  in
  ignore @@ insert_list [1; 2; 3; 4];
  let t  = insert_list [3; 2; 4; 1] in
  let t' = insert 8 t in
  Printf.printf "Inverse insert: %d\n" @@ insert' t t'

Camlp5 syntax extensions

A few syntax extensions are used in this project.

For testing we use the one from logger-p5 opam package. It allows to convert OCaml expression to its string form. For example, it rewrites let _ = REPR(1+2) to

$ camlp5o `ocamlfind query logger`/pa_log.cmo pr_o.cmo a.ml
let _ = "1 + 2", 1 + 2

For OCanren itself we use syntax extension to simplify writing relational programs

$  cat a.ml
let _ = fresh (x) z
$  camlp5o _build/camlp5/pa_ocanren.cmo pr_o.cmo a.ml
let _ = OCanren.Fresh.one (fun x -> delay (fun () -> z))

PPX syntax extensions

PPX syntax extensions are not related to camlp5 and should be used, for example, if you want decent IDE support. Main extensions are compilable by make ppx

An analogue for logger library is called ppx_repr:

$ cat regression_ppx/test002repr.ml
let _ = REPR(1+2)
$ ./pp_repr.native regression_ppx/test002repr.ml
let _ = ("1 + 2", (1 + 2))
$ ./pp_repr.native -print-transformations
repr

An OCanren-specific syntax extension includes both ppx_repr and extension for creating fresh variables

$ cat a.ml
let _ = fresh (x) z
$ ./pp_ocanren_all.native a.ml
let _ = OCanren.Fresh.one (fun x -> delay (fun () -> z))
$ ./pp_ocanren_all.native -print-transformations
pa_minikanren
repr

There also syntax extensions for simplifyng developing data type for OCanren but they are not fully documented.

Building from sources

This section contains instructions on how to build OCanren from sources and how to build and run tests.

Building the library

OCanren uses dune build system. In order to build the library only, type dune build src (alternatively, use make lib, which is a shortcut for the previous command).

You can build only the bytecode version of the library by dune build src/OCanren.cma or only the native version by dune build src/OCanren.cmxa.

Building the syntax extensions

The ppx syntax extensions can be built by the command dune build ppx ppxnew (or make ppx).

The camlp5 syntax extensions is built by the command dune build camlp5 (or make syntax),

Building and running tests

To build all tests type dune build regression (or make test).

In order to build the single test use dune build regression/testname.exe.

Tests can be run via the custom script test.sh. This script builds the given test, runs it, saves its output to the file regression/testname.log and compares it to the expected output from the regression/orig/testname.orig file. In case of the mismatch it will save the diff of two files into regression/testname.diff

To run all tests type (while in the root folder of the project) ./test.sh all (or, alternatively, make test). In order to run the single test type ./test.sh testname where testname is the name of one of the tests from the regression folder. E.g. ./test.sh test000 will build and run regression/test000.exe producing files regression/test000.log and regression/test000.diff.

It is possible to promote the output of the test and make it the new 'original'. In order to do that type ./test.sh --promote testname. This command will copy the content of the regression/testname.log file into regression/orig/testname.orig. In order to promote all tests, use ./test.sh --promote all (or make promote).

To remove all *.log and *.diff files use make clean-test.

Building and running samples

To build samples type dune build samples (or make samples). To build the single sample type dune build samples/prog.exe where prog is the name of the sample. Samples can be run via dune exec samples/prog.exe (e.g. dune exec samples/tree.exe).

Also, the output of the sample can be compared to the expected one (and promoted) by the script ./test.sh (e.g. ./test.sh tree will run the sample tree.exe and compare its output against samples/orig/tree.orig).

Building documentation

Starting from version 8 Camlp5 requries extra library pa_ppx.doc to build documentation. To get it you need

  • install this library: opam install pa_ppx

  • add -package pa_ppx.dock in two places in the file /camlp5/dune. Without it all the documentation in files, preprocessed by camlp5 will be omitted.

  • run dune build @doc

  • open _build/default/_doc/_html/index.html

More info

See samples in /regression and /samples subdirectories.

Dependencies (5)

  1. base >= "v0.14.1"
  2. ppxlib >= "0.22.0"
  3. dune-configurator
  4. dune >= "2.5"
  5. ocaml

Dev Dependencies

None

Used by

None

Conflicts

None