Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Verified query bindings #8

Open
chouzar opened this issue Nov 27, 2024 · 0 comments
Open

Verified query bindings #8

chouzar opened this issue Nov 27, 2024 · 0 comments

Comments

@chouzar
Copy link
Owner

chouzar commented Nov 27, 2024

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) })
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant