Skip to content

Verified query bindings #8

@chouzar

Description

@chouzar

The current query engine is almost 1:1 API wrapper for erlang matchspecs. It would be neat to make the API both more functional and type safe, there are a couple of routes.

Typed Algebra

Instead of having a loose algebra for matchspecs implement a recursive Algebra that would always guarantee these are built correctly.

pub type Term(x) {
  Var(x)
  Ignore
  Value(x)
}

pub opaque type Constructor(a, b, c, d, e, f, g, h, i, record) {
  Constructor0(fn() -> record)
  Constructor1(fn(a) -> record)
  Constructor2(fn(a, b) -> record)
  Constructor3(fn(a, b, c) -> record)
  Constructor4(fn(a, b, c, d) -> record)
  Constructor5(fn(a, b, c, d, e) -> record)
  Constructor6(fn(a, b, c, d, e, f) -> record)
  Constructor7(fn(a, b, c, d, e, f, g) -> record)
}

pub opaque type Spec(a, b, c, d, e, f, g, h, i) {
  Spec0(#())
  Spec1(#(Term(a)))
  Spec2(#(Term(a), Term(b)))
  Spec3(#(Term(a), Term(b), Term(c)))
  Spec4(#(Term(a), Term(b), Term(c), Term(d)))
  Spec5(#(Term(a), Term(b), Term(c), Term(d), Term(e)))
  Spec6(#(Term(a), Term(b), Term(c), Term(d), Term(e), Term(f)))
  Spec7(#(Term(a), Term(b), Term(c), Term(d), Term(e), Term(f), Term(g)))
}

Functional API

One way to make the API a bit more functional would be to have a bind function that works on constructors and other functions.

fn bind(query, constructor) -> Query(index, record, x) {
  let #(#(index, record), conditions, body) = query
  let shape = ffi_build_matchhead(constructor)
  #(#(index, shape), conditions, shape)
}

@external(erlang, "lamb_erlang_ffi", "from_constructor")
fn ffi_from_constructor(constructor: constructor) -> record

Then on erlang's FFI:

from_constructor(Constructor) when is_function(Constructor) ->
    Arity = fun_info(Constructor, arity);
    Args = generate_args(Arity, []);
    Tag = element(1, Record);
    case is_record(Record, Tag, Arity) of
        True -> {ok, list_to_tuple([Tag | Args])};
        False -> {error, nil}
    end.

generate_args(0, Args) -> Args;
generate_args(Arity, Args) when is_integer(Arity), is_list(Args) ->
    generate_args(Arity - 1, [variable(Arity) | Args]).

variable(X) when is_integer(X) ->
    N = erlang:integer_to_binary(Arity);
    erlang:binary_to_atom(<<"$", N>>).

A big disadvantage of this approach is that it can introduce unexpected crashes, most non-opaque constructors would work very well but custom functions will try to do invalid operations like:

'$1' + 3

This approach was implemented in v0.4.1 with good but magical results.

Typed bindings

Have intermediate types that hold the table together, possible APIs:

query
|> bind(fn(a, b, c) { Record("value", a, b, c) })
|> map(fn(x: V1(a), y: V2(b), z: V3(c)) { record(NewRecord, #(x, y, z)) })
fn bind(fn(a, b, c, d) -> record, fn(index, record) -> y) -> Query

Check the decode and zero APIs and learn more about continuations for functional programming:

let record_4 = fn(constructor) {
  fn(_, _, _, _) {
    ???
  }
}
let is = fn(x) { fn(_) { x } }

q.new
|> q.match_index(is(3))
|> q.match_record(is(3))
|> q.match(fn(index, record) { #() index: is(3), record: bind(User))
|> q.filter(fn(index, record) { q.equals(index, record.id) })
|> q.map(fn(index, record) { #(index, record.name) })

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions