Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/wqferr/functional

Functional programming utilities implemented in pure Lua.
https://github.com/wqferr/functional

functional-programming lua lua-library luarocks teal

Last synced: 3 months ago
JSON representation

Functional programming utilities implemented in pure Lua.

Awesome Lists containing this project

README

        

# functional
Functional programming utilities implemented in pure Lua.

> The motivation behind this module is, again, portability.
If you want to embed this code on a webpage, or use it in some weird
system for which a C binding wouldn't work, this project is aimed
at you.

[The docs can be found here](https://wqferr.github.io/functional/), and were
generated by [LDoc](https://github.com/stevedonovan/LDoc). This module is
published as a rock at [luarocks.org/modules/wqferr/functional](http://luarocks.org/modules/wqferr/functional).

# About
This module seeks to provide some utility functions and structures
which are too verbose in vanilla Lua, in particular with regards to iteration
and inline function definition.

The module is writen completely in vanilla Lua,
with no dependencies on external packages. This was a decision made for
portability, and has drawbacks. Since none of this was written as a C binding, it is not
as performant as it could be.

For example, [luafun](https://github.com/luafun/luafun)
is "high-performance functional programming library for Lua designed with
[LuaJIT](http://luajit.org/luajit.html)'s trace compiler in mind".
If your environment allows you to use LuaJIT and performance is a
concern, perhaps luafun will be more suited for your needs.

**The motivation behind this module is, again, portability.
If you want to embed this code on a webpage, or use it in some weird
system for which a C binding wouldn't work, this project is aimed
at you.**

## Teal
This project also includes `functional.d.tl`, which allows it to be used with
[Teal](https://github.com/teal-language/tl): a typed dialect of Lua. The Makefile
included is a workaround so LuaRocks actually installs the `.d.tl` file as well.

Teal support is not complete, however, since some functions require features not
yet stable on the Teal compiler side. For more information on how to use this
library with Teal, see "Usage with Teal" below.

# Learning the Ropes

If this is your first time using (or even seeing) these kinds of "stream manipulating
operators" and all those other fancy words, don't worry: we'll start from the beginning.

## Iterators

Sometimes called "streams", they are just that: some... *thing* that can produce values
over time through iteration.

Let's say we want to get an array with the numbers from 1 to 10. Sounds easy?

```lua
local f = require "functional"

local my_counter = f.counter()
local my_capped_counter = my_counter:take(10)
local my_array = my_capped_counter:to_array()

print(type(my_array))
for i, v in ipairs(my_array) do
print(i, v)
end
```

That may seem like a lot, but those three lines make sense if you think of each step
individually. Starting from the top down:

`f.counter()` is a function that creates an iterator that just counts up. Forever.
Well, this is a start, but we don't want an infinite array! We want to cut it short!

`my_counter:take(10)` will do just that: cut the previous thing short, and stop
after 10 elements have gone through this step.

The `to_array` method just collects all the items in the stream into an array. That
transforms the abstract and scary "iterator" into a more familiar face.

From that point on, it's just regular Lua: `ipairs` into printing.

## Operator Chains

Now here's the neat part: you don't have to assign each step to a new variable. We don't
even use most of them except to define the next step.

Instead, we can just collapse them all, as below:

```lua
local my_array = f.counter() -- my_counter
:take(10) -- my_capped_counter
:to_array() -- my_array
for i, v in ipairs(my_array) do
print(i, v)
end
```

And of course, the line breaks between each step are optional: I just put them there for clarity.
In other words, you can create that 1-10 array in a single line!

What? You could've just typed `{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}`, yeah, but what about getting
an array with all numbers from 1 to 1000? You'd have to write a for loop, and for loops in Lua
are verbose.

And this is the bare-bones example: how about we try something a little more interesting?

## Filtering

Given a list of names, which ones contain the letter B? Let's assume you already have this
chunk of code:

```lua
local names = {
"Arya",
"Beatrice",
"Caleb",
"Dennis"
}

local function has_b(name)
if name:find("[Bb]") then
return true
else
return false
end
end
```

In pure Lua, you could write something like:
```lua
local names_with_b = {}
for _, name in ipairs(names) do
if has_b(name) then
table.insert(names_with_b, name)
end
end
```

Here, `has_b` is called a predicate: a simple function which returns `true` or `false` for any given name.
Predicates are especially good for filtering. Either you keep something, or you throw it out. In fact, the
whole loop is a really common pattern: `if predicate(val): keep(val)`. `functional` has the function
`filter` and the operator `:filter` to do just that:

```lua
local names_with_b = f.filter(names, has_b):to_array()
```

## Mapping

Now, what if you wanted all these names in all caps? In pure Lua, you'd have to change
the core loop:

```lua
local names_with_b = {}
for _, name in ipairs(names) do
if has_b(name) then
table.insert(names_with_b, string.upper(name))
end
end
```

In a functional environment, you can just add another operator. Since we're applying the same
function to all elements in a stream, we can use the `:map` operator. It transforms (or "maps")
every element in the stream to the return value of a function.

```lua
local names_with_b = f.filter(names, has_b)
:map(string.upper)
:to_array()
```

Now with line breaks for readability.

## TODO Reducing

## Lambdas
### What are lambdas?

If you've never heard of lambdas, you might be thinking this is some sort of "ligma" joke. It isn't.

Many languages (including Lua!) have a way to declare anonymous functions. That is, a function
that is declared "on the spot", just to be used as an argument to another function, or to be
stored in a variable. What most of these languages have that Lua lacks is a shorthand notation
for creating such functions.

In Lua, there's no getting around typing `function()` (plus parameters) and `end`. That's 13
characters typed minimum for the simplest anonymous functions. As an example, here's the definition
of a function that doubles its argument:

```lua
triple = function(x) return 3*x end
```

Compare that to Python:

```python
triple = lambda x: 3*x
```

That's nearly half the characters, and the Lua example doesn't even declare `triple` as a local.
And JavaScript's is even shorter!

```javascript
triple = (x) => 3*x
```

### OK, but why does it matter?

It's perfectly fine to use vanilla Lua's notation to declare anonymous functions, and for anything
more complex than a single operation, you **should** create a proper Lua function instead of using
this library's lambdas. But for extremely short functions, it's nice to have a shortcut, both for
coding quick and dirty examples and for readability.

### Defining lambdas

The way you declare a lambda with functional is with a simple function call:

```lua
triple = f.lambda "(x) => 3*x"
```

This syntax is quite similar to the Javascript one, where the parameter names are given between
parenthesis and there is an arrow separating the parameters from the body. The syntax also allows
for higher order lambdas easily by chaining `()=>` sections:

```lua
linear_combinator = f.lambda "(m) => (x, y) => m*x + y"
comb2 = linear_combinator(2)
print(comb2(3, 4)) --> 10
```

### Security concerns and limitations

Under the hood, `f.lambda` uses `load()` (or `loadstring()` in older versions of Lua).

**That means creating lambdas from unknown strings is a security risk.**

If you're hardcoding all lambdas, it *should* be fine. There are some checks done internally
before `load()` takes place, to prevent simple errors or naïve attacks. You can check the
documentation proper for the exact restrictions, but you shouldn't run into any of them with
simple functions.

### Environments

By default, a lambda function's environment is set to an empty table. That means it only has
access to its own parameters, and cannot read or write to globals or variables in the enclosing
scope of its creation. For example, the following lambda:

```lua
local constant = 3.14
local l = f.lambda "() => constant"
print(l()) --> nil
```

Will print out `nil`: `constant` is an undefined variable from the lambda's point of view, so
its value is `nil` since it was never assigned.

You can get around this and set the desired environment for a lambda as follows:

```lua
local constant = 3.14
local l = f.lambda("() => 3*con", {con=constant})
print(l()) --> 9.42
```

Or, as a shorthand:

```lua
local constant = 3.14
local l = f.lambda{"() => 3*con", con = constant}
print(l()) --> 9.42
```

Of course you could use (almost) any name you wish for the lambda environment variables.
In this case, we chose to distinguish the names for `con` and `constant` for the sake of clarity
for what is and isn't in the lambda scope.

## You don't need `:to_array()` (probably)
In most cases, unless you specifically need an array, you can use the iterator itself in a for
loop. For example, if we wanted to print all the names with "b", we could write:

```lua
local names_with_b = f.filter(names, has_b) -- note the lack of :to_array()
for name in names_with_b do
print(name)
end
```

This is preferred for a couple reasons: (1) it removes the need to allocate a new table which
would be later discarded; and (2) it postpones processing to when it actually needs to happen.

The second point is due to iterators being lazy, in the technical sense. They don't actually go
through their input and save whichever values they should return. Instead, whenever they're asked
for the next value, they process it and return it.

This also means, however, that an iterator can't rewind. If you need to iterate through the same
values twice, then maybe `:to_array()` is indeed the solution. If the sequence is too large
to be put into an array, you could instead `:clone()` the iterator. It will make a snapshot
of the iterator and all its dependencies, and return it as a new, independent iterator.

Yet another alternative is using the `:foreach()` operator: it applies the given function to
all elements in a sequence. Rewriting the above example using `:foreach()`:

```lua
local names_with_b = f.filter(names, has_b)
names_with_b:foreach(print)
```

Or simply:

```lua
f.filter(names, has_b):foreach(print)
```

## Examples
### Filter

Print all even numbers up to 10:
```lua
local is_even = function(v) return v % 2 == 0 end
f.range(1, 10) -- run through all the numbers from 1 to 10 (inclusive)
:filter(is_even) -- take only even numbers
:foreach(print) -- run print for every value individually
```

### Map

Fix capitalization on names:
```lua
local names = {"hellen", "oDYSseuS", "aChIlLeS", "PATROCLUS"}
local function fix_case(name)
return name:sub(1, 1):upper() .. name:sub(2):lower()
end

for name in f.map(names, fix_case) do
print("Fixed: ", name)
end
```

### Reduce

Sum all numbers in a range:
```lua
local add(acc, new)
return acc + new
end

local numbers = f.range(10, 120)
local sum = numbers:reduce(add, 0)
print(sum)
```

### Lambdas
#### Keep even numbers

```lua
local numbers = {2, 1, 3, 4, 7, 11, 18, 29}
local is_even = f.lambda "v % 2 == 0"
local even_numbers = f.filter(numbers, is_even):to_array()
```

Or you could inline the lambda, which is the more common approach:

```lua
local numbers = {2, 1, 3, 4, 7, 11, 18, 29}
local even_numbers = f.filter(numbers, f.lambda "v % 2 == 0"):to_array()
```

#### Get first element of each row

```lua
local matrix = {
{1, 2, 3}, -- first element of matrix
{4, 5, 6}, -- second element of matrix
{7, 8, 9} -- third element of matrix
}

-- map will iterate through each row, and the lambda
-- indexes each to retrieve the first element
local vals = f.map(matrix, f.lambda "v[1]"):to_array()
```

# Usage with Teal
**It is recommended that you read the Learning the Ropes section first so
you're not completely lost here.**

Due to the nature of Lua, Teal's types can be a bit too strict at times.
That's why Teal has casts. However, when you're dealing with functions,
casts can be quite verbose, and this is something the library is meant
to remedy!

For that, there are 3 aliases for common function types:
Alias|Explicit Teal type
:---:|:----------------:
`consumer`|`function(T)`
`producer`|`function(): T`
`mapping`|`function(U): V`

Short story long: consumers take in a value and return nothing; producers
take in nothing and produce a value; and mappings take a value and transform
it into another.

## A practical example

Let's say you want to print every third number up to 20, except if it's prime.

```lua
local is_prime = function(n: integer): boolean ... end
local my_numbers = f.range(20)
:every(3)
:filter(f.negate(is_prime))
```

So far, so good! `is_prime` is a `mapping` from integer to boolean, so negate
properly propagates the types and filter accepts it, since `range` produces
integers.

However, when we try to print the numbers:

```lua
my_numbers:foreach(print)
```

A type error! `print` is a consumer of `any`, but `foreach` expected a consumer
of integers! A simple cast will suffice though:

```lua
my_numbers:foreach(print as f.consumer)
```