-
Notifications
You must be signed in to change notification settings - Fork 3
Description
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) -> recordThen 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' + 3This 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) -> QueryCheck 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) })