Forwards

From Esolang
Jump to navigation Jump to search
Forwards
Paradigm(s) Functional
Designed by User:Sλλλ1(210)
Appeared in 2021
Computational class Turing complete
Reference implementation Unimplemented
File extension(s) .txt

Forwards is an esolang created by Andrew Phillips in 2021. Forwards is loosely inspired by Haskell, F#, and Dyalog APL, and is an attempt to write a functional language that is both short and expressive.

Feature Specification

Overview

In Forwards, everything is a function. 8 is the constant 8 function. * 2 is the multiply by 2 function. To define different sorts of things in Forwards, we first need to understand how Forwards understands functions.

To write a function, the coder uses the let clause. the syntax can take four variations, depending on whether the argument is marked, and whether the body is in an indented block.

// with argument, no block
let (x) foo <operands>: <expression>

// no argument, no block
let foo <operands>: <expression>

// with argument, block
let (x) foo <operands>:
    <expression>
    <subclauses>

// no argument, block
let foo <operands>:
    <expression>
    <subclauses>
  • The <expression> is the return value, in terms of the operands and argument (if bound).
  • The <operands> is a space-separated list of alphanumeric identifiers.
  • <subclauses> contain any amount of let clauses which further define terms in the <expression>.
  • The part of the definition from let to the colon is called the signature.
  • The expression after the colon is called the body.

There is also an inline version of the let clause, which is fn. This can be used to represent complicated anonymous functions. the syntax is much simpler, being inline only and requiring the argument specified:

fn arg <operands>: <expr>

In either structure, an input which is a list may be unpacked to a list in the argument.

[3, 4, 5] sum_three
let ([a, b, c]) sum_three: a + b + c
// or
[3, 4, 5] (fn [a, b, c]: a + b + c)

Rank

The rank of a function is a list (in braces) of the ranks of its operands. a constant function such as 4 takes no operands, so has rank {}, or rank 0. Every completed function (or lambda) has rank 0, including complicated ones such as + 2, ~ / 3, or !! 0 +.

If rank 0 is {}, denoting a function without operands, then a rank n function is one of rank {n-1}, taking one operand of lower rank. For example, ! has rank 2, or equivalently rank {{{}}}.

tokens are grouped and evaluated using ranks. In order for a function to accept the token on its right as an operand, the token's rank must match the first item of the function's rank. consider the different combinations (where square brackets denote the rank):

+[1] 7[0] => (+7)[0]
just the ranks: 1 0 == {0} 0 => {} == 0

~[1] *[1] 7[0] => ~[1] (*7)[0] => (~*3)[0]
1 1 0 == 1 {0} 0 => 1 {} == {0} 0 => {} == 0

![2] +[1] => (!+)[0]
2 1 == {1} 1 => {} == 0

?[{1 0}] <[1] => (?<)[{0}] == (?<)[1]
{1 0} 1 => {0} == 1

![2] ?[{1 0}] <[1] => ![2] (?<)[1] => (!?<)[0]
2 {1 0} 1 => 2 {0} == {1} 1 => {} == 0

If the compiler is unable to reduce the program to a rank 0 function, it will raise a RANK ERROR (as in the case of ?<).

When writing functions, the rank can be specified on the function name, or any operand. The function below will throw a RANK ERROR if the first operand is not rank 0 (a lambda).

let getconst x{}: ...

Similarly, the below functions will throw RANK ERROR if the rank of any operands are wrong, or if the amount of operands is wrong.

let foo{1 2} x y: x y // does not compile: x (rank 1) does not accept operand y (rank 2).
let bar{} a: 3 // does not compile: bar is not rank 0. the marking is improper.
fn{1 0} f x: f x // on an anonymous function, put the rank after 'fn'.

One special function to note is \, called Elevate. It turns a rank 0 function into a rank 1 function, for use as an operator in another function. Its definition is approximately:

let \ x _ : x
?=4 2 // compiles, but does not work as intended:
// will check if input is 4, and if not, changes the input to 4,
// but in any case the value is immediately replaced by a constant 2.

?\=4 2 // works as intended:
// check if input is 4, and if not, returns 2 (otherwise returns the input (4)).

Composition of Lambdas

Rank 0 functions, that is, functions without operators, go by many names in this document, including lambda. It is called so because it takes a lambda as argument and returns a lambda as output. A special case of lambda is the value: a constant lambda which returns itself, and which represents a value such as 5.

A Forwards program consists entirely of lambdas. The program ?> will not compile, as it is not a lambda (rather, it is rank 1). When a program is a string of lambdas, it performs each operation at a time, left-to-right (or forwards). In other words, a length of lambdas reduces to a single lambda which is the composition of its components.

The arithmetic functions are functions that act deterministically on values. If a program consists of a value followed by a length of arithmetic functions, it is always equivalent to a single value. (These are as opposed to nonarithmetic functions, which include side-effecting code, like IO, or nondeterministic functions, like RNG.)

5 + 3 / 2 ** 2 is the same thing as 16
because for all values of x, x (5 + 3 / 2 ** 2) gives the same result as x 16.

