Skip to content

Commit

Permalink
Update README
Browse files Browse the repository at this point in the history
  • Loading branch information
slightknack committed Sep 20, 2021
1 parent 1152324 commit 6f5a0cd
Showing 1 changed file with 57 additions and 14 deletions.
71 changes: 57 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,36 +65,79 @@ Where this overview gets really exciting is when we dive into [macros](#macros).
### Syntax
The goal of Passerine's syntax is to make all expressions as *concise* as possible while still conserving the 'feel' of *different types* of expressions.

We'll start simple; here's a simple snippet that defines a distance function:
We'll start simple; here's a function that squares a number:

```elm
square = x -> x * x
square 4 -- is 16
```

> By the way: Passerine uses `-- comment` for single-line comments and `-{ comment }-` for nestable multi-line comments.
There are already some important things we can learn about Passerine from this short example:

Like other programming languages, Passerine uses `=` for assignment. On the left hand side is a *pattern* – in this case, just the variable `square` – which destructures an expression into a set of bindings. On the right hand side is an *expression*; in this case the expression is a *function definition*.

> Because Passerine is *expression-oriented*, the distinction between statements and expressions isn't made. In the case that an expression produces no useful value, it should return the Unit type, `()`. Assignment, for instance, returns Unit.
This function call `square 4` may look a bit alien to you; this is because Passerine uses whitespace for function calls. A function call takes the form `l e₀ ... eₙ`, where `l` is a function and `e` is an expression. `square 4` is a simple case because `square` only takes one argument, `x`... let's try writing a function that takes two arguments!

Using our definition of `square`, here's a function that returns the Euclidean distance between two points:

```elm
distance = (x1, y1) (x2, y2) -> {
sqrt (square (x1 - x2) + square (y1 - y2))
}

length = distance (0, 0)
length (1 + 2 * 3, 4)
origin = (0, 0)
distance origin (3, 4) -- is 5
```

There are already some important things we can learn about Passerine from this short example:
Passerine is an *expression-oriented* language, because of this, it makes sense that *all functions are anonymous*. All functions take the form `p₀ ... pₙ -> e`, where `p` is a pattern and `e` is an expression. If a function takes multiple arguments, they can be written one after another; `a b c -> d` is equivalent to `a -> (b -> (c -> d))`.

The function `distance` is a bit more complex that `square`, because the two arguments are bound using *tuple destructuring*.

Like other programming languages, Passerine uses `=` for assignment. On the left hand side is a *pattern* – in this case, just the variable `linear` – which destructures an expression into a set of bindings. On the right hand side is an *expression*; in this case the expression is a *function definition*.
As you can see, `distance` takes two pairs, `(x1, y1)` and `(x2, y2)`, and *destructures* each pair into its component parts. For instance, when we call `distance` in `distance origin (3, 4)` the function pulls out the numbers that make up the pair:

Passerine is an *expression-oriented* language, because of this, it makes sense that *all functions are anonymous*. All functions take the form `p₀ ... pₙ -> e`, where `p` is a pattern and `e` is an expression.
- `origin`, i.e. `(0, 0)`, is matched against `(x1, y1)`, creating the bindings `x1 = 0` and `y1 = 0`.
- the tuple `(3, 4)` is matched against `(x2, y2)`, creating the bindings `x2 = 3` and `y2 = 4`.

> Because Passerine is *expression-oriented*, the distinction between statements and expressions isn't made. In the case that an expression produces no useful value, it should return the Unit type, `()`.
The body of `distance`, `sqrt (...)` is then evaluated in a new scope where the variables defined about are bound. In the case of the above example:

Passerine respects operator precedence.
```elm
-- call and bind
distance origin (3, 4)
distance (0, 0) (3, 4)

Passerine uses whitespace for function calls. A function call takes the form `l e₀ ... eₙ`, where `l` is a function and `e` is an expression. If we substitute `linear`, the first example is equivalent to:
-- substitute and evaluate
sqrt (square (0 - 3) + square (0 - 4))
sqrt (9 + 5)
sqrt 25
5
```

Now, you may have noticed that `distance` is actually two functions. It may be more obvious it we remove some syntactic sugar rewrite it like so:

```elm
distance = (x1, y1) -> { (x2, y2) -> { ... } }
```

The first function binds the first argument, then returns a new function that binds the second argument, which evaluates to a value. This is known as *currying*, and can be really useful when writing functional code.

> To leverage currying, function calls are *left-associative*. The call `a b c d` is equivalent to `((a b) c) d`, not `a (b (c d))`. This syntax comes from functional languages like Haskell and OCaml, and makes currying (partial application) quite intuitive.
In the above example, we used `distance` to measure how far away `(3, 4)` was from the origin. Coincidentally, this is known as the *length* of a vector. Wouldn't it be nice if we could define length in terms of distance?

```elm
length = distance origin
length (5, 12) -- is 13
```

> TODO
Because distance is curried, we can call it with only one of its arguments. For this reason, `length` is a function that takes a pair and returns its distance from the `origin`. In essence, we can read the above definition of `length` as:

> Function calls are left-associative, so the call `a b c d` is equivalent to `((a b) c) d`, not `a (b (c d))`. This syntax comes from functional languages like Haskell, and makes currying (partial application) quite intuitive.
> `length` is the `distance` from `origin`.
Passerine uses `-- comment` for single-line comments and `-{ comment }-` for nestable multi-line comments.
Transforming data through the use of and functions and pattern matching is a central paradigm of Passerine. In the following sections, we'll dive deep and show how this small core language is enough to build a powerful and flexible language.

#### A Quick(-sort) Example
Here's another slightly more complex example – a recursive quick-sort with questionable pivot selection:
Expand Down Expand Up @@ -808,7 +851,7 @@ circle = {
`mod` is nice because it's an easy way to have multiple returns. In essesence, the `mod` keyword allows for first-class scoping, by turning scopes into structs:

```elm
index = numbers pos
index = numbers pos
-> floor (len numbers * pos)

quartiles = numbers -> mod {
Expand Down Expand Up @@ -856,7 +899,7 @@ numbers = [1, 1, 2, 3, 5]
print (list_util::sum numbers)
```

Note that the `use` keyword is essentially the same thing as wrapping the contents of the imported file with the `mod` keyword:
Note that the `use` keyword is essentially the same thing as wrapping the contents of the imported file with the `mod` keyword:

```elm
-- use list_util
Expand Down

2 comments on commit 6f5a0cd

@dumblob
Copy link

@dumblob dumblob commented on 6f5a0cd Sep 20, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow, Passerine is getting unbeliavably close to XL (as mentioned in #50 (comment) ). Feel free to look at the extensive documentation of XL to see the cool features (basically everything I've ever seen in PLs throughout my life can be easily modeled in XL - incl. things like Rust's borrow semantics etc.).

But see also the kind of sad story about the flexibility as XL doesn't provide good support for restricting the programmer (restricting the programmer by default en masse is enormously important and then providing support for specifying exceptional highly-specific cases on per-lexical_place basis proves to be more practical than giving the full power straight ahead).

@slightknack
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've taken a look and XL and I think it's a nice language — my only qualms are with some of the ambiguities in the grammar (i.e. statements vs expressions), but other than that I think it could serve as a nice core for a language. My only question is how easy it is to compile. Because every construct is so meta, it seems like XL should be restricted to compile-time evaluation, i.e. interpretation. Can it compile to tight bytecode or assembly?

Reading your comment again, what you mention about restricting the programmer is really important. I think languages should be restrictive by default, with escape hatches for people who know what they are doing, but only if such escape hatches don't mangle the way the language works in unexpected ways. It's a hard problem to be solved, for sure.

Please sign in to comment.