Quick Tour
This quick tour will provide you with a brief overview of the basics of MindScript.
At its core, MindScript is a very simple programming language for processing JSON objects using a combination of traditional programming constructs and LLM processing. It has only few language constructs that can be easily picked up in a single session.
Oracles and Annotations
The main novelty of MindScript is its seamless integration with LLMs. Two essential elements of MindScript are oracles and annotations. Oracles are functions performed by the LLM, while annotations serve as hints for the LLM to do its job. They can be used in tandem to combine natural language with formal expressions.
Take for example the following annotation (which can be seen as a comment in the code), and the subsequent declaration of an oracle (which can be thought of as a regular function carried out by the LLM):
# Write the name of an important researcher in the given field.
let researcher = oracle(field: Str) -> {name: Str}
This creates an anonymous oracle based on the information provided by the annotation, which guides the generation of the output. As can be seen in the code, MindScript is typed: the input to the oracle is the value named field
of type Str
(a string), and the output is an object of type {name: Str}
, which implies that the oracle is of type Str -> {name: Str}
.
The annotation Write the name ... in the given field
is the informal type of the function, which conveys its semantics by a natural-language specification (e.g. a comment) describing its intended behavior. (Under the hood, informal types get added as an instruction to the LLM prompt.)
We can now use the oracle as if it were a function:
> researcher("physics")
{"name" : "Albert Einstein"}
> researcher("biology")
{"name": "Charles Darwin"}
Let's take a quick dive into these and other nice features of this language.
Expressions
Another important aspect of MindScript is that everything is an expression, that is, every construct produces a value. For instance, all of the following expressions evaluate to 42
:
This is particularly relevant in the context of functions and control structures, where any expression (including break
and continue
"statements") return values (for an example, see below).
Types
Just like JavaScript or Python, MindScript is dynamically typed: only the values have a type, not the variables.
This defines a variable named greeting
containing a value Hello, world!
of type Str
.
Builtin types
The primitive types are:
Null
, with a single valuenull
. Null values often indicate a missing value or an error.Bool
: a boolean value, eithertrue
orfalse
.Int
: an integer number, for instance42
or-100
.Num
: a floating-point number, such as42.0
or-1e2
.Str
: a string, delimited by either double or single quotes, as in"Hello, world!"
or'Hello, world!'
.Type
: the type of a type.
In addition, there are two container types:
- Arrays, as in
[1, 2, 3]
of type[Int]
. These are containers for a single type. Tuples do not exist in MindScript. - Objects, as in
{name: "Albert Einstein", age: 76}
of type{name: Str, age: Int}
.
There are also enumerated types, which can be created using Enum
and an exhaustive list of permitted values:
Function types are indicated using the ->
constructor. For instance cos(x: Num) -> Num
is of type Num -> Num
;
Finally, there are special types:
- The
Any
type acts as a wildcard for any type. - The nullabe types, indicated with a type followed by a question mark. For instance,
Str?
is either a string ornull
, whereasStr
can only be a string. - (Only for object fields) The mandatory object properties are indicated with
!
following the name of the key. For instance, in{name!: Str, age: Int}
, only thename
property is required, whereasage
can be ommitted.
Type aliases
New types are declared using the type
keyword followed by a type expression:
let Speed = type Num
let Hobbies = type [Str]
let Person = type {
name!: Str,
email!: Str?,
age: Int,
hobbies: [Str]
}
Speed
, Hobbies
, and Person
. Note that custom types are only aliases of the underlying structure, not separate types. Hence two types with different names are equal if their structures match.
Once created, they can be used as a normal MindScript values of type Type
:
> typeOf(42)
Int
> typeOf(type Int)
Type
> typeOf(Person)
Type
> isSubtype({name: "Albert", email: null}, Person)
true
Arrays and objects
Arrays are created by listing its members
The elements of an array can be retrieved through indexing
You can push
, pop
, shift
, and unshift
elements into an array:
> let a = ["a", "b", "c"]
["a", "b", "c"]
> push(a, "new")
["a", "b", "c", "new"]
> pop(a)
"new"
> a
["a", "b", "c"]
> shift(a, "new")
["new", "a", "b", "c"]
> unshift(a)
"new"
> a
["a", "b", "c"]
You can extract slices from an array using slice
:
To create an object, just write key-value pairs enclosed by curly brackets:
The double-quotes for keys can be ommitted. The code below is equivalent to the previous one.Setting and retrieving properties can be done using the syntax the dot notation or by invoking the set
and get
functions:
> let person = {}
{}
> person.name = "Sarah"
"Sarah"
> person.name
"Sarah"
> person
{"name": "Sarah"}
> set(person, "name", "Jessica")
"Jessica"
> get(person, "name")
"Jessica"
> person
{"name": "Jessica"}
Annotations
As mentioned in the beginning, annotations are an essential component of MindScript: they constitute the informal type used for providing interpretation hints for an oracle.
Any value can be annotated with an explanatory comment using the #
operator, which attaches a string to the value of the next expression. For instance:
c
in the REPL, we get both its value and annotation:
Likewise, it is possible to annotate type expressions:
This is particularly useful to provide an oracle with semantic hints about inputs and outputs.Consider for example the annotations within the type Person
above. If an oracle is declared as receiving an argument of type Person
, the annotations help clarifying the meaning of the object's properties (see the example in the section on oracles below).
Important
Outside the context of oracles, annotations work as regular comments, i.e. they are relevant only for the human programmer.
Operators
Most of the usual operators are available and they have the expected precedence rules:
Types aren't cast automatically, and applying an operator to values having incompatible types will lead to runtime errors.
Functions
Functions are defined with the fun
keyword (see the oracle
keyword below). This declares an (anonymous) lambda expression. Functions can have one of more arguments and they can be typed.
Take for example, a simple function that sums two integers:
The body of the function is enclosed within ado ... end
bracket containing one or more expressions. If an explicit return(value)
is not provided, a function will simply return the last evaluated expression. This means that we can also implement the function as follows:
And, given that print
returns its argument, we can further simplify this as:
Currying
Let's have a look at the type of this function:
In typical programming languages it would be something like(Int, Int) -> Int
. MindScript is different. As in functional programming languages like Haskell, MindScript considers an argument list as a sequence of single-argument functions. This is called currying. Hence the type of the function is Int -> Int -> Int
, and it is valid to invoke sumints
with a single parameter:
The first evaluation sumints(3)
returns a new function of type Int -> Int
taking a single integer argument, where n
is bound to 3
, i.e. add3
adds 3 to its argument.
If the function is declared without an argument, then it is interpreted as Null
. If no return type is declared, it is interpreted as type Any
. Hence,
Null -> Any
. Let's play with this:
> sayHello()
Hello!
> typeOf(sayHello)
Null -> Any
> let greeting = sayHello()
Hello!
> typeOf(greeting)
Str
> print(greeting)
Hello!
sayHello
does not only print "Hello!" but also returns the corresponding string. How would this work without the print
statement? Try it out in the playground!
Oracles
Like functions, oracles produce outputs from inputs, but they do so using an LLM (or, technically, using the induction afforded by the LLM). As shown in the beginning, oracles are declared using the oracle
keyword and specified by all the annotations provided. Importantly, annotations that might be present in the types the oracle takes as arguments are also relevant. Take for instance the following annotated type:
let Person = type {
# The name of the person.
name!: Str,
# Date of birth in DD/MM/YYYY format.
dob!: Str
}
Thus, the oracle uses the annotation # Date of birth in DD/MM/YYYY format
associated to the formal expression dob!: Str
to come up with an answer:
Building Oracles using Examples
To help with the induction process one can also build the oracle
with examples. These are given using the from
keyword plus an
array containing the examples.
let examples = [
[0, "zero"], [1, "one"], [2, "two"],
[3, "three"], [4, "four"], [5, "five"]
]
let number2lang = oracle(number: Int) -> Str from examples
Then we can ask the oracle to evaluate new inputs.
Obviously, since oracles compute by guessing plausible outputs (i.e. performing inductive inference), these are not guaranteed to be correct as in the previous example.Each example must have the format
For instance,[3, 2, "five"]
is a valid example for a function of type Int -> Int -> Str
.
Control structures
There are only three control structures in MindScript:
- logical expressions
- conditional expressions
- for-loop expressions (there are no while loops)
In particular, unlike Python, Javascript, and most C-like languages, conditionals and loops are expressions that return a value.
Logical expressions
These expressions are built with the usual operators:
Important
Logical expressions are short-circuited, which means that as soon as their truth value is known, the remaining subexpressions are not evaluated.
For instance:
will only evaluate up to(2/2 == 1)
, omitting the evaluation of (2/3 == 2)
.
Conditional expressions
These expressions consist of a simple if ... then ... else ... end
block structure with the
familiar semantics:
if n == 1 then
print("The value is 1.")
elif n == 2 then
print("The value is 2.")
else
print("The value is unknown.")
end
null
otherwise.
For-loops
For-loops iterate over the outputs of an iterator it
, which is a "function" of type Null -> Any
that generates a sequence of values. The for loop will repeatedly call it()
until the latter returns a null
value. Take for example:
range
, which in this case generates
Other built-in iterators are natural
(= 1, 2, 3, ...) and natural0
(= 0, 1, 2, ...).
Iterators can also be created from arrays and dictionaries using the
iter(value: Any) -> Null -> Any
built-in function. Take for example:
Pseudo while-loops
An equivalent to a while loop can be constructed using an infinite iterator and a breaking condition. The following example uses the built-in iterator natural
, which iterates over all natural numbers (i.e., up to infinity):
# Return the Fibonacci series up to the Nth term
let fib_series = fun(N:Int) -> [Int] do
# First two Fibonacci values.
let output = [0, 1]
# First N Fibonacci values.
for k in natural() do
if k < N - 1 then
push(output, output[k - 1] + output[k])
else
break(null)
end
end
return(output)
end
return(output)
expression is redundant and can be ommitted, because the entire for-loop evaluates to the output
array.
Note that the execution flow of a for-loop can be modified through:
continue( expr )
, which evaluates toexpr
and initiates the next iteration, orbreak( expr )
, which evaluates toexpr
and exits the entire for-loop.
See more examples of pseudo while-loops implemented using the functions filter and map.
Destructuring
Destructuring assignment is a syntax that permits unpacking the members of an array or the properties of an object into distinct values.
After this assignment,x == 2
and y == -3
. The third element 1
gets ignored.
After this assignment, n == "Albert"
and e == "albert@einstein.org"
. The
property id
gets ignored.
These can be arbitrarily nested.
Standard Library
MindScript fires up with a set of built-in functions to operate on arrays, iterators, objects, strings, as well as a standard set of mathematical functions.
To obtain an object that shows all the variables defined, use getEnv
:
> getEnv()
{
"dirFun": obj:{} -> [Str],
"mute": _:Any -> Null,
"dir": obj:{} -> [Str],
"netImport": url:Str -> {},
"www": url:Str -> Str?,
"natural0": _:Null -> (Null -> Int?),
"natural": _:Null -> (Null -> Int?),
...
Importing modules
Modules are text files with MindScript code. It is customary to use the extension .ms
for MindScript code.
You can import a module using the import
function if the code is on your local filesystem, or using netImport
for remote modules.
For instance, try importing the language module lang.ms
provided with the standard library.
> let lang = import("ms/lib/lang.ms")
{
"write": instruction:Str -> {result: Str}?,
"similarity": text1:Str -> text2:Str -> Similarity?,
"similarityExamples": [
...
lang
object.
We can list the properties of a module (or any object for that matter) using dir
:
> dir(lang)
[
"write",
"similarity",
"similarityExamples",
"Similarity",
"keywordsExamples",
"keywords",
"coref",
...
> lang.keywords("JavaScript is a high-level, often just-in-time compiled language that conforms
to the ECMAScript standard.")
{
"keywords": [
"JavaScript",
"high-level",
"just-in-time compiled",
"language",
"ECMAScript standard"
]
}
To explore the standard library, just type the name of an object—the informal type annotation will provide information about what it does.