Skip to content

Commit

Permalink
Merge branch 'master' of github.com:bijoutrouvaille/fireward
Browse files Browse the repository at this point in the history
  • Loading branch information
bijoutrouvaille committed Apr 11, 2019
2 parents 5894a9b + 1ba631b commit 9fe4256
Show file tree
Hide file tree
Showing 2 changed files with 99 additions and 49 deletions.
102 changes: 67 additions & 35 deletions docs/func-monad.md
@@ -1,42 +1,74 @@
# A function monad tutorial.
### Explanation of convertToNative
# A function monad tutorial.

Warning: this is just a theoretical exercise, not at all necessary for understanding the core code in this repo.

### Explanation of `convertToNative`

In the code this function is used:

convertToNative = flip maybe id <*> flip lookup natives

and explanation was promised. In a simple, not point-free way, the function can be written as

`convertToNative = flip maybe id `ap` flip lookup natives`
Because `<*>` is defined as `(<*>) = ap`, therefore,
a pointfree version of
convertToNative name = maybe name id (lookup name natives)

and it searches the dictionary `natives` for a `name` as the key and returns the value if there, and if not, returns itself.

The interesting part in this tutorial is the usage of operator `<*>`, defined as `(<*>) = ap`. `ap` has the type

ap :: Monad m => m (a -> b) -> m a -> m b

Let's annotate all the other constituents too:

maybe :: y -> (x -> y) -> Maybe x -> y
lookup :: Eq p => p -> [(p, a)] -> Maybe a
ap :: Monad m => m (a -> b) -> m a -> m b
ap m1 m2 = do { x1 <- m1; x2 <- m2; return (x1 x2) }
flip maybe id :: m (a -> b) === y -> Maybe x -> y === Maybe res -> (res or default name)

And, informally, the two expressions:

flip maybe id :: m (a -> b) === y -> (Maybe y -> y) === default -> Maybe result -> (result or default)
flip lookup natives :: m a === p -> Maybe a

### A how is a function a monad?
A function is a monad, defined as: `Monad ((->) r)`
think `Monad (Func r)` if `data Func ret arg = Func ret arg`.
Here, then, r stands for the return value.
What does the the bind operator do for functions?
Simple example: `let z = do { x <- f; return x }`
here `z` is a function that takes a param, binds it with `<-` to `x`
and then converts it into a new monad, which is `\_->x`. Therfore
`z` is equivalent to `f`. Precisely, `z x = const (f x) x`.
You may soon see what `m (a->b)` and `m a` are above. But the glue here is `ap`, and it glues two functions together somehow. The function `ap`, which seems to stand for "apply", takes a mapping function `a->b` wrapped in a monad and a monadic value `a` that the function would map over, producing the same monad with the mapped value `b`. It is defined like so:

ap m1 m2 = do { f <- m1; x <- m2; return (f x) }

For a more intuitive monad, `Maybe`, an example usage could look like this:

Just (+4) `ap` Just 7

Which would result in `11`. But...


### How is a function a monad?

A function is a monad, and is defined as: `Monad ((->) r)`, where `r` is the function's return value.
It helps to imagine it as a data type `data Func ret arg = Func ret arg` and think of it as `Monad (Func r)`.

What does the the `bind` function do for functions then?
Let's take a simple example: `let z f = do { x <- f; return x }`.
Here `z` is a function that takes a monadic param `f`, binds it with `<-` to `x`,
and then wraps it into the same monad. So `z` is `\_->x` aka `const`: it extracts the "future" return value from `f`, leaving an unapplied slot open for `f`'s argument. Therfore `z` is equivalent to its argument `f`. Precisely, `z f x = const (f x) x`.

The do notation for the function monad takes each binding
and applies the same parameter to each. For a more complex example:
`let z = do { a <- f; b <- g; c <- h; return [a,b,c] }`
Here `z` is same as: `z x = [f x, g x, h x]`.

Now we can know what ap does: it takes a function
with the return value of `(a->b)` and a function with the
return value of b and returns a function with the return
value of b. For example:
given: `f x a = show x++": "++show a; g x = show x;`
we have `ap f g === f `ap` g === fg :: a -> Int; fg p = (g p)`
or in longer terms:

fg p = result where -- with p = 77
x1 = f p -- :: a -> String = f a = "77: "++show a
x2 = g p -- :: String = "77"
result = x1 x2 -- :: String = "77: "++show "77" = "77: \"77\""

Hope this clarifies things
and applies the same parameter to it. In a slightly more complex example,

