Skip to content

Approximating JSPath with Partial Lenses

Vesa Karvonen edited this page Jan 14, 2018 · 3 revisions

JSPath is a DSL for querying JSON.

This is the beginnings of a translation of JSPath queries to Partial Lenses expressions. The translation is based on the description of the semantics in the JSPath documentation and some experiments in the REPL with JSPath. The rules are not (yet) complete and might contain errors.

      JSPath(path, data)  ~  collectM(P"path", data)

A JSPath(path, data) expression can be translated by translating the path to a monadic traversal, P"path" and using a monadic collectM function to collect the results.

const CollectM = {
  map: (xy, xM) => [xy(xM[0]), xM[1]],
  ap: (xyM, xM) => [xyM[0](xM[0]), [xyM[1], xM[1]]],
  of: v => [v, []],
  chain: (xyM, xM) => {
    const yM = xyM(xM[0])
    return [yM[0], [xM[1], yM[1]]]
  }
}

const collectM =
  R.pipe(
    L.traverse(CollectM, v => [v, {v}]),
    L.collect([L.flatten, 'v'])
  )

The reason for needing a monadic traversal is that the .. operator of JSPath targets both the focus itself and the children of the focus.

                    P"."  ~  immediate
                   P".."  ~  nested
                 P"name"  ~  prop(name)
          P"{predicate}"  ~  L.when(F"predicate")
      P"path1 ... pathN"  ~  [P"path1", ..., P"pathN"]
  P"path1 | ... | pathN"  ~  L.seq(P"path1", ..., P"pathN")

For this reason the definition of nested uses L.seq, which requires a monad.

const immediate = L.ifElse(R.is(Array), L.elems, L.identity)

const nested = L.lazy(rec => L.cond(
  [R.is(Array), [L.elems, rec]],
  [R.is(Object), L.seq(L.identity, [L.values, rec])]
))

const prop = name => [L.when(R.has(name)), name]

Predicates are translated to functions that take the data at the focus as their parameter.

                 F"path"  ~  collectM(P"path")
             F"constant"  ~  R.always(constant)
           F"!predicate"  ~  negate(F"predicate")
           F"lhs && rhs"  ~  andOp(F"lhs", F"rhs")
           F"lhs || rhs"  ~  orOp(F"lhs", F"rhs")
        F"lhs cmpOp rhs"  ~  cmpOp(F"lhs", C"cmpOp", F"rhs")
      F"lhs arithOp rhs"  ~  binOp(F"lhs", O"arithOp", F"rhs")

Comparison operators are promoted to operate on sets (arrays) of elements.

const promote = x => R.is(Array, x) ? x : [x]

const cmpOp = (lhs, op, rhs) => data => {
  const lhss = promote(lhs(data))
  const rhss = promote(rhs(data))
  return R.any(lhs => R.any(rhs => op(lhs, rhs), rhss), lhss)
}

Other binary operators aren't.

const binOp = (lhs, op, rhs) => data =>
  op(lhs(data), rhs(data))

Logic operations are lazy.

const orOp = (lhs, rhs) => data =>
  lhs(data) || rhs(data)
const andOp = (lhs, rhs) => data =>
  lhs(data) && rhs(data)