This reasoning allows compilers to reduce arithmetic compositions into shorter forms.

Abstract Values

Arithmetic functions expect their input and argument in specific forms. The program + "hello" will produce an ARITHMETIC ERROR thrown by +, because it expects a number for its operand. However, some lambdas that are not strictly values can be accepted as abstract values. Because values and lambdas are both rank 0, there is no compiler error thrown by the code * + 2. The diagram below shows how this function can be applied to an argument:

3 * + 2 => 3 * (3 + 2) => 15
4 * + 2 => 4 * (4 + 2) => 24
5 * + 2 => 5 * (5 + 2) => 35

The abstract value derives its value from modification of the argument. In reality, the same approach works for arithmetic values, if one recalls that these values are also constant lambdas:

3 * 6 => 3 * (3 6) => 3 * (6) => 18

The idea of abstraction is easy to apply to function definitions. One could write:

let (q) foo x_:
    <expression containing x>
    let x: q x_

And the operand x_ represents an abstract value, which resolves to x. There is a shortcut, however, using asterisks. The below code is identical to the above:

let foo *x: <expression containing x>

In the reference, there are many arithmetic functions whose signatures contain abstract values. These indicate that while the functions expect arithmetic values such as 5, they will also allow abstract values that return the proper type.

One special function to note is $, called Fork. Fork uses two abstract values to distribute the argument across. It is useful for writing point-free style functions.

let (x) square_minus_half: x ** 2 - (x / 2)
let square_minus_half_fork: $ ** 2 - / 2

Reference

Arithmetic Built-ins

This list only contains the built-ins from the main module of Forwards. More case-specific functions can be imported using use <module name> in the head of the program.

Arithmetic Built-ins
Name Signature and Description
Add (+) let (x) + *y:
Subtract (-) let (x) - *y:
Multiply (*) let (x) * *y:
Divide (/) let (x) / *y:
Power (**) let (x) ** *y:
Modulus (%) let (x) % *y:
Returns the sum/difference/product/quotient/power/remainder of x and y.
GT (>) let (x) > *y:
LT (<) let (x) < *y:
GTE (>=) let (x) >= *y:
LTE (<=) let (x) <= *y:
EQ (=) let (x) = *y:
neq let (x) neq *y:
Compares x and y. Returns 1 for true, 0 for false.
not let (b) not:
Returns the negation of the logical value.
and let (a) and *b:
or let (a) or *b:
nand let (a) nand *b:
nor let (a) nor *b:
xor let (a) xor *b:
Performs the specified logical operation.
Index (.) let (ls) . *i:
of let (i) of *ls:
Extracts the ith value of ls
Catenate (++) let (ls) ++ *ls2:
Joins two lists by catenation.
zip let (ls) zip *ls2:
Zips two lists into a list of length-2 lists. Discards unmatched values if either list is too long.
zipwith let (ls) zipwith{1 0} f *ls2:
Creates a list where the nth element is the application of f on the nth values of ls and ls2.
Map (~) let (ls) map{1} f:
Applies f to each element of ls.
Accumulate (!!) let (ls) !!{0 1} *c f:
Take a list of form [x1, x2, x3, ...] and return c f x1 f x2 f x3 ...; that is, modify the accumulator c by each term in the list.
Reduce (!) let (ls) !{1} f:
Insert the function f "between" each value in ls. A special form of Accumulate where the accumulator starts as the first list item.
filter let (ls) filter test:
Returns a list of the elements of ls that pass the test.
reverse let (ls) reverse:
Reverses the order of list elements
Choose (?) let (x) ?{1 0} test *y:
If x test y is true (nonzero) return x. Otherwise return y.
Elevate (\) let \{0 0} f _ :
\f represents a rank 1 function which ignores its operand and calls f on its argument.
Commute (^) let (y) ^{1 0} f *x:
Takes an argument and an operand and calls the function with the order switched (x f y).
Fork ($) let ${0 1 0} *x f *y:
Distributes the argument into two abstract values, then combines them with f.

Nonarithmetic Built-ins

This list only contains the built-ins from the main module of Forwards. More case-specific functions can be imported using use <module name> in the head of the program.

Nonarithmetic Built-ins
Name Signature and Description
print let (x) print:
println let (x) println:
Prints the value to stdout with/without a newline
input let input:
Returns the next character from stdin
random let random:
Returns a random float in the interval [0, 1)
randint let randint *x:
Returns a random int between 0 and x, inclusive
randelem let (ls) randelem:
Returns a random element of ls

Areas for Expansion

There are many weird aspects of Forwards, some of which might be able to be remedied without breaking the language concept. Two of these are its lack of truly higher order functions and its lack of a type system.

Without changing the language, however, there are several modules which should be created if Forwards were to be of any practical use.

  • A Math module containing a plethora of mathematical functions and constants
  • A String module which allows for ease of string formatting
  • A List module which contains more list functions such as sort
  • A File or Sys module which allows manipulation of the filesystem