0.1.17 • Published 3 years ago

fauna-fp v0.1.17

Weekly downloads
17
License
MIT
Repository
github
Last release
3 years ago

fauna-fp: Functional Programming for FaunaDB

fql is clearly a functional programming language, with functions like map and reduce as first-class tools. Despite that, we don't have all the tooling that is associated with modern functional languages like function composition, parameter currying and strong type inference. fauna-fp is a project trying to bridge that gap.

Vision

To allow writing complex FQL queries using elevant function composition that looks like this:

const emailValidator = compose(q.select("email"), q.containsStrRegex("^.+@.+..+$"));
const passwordValidator = compose(q.select("password"), q.length, q.gte(8));

const validate = (f) => (x) => q.iff(f(x), x, q.abort("invalid data"));

const validateEmail = validate();
const validatePassword = validate(compose());

const register = compose(
  validate(emailValidator),
  validate(passwordValidator),
  (x) => ({ data: x }),
  q.create(usersCollection)
);

or, with types:

interface User {
  email: string;
  password: string;
}

// emailValidator: (x: Arg<{email: string}>) => Query<boolean> (inferred automatically)
const emailValidator = compose(select("email"), containsStrRegex("^.+@.+..+$"));
// passwordValidator: (x: Arg<{password: string}>) => Query<boolean> (inferred automatically)
const passwordValidator = compose(select("password"), length, gte(8));

const usersCollection = collection<User>("users");

const validate = <T>(f: (x: Arg<T>) => Query<boolean>) => (x: Arg<T>) =>
  iff(f(x), x, abort("invalid data")); // inferred as Query<T>

// register : (x: Arg<User>) => Query<Document<User>>;
const register = compose(
  validate(emailValidator),
  validate(passwordValidator),
  (x) => ({ data: x }),
  q.create(usersCollection)
);

Image showing type inference of newUser variable

Image showing type inference of insertedEmail variable

Here's what the query above might look like in "vanilla" FQL.

const Validator = (f: (x: ExprArg) => ExprArg) => (x: ExprArg) =>
  q.If(f(x), x, q.Abort("invalid data"));

const PasswordValidator = Validator((x) => q.GTE([8, q.Length(q.Select(["password"], x))]));

const EmailValidator = Validator((x) => q.ContainsStrRegex(q.Select(["email"], x), "^.+@.+..+$"));

const Register = (x: User) =>
  q.Create(q.Collection("users"), {
    data: PasswordValidator(EmailValidator(x)),
  });

const NewUser = Register({ email: "", password: "" });

const Email = q.Select(["data", "email"], newUser);

Status

Experimental. See some test files for example usage.

I think a critical advantage of this library comes from getting the types right, so I have started there. I did try to add function composition at the same time, but I'm not enough of a TypeScript expert to be able to pull off all the generic shenanigans that are required.

I haven't written analogues for all FQL functions yet, but I've written enough that I'm starting to see dividends in my own codebase. Having strongly-typed queries is really valuable!

Approach

Separate Library

To get the benefits of strong typing, it might have been possible to simply write an alternative type definition for the fauna JS client. I have chosen not to do that because I have broader ambitions for fauna-fp than just types. Things liks parameter order is inconsistent in FQL (see Map and Reduce which take data as first and last parameters respectively). Currying wouldn't be possible if I just wrote typedefs. I think we can do better.

Opaque types

I don't want to expose this library to the particular data structure used by Fauna under-the-hood. Instead, I have created a generic opaque Query<T> type which represents the result of computing a query. This type should never be constructed manually - it should only ever be returned by a fauna-fp function. Similarly, a Query type should never be inspected or used directly (it will appear to only have a __TYPE__ property upon inspection). Instead, it should be passed to the fauna-fp query function, which will execute the query and return the actual payload.

These opaque types allow me to represent the conceptual types being represented by the various FQL datatypes, withouot coupling to the internal structure, and while safeguarding myself from future changes to this structure.

0.1.17

3 years ago

0.1.16

3 years ago

0.1.15

3 years ago

0.1.14

3 years ago

0.1.13

3 years ago

0.1.12

3 years ago

0.1.10

3 years ago

0.1.11

3 years ago

0.1.9

3 years ago

0.1.5

3 years ago

0.1.4

4 years ago

0.1.2

4 years ago

0.1.3

4 years ago

0.1.1

4 years ago

0.1.0

4 years ago