Actions

A programming language that combines features from functional and imperative languages. The main interesting feature is actions, which are intended as an alternative to Haskell's monads.

While many of my other esolangs are based on ideas that would clearly not be good in a non-esoteric language, this one I intended more as a proof-of-concept of something I thought might be able to be developed into a good idea, though in its current form many desirable language features are missing (like integer, string, and list literals).

Contents

Data and expressions

There are three types of data: functions, records, and actions.

Functions

Functions act a lot like in Haskell: put two functions next to each other to apply a function; functions are typically curried; and functions are pure. If an argument to a function is more than just a variable, it will often need to be enclosed in parentheses.

Lambda expressions and function definitions also have a similar syntax to Haskell. Function definitions have the form f x = x, and lambda expressions have the form \x. x. The dot may be omitted from lambda expressions if the first thing after it isn't a variable. The original version of the language required braces around the body of the lambda expression (\x {x}); it may be good to always use braces, since lambda expressions have high precedence (\x. x y is (\x. x) y, not \x (x y) as it would be in Haskell), and since it makes a bit more sense when dealing with actions.

Records

A record is a mapping from names to values. Records are constructed using brackets, with names and values separated by an equals sign and name/value pairs separated by semicolons (with an optional semicolon at the end). Example:

[id = \x. x; const = \x y. x; compose = \f g x {f (g x)}]

Record members may also have function parameters, though keep in mind the function is not bound to local scope, so you can't recurse like that. E.g., the following is equivalent to the previous example:

[id x = x; const x y = x; compose f g x = f (g x)]

The values can be omitted to capture variables from the current scope; e.g. [id; const; compose] is equivalent to [id = id; const = const; compose = compose].

Record values can be accessed with ., similar to C++, Java, etc. E.g., [id x = x].id is \x. x. . has the highest precedence.

If the interpreter tries to evaluate record.name but there isn't a value with the name name, but there's a value called GET, then the interpreter calls record.GET \x {x.name}. If there's no value named GET either, then the interpreter attempts to return record.DEFAULT (without calling it), and if that fails, it's an error.

Symbols

There isn't a symbol or enum type; however, they can be simulated by using a function that selects a specific member from a record. There is a special syntax for creating such functions: #something is equivalent to \x. x.something.

The equivalent of a switch/case statement is to simply apply the symbol to a record:

color [
	red = [r=1; g=0; b=0];
	green = [r=0; g=1; b=0];
	blue = [r=0; g=0; b=1];
	DEFAULT = [r=0; g=0; b=0];
]

Booleans are represented by #true and #false.

Other types

There is some support for natural numbers and lists in stdlib.actions. See that file for details.

'Let' statements

Braces are the equivalent to Haskell's let statement. Inside the braces are variable and function declarations separated by semicolons; the last item in the statement is the return value. Items in the statement can be mutually recursive. Variables already in scope cannot be redeclared. There is no pattern matching. Example:

{
	length l = l.null [true = Nat.0; false = (length l.rest).succ];
	length getSomeList
}

Actions

Actions are a type, separate from functions. Like in Haskell, actions take no arguments (but there can be functions that take arguments and return actions) and return some value. If no return value is needed, they can return the empty record. To use an action, a function must have access to that action (e.g., as an argument), and it must return an action; any function that doesn't do that is pure. However, functions can create temporary actions to use internal state, similar to using various non-IO monads in Haskell (in fact, I believe these actions are equivalent to Haskell monads in what they can do).

Using actions

A function that uses an action will usually be passed that action as an argument. The function can then execute that action using the ! operator, which is placed after the action name. Unlike in Haskell, actions can be composed with other functions without being placed on a separate line, e.g. set (f (get!))!. Actions are executed left-to-right.

Braces have some additional uses regarding actions. They determine the sequence in which actions are performed, and are sort of like Haskell's do statement; actions may be mixed in with pure declarations (though recursion is not allowed to go past an action). They also quote actions: the result of { less (x.get!) 10 } is an action (and x.get! will be executed when the action executes), whereas less (x.get!) 10 retrieves the value of x right away and returns whatever less returns (presumably a boolean).

Note that lambda expressions do not quote actions; \x (v.get!) will retrieve the value of v immediately, rather than when the function is called. \x (x.get!) is an error, since the value of x is not known yet. This is why I recommended braces above; \x {x.get!} works as expected.

If ! is used on something that's not an action, it just returns it. This means that you can have something that conditionally returns an action; however, it also means that directly returning an action from an action won't always have the expected effect. Furthermore, if no ! expression is encountered that is actually an action, the result of the expression will not be an action.

Local actions

There are a few functions in stdlib.action (and others can be defined) that create local actions. For instance, var is equivalent to Haskell's State monad, and try is sort of like Maybe. They typically take a function as an argument, which takes an action or a record containing actions and uses those actions. These actions can be used as normal. However, they cannot be used outside the function that you pass; doing otherwise results in an error.

Creating actions

Actions can be created by combining other actions with pure functions, of course. This includes control structures; for instance, while can be defined in the language. However, more interesting is how to create local actions.

Local actions can be created with PRIM.startAction. This takes one argument, which is a function that takes a function that takes an action constructor and returns the action to perform. If you pass a value to the action constructor, the action will be suspended and the value passed to the action constructor will be returned from startAction (wrapped in a record as described below).

startAction can return one of three things:

You do not need to call next exactly once for each record; you can choose not to call it to terminate the action (like try, or the Maybe monad in Haskell) or you can call it multiple times (like the list monad in Haskell).

IO actions

A record containing IO actions is passed to the main program. Currently the only function is print, which takes a list of natural numbers, interprets those numbers as Unicode characters, and prints the result to the screen.

Modules and main program

Each file contains a single statement (often a let statement), preceded by zero or more import statements.

An import statement has the form + name, where name is a valid identifier. This will import the file name.actions from the current directory, evaluate it, and put the result into variable name.

The result of evaluating a module will usually be a record; this can be created by putting a semicolon-separated list of names to export in square brackets at the end of the module. The result of evaluating a program must be a function that takes an IO object and returns an action to perform.

A few extra features that are useful with modules:

Summary

Expressions

+ name expr
Import a file (only at top-level)
expr expr
Apply a function
expr !
Perform an action
\ args [.] expr
Lambda
name
Variable
expr . name
Record member access
# name
Record member access function (symbol)
{ declarations }
Declare local variables, perform sequential actions, quote actions
[ declarations ]
Create a record

Declarations

name [names] = expr
Declare a variable or a function
[name [names]] + names = expr
Unwrap a record
- name (in {})
Declare a name private
expr (in {})
Perform an action or return a result
name (in [])
Shortcut for name = name

Interpreter

Interpreter written in Haskell (updated 2020; source only; download and run; 25K compressed, 184 KiB uncompressed; also includes Data.Supply from here because I didn't want to have to deal with installing packages)

Original interpreter written in Haskell (not compatible with modern versions of GHC; source only; download and run; 21K compressed)

Note that the current directory matters when running the examples.