-
Notifications
You must be signed in to change notification settings - Fork 4
Docs
This is the official documentation for the Scotch programming language. Visit our IRC channel on freenode at #scotch, or the development channel #scotch-dev.
- Hash objects
- "take" from infinite functions
- JIT compilation: after a module is parsed, a binary version is stored, resulting in a significant speedup
- New std libraries: std.decimal (accurate decimal arithmetic), std.fraction (fractional arithmetic), std.units (SI unit conversion), std.unit (simple unit testing)
- Operator overloading and commutative operator definition with <=>
- Take: "take 10 from [1..10000]" returns [1..10], etc
- Infinite lists: [1..], [2.., 2]
- List/string slicing: [1..100] @ [0..9] returns [1..10]
- Implemented a hash table for variable/function definitions to improve lookup time, increasing speed of recursive functions dramatically
- Fixed various parsing errors
- Significant whitespace, reducing the need for parentheses, semicolons, etc.
- Conversion to list with
list() - Infinite lists
The Scotch interpreter is available open source under the GNU General Public License (http://www.gnu.org/licenses/). This does not mean that any Scotch project must also be GPL licensed, however. The Scotch standard libraries are freely available to be used or modified.
All data types in Scotch are immutable; i.e., when "adding to a list," the original list is unchanged, and a new list is created.
null is a reserved word, representing nothing.
Two numeric types are supported by default in Scotch: integers and floats. Type conversion between these two types is automatic.
The following operations are defined:
- Addition:
2 + 1 - Subtraction:
2 - 1 - Multiplication:
2 * 1or2(1) - Division:
2 / 1 - Exponent:
2 ^ 2 - Remainder:
2 % 1or2 mod 1 - Equality:
2 == 2 - Greater than:
3 > 2 - Less than:
2 < 3 - Inequality:
1 != 2or1 not 2
Note that if highly accurate decimal calculations are needed, floats are not sufficient as they are imperfect approximate representations of numbers. This can produce unexpected results, like the following:
>> 0.2 + 0.4 == 0.6000000000000001
true
>> 0.2 + 0.4 == 0.6
false
You should use the decimal type defined in std.decimal or the fraction type defined in std.fraction instead; these types preserve accuracy.
The reserved words true and false represent the boolean values.
Boolean operations include and (or &), or (or |), and not.
Strings are denoted using 'single quotes' or "double quotes". The string
doesn't end at the end of the line, but at the closing quote, allowing for
multiline strings:
"Here's
an example
of a multi-
line string"
Strings can be added together: "a" + "b" == "ab". They can also be multiplied,
producing multiple copies: "abc" * 3 will produce "abcabcabc". When a number
is added to a string, the number is automatically converted to its string representation.
An empty string ('' or "") is considered equal to an empty list []. They
can be used interchangeably. Adding an empty list [] to a string makes no
change. A string can be thought of as a special instance of a list - a list of
characters.
Strings are lazily evaluated, meaning that operations such as
take 5 from "abc" * 1000000 will only evaluate the part of the string that
is needed, in this case the first 5 characters.
Lists are a fundamental type in any functional language. In Scotch, lists are immutable and can contain heterogeneous mixes of datatypes. Lists are designated with square brackets, like so:
[1, 2, 3, "a", "b", "c"]
List members are accessed with the @ operator: [1, 2] @ 0 returns the first
element, 1. Using the @ operator followed by a list will access several
members at once: [1, 2, 3] @ [0, 1] returns the first two elements of the
list, or [1, 2].
Lists can be multiplied, producing multiple copies; for example, [1,2] * 3
will produce [1,2,1,2,1,2].
Adding a single value to a list will add that value to the list. For example,
[1,2,3] + 4 == [1,2,3,4]
To add multiple values to a list, add two lists together. For example,
[1,2,3] + [4,5,6] == [1,2,3,4,5,6]
Importantly, there are no "tuples" in Scotch. A tuple is generally defined as an immutable list of heterogeneous datatypes; since Scotch lists fit this definition, lists can be used in place of tuples.
Lists are lazily evaluated, meaning elements will only be evaluated as they are needed.
Ranges can be generated automatically, using ..: [1..10] returns the
numbers from 1 to 10. Step size can also be defined: [2..10,2] returns every
2nd number. If no ending number is designated ([1..]), a lazily evaluated
infinite list is returned; this must be used in combination with take or it may
cause Scotch to hang when evaluated.
Hash tables are unordered collections of key-value pairs, allowing values to be looked up quickly by key. Hash tables use the following syntax:
hash = {1:1, 2:2, 'a':3, 'b':4, name="Bob", age=23}
These values can be looked up using the @ operator, i.e. hash @ 'name' or
hash @ 'b'.
A number, string, or even another hash can be used as a hash key; it is
important to note that the type will be converted to a string, so hash @ 1
and hash @ '1' will refer to the same value in the hash table.
To add to a hash table, add two hash tables together. For example,
{a=1, b=2} + {c=3} == {a=1, b=2, c=3}
Hash table values can also be replaced by addition, like so:
{a=1, b=2, c=3} + {b=5} == {a=1, b=5, c=3}
"Hash objects" can be created using qualified variable names. If the following variables are defined,
ben.name = "Ben"
ben.age = 23
the identifier ben (if not otherwise defined) will refer to a hash table:
{name="Ben", age=23}. This also works with imported modules; for example, after
importing the module std.math, hash tables will exist at both std and
std.math containing the imported definitions.
File objects are created like so: file("test.txt").
The following are examples of file operations:
open_file = file("test.txt")
read(open_file)
write(open_file, "blah blah")
append(open_file, "blah blah")
read(file) returns the contents of a file, as a string.
A proc is an imperative-style procedure, which evaluates expressions in
sequence. They are defined with the do keyword like this:
do print 1
print 2
print 3
It is important that the left sides of the expressions be lined up; this
designates what expressions should be part of the proc. The first expression
whose left side is farther to the left than the first proc expression (in this
case, print 1) ends the proc.
Procs can also be expressed inline, using semicolons to eliminate ambiguity:
do print 1; print 2; print 3
Using the thread keyword followed by a proc will execute the proc
concurrently, creating a lightweight thread.
A Scotch code file is called a module. Modules can be imported using the
import keyword, like so:
import std.math
import test as t
Periods in a module name designate directory structure, so std.math would be
found at std/math.sco or std/math/main.sco. The module will be searched for
first in the current directory, then in the scotch.lib directory.
When a module is imported, it is completely executed; any native variable/function definitions from that module are then added to the current definitions.
Definitions from a module are given a qualified name; for example, pi as
defined in std.math would be std.math.pi. You can use any amount of
specificity to refer to this variable (pi, math.pi, or std.math.pi). If
you define a new variable pi after importing std.math, pi will refer to
that new variable, while math.pi and std.math.pi still point to the
definition from std.math.
You can change the qualified name using as: import std.math as m
In Scotch, expression rewriting rules can be defined using both lazy and eager strategies.
a = b + 1
This means that in expressions using a, the a will be substituted with
b + 1. As the value of b changes, the value of a will also change.
a := b + 1
This defines a to be the value of b + 1 at the time it was defined. So, if
b changes after this definition, a will remain the same.
a + b where a = 1, b := 2
where creates a temporary variable definition, valid only for the preceding
expression.
Functions and data constructors (which are identical in Scotch) are defined like this:
f(n) = n * 10
f(a, b) = a + b
f(0) = "The argument is zero!"
The third line is an example of pattern matching. Only when function f is
called with one argument, 0, will it return this definition.
len(head:tail) = 1 + len(tail)
len([]) = 0
This is a recursive list (or string) length function. The first definition will
match either a list or a string; head will be defined as the first element or
character, and tail will be defined as everything else. The function will be
called again on everything but the first element, over and over, until it is
called on an empty list (or string).
f(dog(d)) = "A dog named " + d
This function definition will apply only when f is called with the value
dog(something). The value following the atom Dog will
be set as d.
Functions, data constructors, or other expressions are "called" using parentheses around comma-delimited arguments:
f(1, 2)
f(1)(2)
(x, y -> x + y)(1, 2)
Functions can be passed as arguments to other functions. For example, with a
function prime that returns true if a number is prime,
filter(prime, [1..100])
will return all prime numbers from 1 to 100.
"Partially applied functions" are functions that will be called with additional arguments in the future. For example:
add(x, y) = x + y
apply(f, x) = f(x)
apply(add(10), 5)
add(10) is undefined, because there is no definition of add that takes
only one argument. But when add(10) is called with another argument, 5, the
result is 10 + 5, or 15.
Any arbitrary expression can be given a definition. For example:
a ** b = a ^ b
We've just defined the (previously undefined) ** operator as equal to the exponent operator.
Addition between apples and bananas is also undefined. We can define this operation like so:
apple(a) + banana(b) = strawberry(a + b)
Now, the expression apple(1) + banana(2) will evaluate to strawberry(3).
Anonymous functions are expressed like this:
(x, y) -> x + y
This sample represents an anonymous addition function. It takes two arguments,
x and y, and returns x + y.
These functions can be used in place of regularly defined functions. For
example, reduce(x, y -> x + y, [1..10], 0) will sum all numbers from 1 to 10.
skip does nothing.
if true then "yes" else "no"
Case expressions employ pattern matching similar to functions.
case v of
apple(a) -> a,
banana(b) -> b,
otherwise -> otherwise
In this example, if v was Apple 1, it would match the first case; if it
is neither an Apple nor a Banana, it will match to otherwise (which is really
just a variable identifier, so the value becomes bound to the variable called
otherwise when the expression is evaluated.)
take is used to take the first n elements from a list. For example,
take 10 from [1..]
will return the first 10 elements in the infinite list [1..].
For lists, take will not evaluate the entire list, but only the first n
elements that it needs, so take 10 from [1..100] and
take 10 from [1..100000000] require the same amount of time.
Certain types of functions can also be used in combination with take: either functions that produce an infinite list, or recursive functions that construct a list by adding infinitely many values, such as:
infinite_range(n) = [n] + infinite_range(n+1)
Values can be explicitly converted to an integer, float, string, or list. For example:
str(1)
float(1)
int(1.2)
int('1')
The following types can be converted to a list with list(value):
- Lists (returns the list, unchanged)
- Strings (each character)
- Hash tables (treated as an unordered list of [key, value])
- Files (iterates over the contents of the file as a string, line by line)
- Any other value: returns a single member list containing the value
print "Hello world!"
print "What's your name?"
name := input
print "Hello, " + name + "."
print is used to display output; input gets a line of input from stdin.
input is lazily evaluated, so the user will only be prompted for input
on evaluation.
Using term rewriting, the output of a specific data constructor can be customized:
(print dog(d)) = (print "A dog named " + d)
print dog("Max")
List comprehensions allow quick, easy construction of lists by iterating over a collection.
The pattern looks like this:
[for id1 in collection1, id2 in collection2, ...
expression,
condition1, condition2, ...]
Any value that can be converted to a list can be used as a collection.
The following example returns i * j for every i from 1 to 10 and every
j from 1 to 20, but only if i is more than 5 and j is less than 9:
[for i in [1..10], j in [1..20], i * j, i > 5, j < 9]
The std.lib module contains basic, common functions. This module is
automatically imported, so its definitions are always available.
Contains various useful mathematical functions.
Defines the Decimal data constructor, for high precision decimal arithmetic.
Decimals can be created using the decimal function, i.e. decimal("0.501").
Decimal arithmetic:
decimal("0.5") + decimal("0.1") == decimal("0.6")
Defines the Fraction data constructor, for fractional arithmetic. Fractions
can be created using the fraction function, i.e. fraction("11/32").
Fraction arithmetic:
fraction("1/6") + fraction("1/7") == fraction("13/42")
SI unit conversion. Data constructors representing unit magnitude, i.e.
Kilo 1 or Deci 2, can be converted to and from other SI units like so:
convert_unit(Mega 2, to_kilo) == Kilo 2000.0
A very simple unit testing framework.
No formal specification of the Scotch language exists; the Scotch interpreter represents the de facto language standard.
Use scotch [module] [flags] to run the interpreter. If a module name is
specified, that module will be interpreted.
Flags:
- -v, --verbose: verbose mode; shows the step-by-step process used to evaluate an expression
- -e, --eval: the [module] argument is treated as scotch code and evaluated
- -i, --interpret: launch the interpreter after interpreting a model or evaluating code using --eval
- -s, --strict: interpret in strict mode, which forbids certain weak operations (such as string + number)