Yiyi Wang
miniKanren is an embeded Domain Specific Language for logic programming.
It was originally written in Scheme, and has been ported to dozens of other host languages over the past decade..
The core miniKanren language is very simple, with only three logical operators (eq
, and
, or
) and one interface operator run
.
is a JavaScript implementation of a modified version of miniKanren.
easy syntax
higher order function
independent from any big company
the only native web browser language
big community
lots of tools, libraries, and framworks
var x // x = undefinedvar y = "Hello JS"
function add(x, y) {return x + y}// orvar add = function(x, y) {return x + y}
primitive
String
Number
Boolean
Undefined
Null
non-primitive
Array
Object
RegExp
var x = "a"var y = 'a'x === y // true
var x = [1, 2]console.log(x.length) // 2x.push(3) // x = [1, 2, 3]
var x = {a: 1}console.log(x.a) // 1console.log(x['a']) // 1x.b = 2 // {a: 1, b: 2}x.a = 3 // {a: 3, b: 2}
(the newest version of JavaScript)
var foo = 'OUT'if (true) {var foo = 'IN'}console.log(foo) // IN
let foo = 'OUT'if (true) {let foo = 'IN'}console.log(foo) // OUT
const x = 1x = 2 // cause error.
function(x, y) {return x + y}(x, y) => {return x + y}(x, y) => x + y
function add(x=1, y=2) {return x + y}var add = (x=1, y=2)=> x + yadd() // 3add(3) // 5add(undefined, 3) // 4
function add(first, second, ...rest) {console.log('first: ', first)console.log('second: ', second)console.log('rest: ', rest)}add(1, 2, 3, 4, 5)// first: 1// second: 2// rest: [3, 4, 5]
function* genThree() {yield 1yield 2console.log('Hello World')yield 3}let three = genThree()three.next()// Object { value: 1, done: false }three.next()// Object { value: 2, done: false }three.next()// Hello World// Object { value: 3, done: false }three.next()// Object { value: undefined, done: true }
function* anotherGenerator(i) {yield i + 1;yield i + 2;yield i + 3;}function* generator(i) {yield i;yield* anotherGenerator(i);yield i + 10;}var gen = generator(10);console.log(gen.next().value); // 10console.log(gen.next().value); // 11console.log(gen.next().value); // 12console.log(gen.next().value); // 13console.log(gen.next().value); // 20
⟶ miniKanren in JavaScript.
The logic.js library
Either x is the beginning of the list ["banana", "orange", "apple"],or x is the number 1.
Either x is the beginning of the list ["banana", "orange", "apple"],or x is the number 1.
(The two values of x that make this expression true are 1
and banana
)
const q = lvar('q') // {id: "q"}const x = lvar() // {id: "~.0"}const y = lvar() // {id: "~.1"}
const x = new ImmutableMap() // x => {}const y = x.set('a', 12) // x => {}, y => {a: 12}console.log( x === y ) // falsex.get('a') // undefinedy.get('a') // 12
Given the expression:
Either x is the beginning of the list ["banana", "orange", "apple"],or x is the number 1.
{x: "banana"}
is a substitution map that makes this expression true
Either x is the beginning of the list [y, "orange", "apple"],or y is the number 1.
One substitution map which makes this expression true is:
{x: y, y: 1}
function walk(key, sMap) {if (isLVar(key)) {const val = sMap.get(key)if (val === undefined) return key // not foundreturn walk(val, sMap) // continue} else {return key}}walk(x, {}) // => xwalk(x, {x: y, y: 3}) // => 3
// unify(u, v, sMap)unify(x, "banana", {}) // ==> {x: "banana"}unify("banana", x, {}) // ==> {x: "banana"}
unify(x, "banana", {x: "banana"}) // {x: "banana"}unify(x, "banana", {x: "mango"}) // null// failed to unify two terms
function unify(x, y, sMap) {x = walk(x, sMap)y = walk(y, sMap)if (x === y) {return sMap} else if (isLVar(x)) {return sMap.set(x, y)} else if (isLVar(y)) {return sMap.set(y, x)} else { // failed to unifyreturn null}}unify(x, 12, {}) // => {x: 12}unify(x, y, {}) // => {x: y}unify(x, 1, {x: 2}) // => nullunify(x, m, {x: 3, m: n}) // => {x: 3, m: n, n: 3}
Scheme | logic.js (JavaScript) |
---|---|
== | eq |
conde | or |
no specific name | and |
run | run |
fresh | we don't have that |
eq(x, 1)eq(x, y)
function eq(x, y) {return function*(sMap) {yield unify(x, y, sMap) // unify x and y}}
fresh
, which syntactically looks like lambda
, introduces lexically-scoped Scheme variables that are bound to new logic variables; fresh also performs conjunction of the relations within its body. Thus
(fresh (x y z) (== x z) (== 3 y))
in Scheme
(fresh (x y z) (== x z) (== 3 y))
in JavaScript ES6
(x=lvar(), y=lvar(), z=lvar())=> and(eq(x, z),eq(3, y))
const x = lvar('x'),y = lvar('y')or(eq(x, 1), eq(x, 2))and(eq(x, y), eq(y, 1))or(eq(x, 1),(x=lvar())=> eq(x, 2))
function or(...clauses) {return function*(sMap) {const helper = function(offset, sMap) {const gen = clause(sMap) // clause is a goal(generator)while (true) {const res = gen.next(),sMap = res.valueif (res.done) breakif (sMap) yield sMap}yield* helper(offset + 1, sMap)}yield* helper(0, sMap)}}
function and(...clauses) {return function*(sMap) {const helper = function(offset, sMap) {const clause = clauses[offset] // clause is a goal(generator)const gen = clause(sMap)while (true) {const res = gen.next()if (res.done) breakconst sMap = res.valueif (sMap) {if (offset === clauses.length - 1)yield sMapelseyield* helper(offset+1, sMap)} else { // failed to unifyyield null}}}yield* helper(0, sMap)}}
In prolog
parent(amy,bob). % amy is the parent of bob.parent(bob,marco).parent(bob,mike).grandparent(X,Z) :- parent(X,Y) , parent(Y,Z).
in prolog
parent(amy,bob). % amy is the parent of bob.parent(bob,marco).parent(bob,mike).grandparent(X,Z) :- parent(X,Y) , parent(Y,Z).
in logic.js (miniKanren)
function parent(x, y) {return or(and(eq(x, 'amy'), eq(y, 'bob')),and(eq(x, 'bob'), eq(y, 'marco')),and(eq(x, 'bob'), eq(y, 'mike')))}function grandparent(x, z) {const y = lvar()return and(parent(x, y), parent(y, z))}
run(1, [x], eq(x, 1))// [{x: 1}]run([x], grandparent(x, 'mike'))// [{x: 'amy'}]run([x, y], grandparent(x, y))// [{x: 'amy', y: 'marco'}, {x: 'amy', y: 'mike'}]run(1, [x], and(eq(x, y),or(eq(x, 1), eq(x, 2), eq(x, 3)),eq(y, 2)))// [{x: 2}]
run(1, [x], and(eq(x, y),or(eq(x, 1), eq(x, 2), eq(x, 3)),eq(y, 2)))
run(1, [x], ...)
function eq(x, y) {return function*(sMap) {yield unify(x, y, sMap) // unify x and y}}
function eq(x, y) {return function*(sMap) {yield unify(x, y, sMap) // unify x and y}}
run(1, [x], and(eq(x, y),or(eq(x, 1), eq(x, 2), eq(x, 3)),eq(y, 2)))
Results: [{x: 2}]
function succeed() {return function*(sMap) {yield sMap}}run(1, [x], succeed()) // [{x: x}]run([x], or(eq(x, 1), succeed())) // [{x: 1}, {x: x}]
function fail() {return function*(sMap) {yield null // failed to unify}}run(1, [x], fail()) // []run(1, [x], and(eq(x, 1), fail())) // []
run(1, [x], and(eq(x, 'haha'), stringo(x))) // [{x: 'haha'}]run(1, [x], and(stringo(x), eq(x, 'haha'))) // []
cons(1, [2, 3]) // [1, 2, 3]first([1, 2, 3]) // 1rest([1, 2, 3]) // [2, 3]run(1, [x, y], conso(x, y, [1, 2, 3])) // [ { x: 1, y: [ 2, 3 ] } ]run(1, [x], firsto(x, [1, 2])) // [{x: 1}]run(1, [x], resto(x, [1, 2])) // [{x: [2]}]run(1, [x], emptyo(x)) // [{x: []}]run(1, [x], and(eq(x, 1), membero(x, [3, 2, 1]))) // [{x: 1}]
In prolog
append([],Ys,Ys).append([X|Xs],Ys,[X|Zs]) :-append(Xs,Ys,Zs).
In logic.js (miniKanren)
function appendo(Xs, Ys, Zs) {return or(and(empty(Xs), eq(Ys, Zs)),(X=lvar(), Xs_=lvar(), Zs_=lvar())=> and(conso(X, Xs_, Xs),conso(X, Zs_, Zs),appendo(Xs_, Ys, Zs_)))}
run([x, y], appendo(x, y, [1, 2, 3, 4, 5]))/*[ { x: [], y: [ 1, 2, 3, 4, 5 ] },{ x: [ 1 ], y: [ 2, 3, 4, 5 ] },{ x: [ 1, 2 ], y: [ 3, 4, 5 ] },{ x: [ 1, 2, 3 ], y: [ 4, 5 ] },{ x: [ 1, 2, 3, 4 ], y: [ 5 ] },{ x: [ 1, 2, 3, 4, 5 ], y: [] } ]*/
takes three arguments, and only works when at least two of the arguments are numbers
// x and y are logic variables//run([x], add(x, 3, 5)) // [{x: 2}]run([x], add(x, y, 5)) // []run([x], and(eq(y, 3), add(x, y, 5))) // [{x: 2}]run([x], and(add(x, y, 5), eq(y, 3))) // [{x: 2}]
takes two arguments, and only works when both arguments are numbers.
run([x], and(eq(x, 2), gt(x, 1))) // [{x: 2}]run([x], and(eq(x, 2), lt(x, 1))) // []run([x], and(gt(x, 1), eq(x, 2))) // []
In prolog
factorial(0,1).factorial(N,F) :-N>0,N1 is N-1,factorial(N1,F1),F is N * F1.
by logic.js
function factorial(N, F) {return or(and(eq(N, 0), eq(F, 1)),(N1=lvar(), F1=lvar())=> and(gt(N, 0),add(N1, 1, N),factorial(N1, F1),mul(F1, N, F)))}
function factorial(N, F) {return or(and(eq(N, 0), eq(F, 1)),(N1=lvar(), F1=lvar())=> and(gt(N, 0),add(N1, 1, N),factorial(N1, F1),mul(F1, N, F)))}run([x], factorial(6, x)) // [ { x: 720 } ]
run(4, [x], anyo(or(eq(x, 1), eq(x, 2), eq(x, 3))))// [ { x: 1 }, { x: 2 }, { x: 3 }, { x: 1 } ]run(10, [x], anyo(or(eq(x, 1), eq(x, 2), eq(x, 3))))// [ { x: 1 }, { x: 2 }, { x: 3 },// { x: 1 }, { x: 2 }, { x: 3 },// { x: 1 }, { x: 2 }, { x: 3 },// { x: 1 } ]run([x], anyo(or(eq(x, 1), eq(x, 2), eq(x, 3))))// will run forever
// easyfunction anyo(goal) {return or(goal, ()=> anyo(goal))}
You can find the sourcecode of logic.js
at
https://github.com/shd101wyy/logic.js/blob/master/lib/logic.js
Daniel P. Friedman, William E. Byrd and Oleg Kiselyov. The Reasoned Schemer. The MIT Press, Cambridge, MA, 2005.
Claire E. Alvis, Jeremiah J. Willcock, Kyle M. Carter, William E. Byrd, and Daniel P. Friedman. cKanren: miniKanren with Constraints. In Proceedings of the 2011 Workshop on Scheme and Functional Programming (Scheme '11), Portland, OR, 2011.