let z f g h = do { a <- f; b <- g; c <- h; return [a,b,c] }`

`z` is same as: `z f g h x = [f x, g x, h x]`.

Knowing this, we can monadically describe what `ap` does: it takes a function
with the return value of type `(a->b)` and a function with the
return value of type `b` and returns another function with the return
value of type `b`. For example, given: `f x a = show x ++ ": " ++ show a` and `g x = show x;`, `ap f g` would be the same as:

```haskell
fg p = result where -- e.g. p = 77
x1 = f p -- :: a -> String; f a = "77: " ++ show a
x2 = g p -- :: String = "77"
result = x1 x2 -- :: String = "77: "++show "77" = "77: \"77\""
fg 77
```

Function `f` returns `a->b` and `g` returns `a`, `ap` applies the second to the first, and the result is same as (f x) (g x), but we didn't have to repeat the `x` twice, which is exactly what we did in the plain version of the `convertToNative` function. There, `f` returns the first argument, if the second one is `Nothing`, and `g` returns a value wrapped in `Just` or `Nothing`, if not found.

That's it.
46 changes: 32 additions & 14 deletions readme.md
@@ -1,6 +1,6 @@
# Fireward
# FireWard

A successor to Firebase Bolt for writing Firestore rules. It also generates Typescript typings. The idea is to be able to add automatic type validation to routes.
A successor to Firebase Bolt for writing Firestore rules. It also generates TypeScript typings. The idea is to be able to add automatic type validation to routes.

## Status

Expand All @@ -18,7 +18,9 @@ Download a [release](https://github.com/bijoutrouvaille/fireward/releases) binar

#### Method 2: Compile Yourself

Download the [Haskell Stack tool](https://docs.haskellstack.org/en/stable/README/), `cd` into the project directory and run `stack install`.
Download the [Haskell Stack tool](https://docs.haskellstack.org/en/stable/README/), `cd` into the project directory and run `stack install`.

Stack will download and install the compiler and dependencies and put the `fireward` executable into the path given by `stack path --local-bin` command, which you may want to add to your `PATH`. If you have none of Haskell tooling installed, the build may take an unsupervised 30±20 minutes.

## Usage

Expand All @@ -38,20 +40,21 @@ Example:

Generate rules: `fireward -i myrules.ward > firestore.rules`

Generate typescript definitions: `fireward -i myrules.ward --lang=typescript > MyTypings.ts`
Generate TypeScript definitions: `fireward -i myrules.ward --lang=typescript > MyTypings.ts`

## Rules Syntax

Fireward tries to keep things simple by mostly using syntax that already exists from the two languages it compiles to: Firestore Rules and Typescript. Basic steps are: 1. Define a type 2. Assign it to a route. The .ward file is essentially the Firestore rules augmented with Typescript types.
Fireward tries to keep things simple and easy to learn by mostly using the syntax that already exists in the two languages it generates: Firestore Rules and TypeScript. The basic steps in writing Fireward are: 1. Define a type with TypeScript-like syntax. 2. Assign it to routes written with Firestore Rules syntax. Therefore, the .ward file is essentially the Firestore rules augmented with TypeScript types.

Fireward will wrap the code with the boilerplate
Fireward will wrap the code with the boilerplate:
```
service cloud.firestore {
match /databases/{database}/documents {
...
}
}
```
Currently, it is an error to do so yourself.

### Basic Example

Expand Down Expand Up @@ -81,39 +84,54 @@ match /users/{userId} is User {

#### Types

Firestore rules' types don't map exactly to JavaScript, and Fireward handles them. In particular:
- `int` and `float` map to Typescript `number`
Firestore rules language' primitive types don't map exactly to JavaScript, so Fireward has to convert them when generating TypeScript definitions. In particular:
- `int` and `float` map to TypeScript `number`
- `timestamp` maps to `Date|{isEqual: (other: any)=>boolean}`. Snapshots will come as a `Date`, but you can additionally assign a server timestamp object (`firebase.firestore.FieldValue.serverTimestamp`) when writing to the database.
- `bool` in rules maps to TS `boolean`

#### Unions

Union types are supported. Intersections are not. The usage is simple and demonstrated in the basic example above.

#### Lists

Rules support lists, which transpile to arrays or tuples in TS. The syntax is `MyType[]` or `MyType[n]`. The second variant will transpile to a MyType tuple up to n in size. If n is 4 (`MyType[4]`), for example, then the result will be a 0,1,2,3 or 4-tuple. Check the top of the generated TS file for the exported types that represent it.
Firestore lists are supported and transpile to arrays or tuples in TS. The syntax is `MyType[]` or `MyType[n]`. The second variant will transpile to a MyType tuple up to n in size. For example, if n is 4 (`MyType[4]`), then the result will be a 0,1,2,3 or 4-tuple, basically, an array up to 4 in length. Check the top of the generated TS file for the exported types that represent it.

#### Optional Types and `null`

Unlike in Firebase Realitime Database, optional types differ from `null`s. Optional types are indicated with a `?` before the colon, e.g. `{phone?: string}`. _Warning_: this will allow you to define keys with value `undefined`, which Firestore may reject as an error. Firestore has no equivalent to the JavaScript `undefined`.
Unlike in Firebase Realitime Database, optional types differ from `null`s. Optional types are indicated with a `?` before the colon, e.g. `{phone?: string}`. _Warning_: if you are relying on TypeScript, this will allow you to define keys with value `undefined`, which Firestore may reject as an error. Firestore has no equivalent to the JavaScript `undefined`.

#### Punctuation

is important. The example above demonstrates it. Extra or missing marks may cause the file to fail compilation.
is important. The example above demonstrates correct usage. Extra or missing marks may cause the file to fail compilation.

#### Route Matching, Conditions and Functions

For the exception of assigning a type to a route, the syntax is identical to the Firestore rule language syntax.

#### Comments and Splitting Across Files

are not yet supported. Both are in the immediate plans.

## Contributing

Contributions are welcome!

The project uses the stack tool and puts shortcuts into the makefile.
The project uses the stack tool and puts useful shortcuts, like `make test`, into the makefile.

The project was born from an exercise in monadic programming (based on _Monads for Functional Programming_ by Wadler, and _Thinking Functionally in Haskell_, the chapter on Parsing), so the parser is written from scratch. It seems to be the same in concept as Parsec, but with less functionality.

### Unit testing

The project was born from an exercise in monadic programming (based on _Monads for Functional Programming_, Wadler, and _Thinking Functionally in Haskell_, chapter on Parsing), so the parser is written from scratch. It seems to be the same in concept as Parsec, but with less functionality.
Please unit test contributions and make sure all the tests are passing when submitting. The project makes that part easy and fun.

## TODO
## Roadmap

- Rewrite in actual Parsec to allow for better error messages
- Add comments
- Allow for importing files
- Allow for read/write conditions within types
- Add Windows and Linux release executables pipelines.

## License

Expand Down

0 comments on commit 9fe4256

Please sign in to comment.