-
Notifications
You must be signed in to change notification settings - Fork 4
Docs
This is the official documentation for the Scotch programming language.
[TOC]
- 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()
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 * 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
An operation between an integer and a float will be converted to a float.
Operations between only integers, for example, 3 / 2, will produce an integer
result; in this case, 1 instead of the expected 1.5. To get around this,
use 3 / 2.0 instead; the result will be expressed as a float.
Note that if highly accurate decimal calculations are needed, floats are not sufficient as they are imperfect floating point 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"
When a number is added to a string, the number is automatically converted to
its string representation; adding an empty list [] to a string makes no
change. Strings can also be added together: "a" + "b" == "ab"
An empty string ('' or "") is considered equal to an empty list []. They
can be used interchangeably.
Strings can be multiplied, producing multiple copies; for example, "abc" * 3
will produce "abcabcabc".
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.
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..]), an infinite list is
returned; this must be used in combination with take or it will
never end.
See also list comprehensions.
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'.
Any datatype 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}
File objects are created with angled brackets around a filename (as a string)
like so: <"test.txt">.
The following are examples of file operations:
file = <"test.txt">
read(file)
write(file, "blah blah")
append(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.
The function std.lib.execute evaluates each expression in a list. Passing a
list of procs will evaluate each proc in sequence.
Using the thread keyword followed by a proc will execute the proc
concurrently, creating a lightweight thread.
Custom data constructors can be used without being defined. A data constructor is an atom (an identifier in CamelCase) followed by either a single value or a comma-separated list of values, surrounded by parentheses.
Here are some examples:
Dog "Max"
Cat "Sandy"
Decimal(10, 5)
Person("Sam", 15, [1,2,3])
A Scotch code file is called a module. Modules can be imported using the
import keyword, like so:
import std.math
import test
Periods in a module name designate directory structure, so std.math would be
found at std/math.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
Scotch defines variables 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.
f(n) = n * 10
f(a, b) = a + b
f(0) = "The argument is zero!"
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) (a data constructor). The value
following the atom Dog will be set as d.
Named function call:
f(1, 2)
Lambda call (call an expression representing either an anonymous function or a partially applied function):
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) would normally not be a valid function call 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.
Custom binary operators can be defined as if they were functions. For example:
a ** b = a ^ b
We've just defined the (previously undefined) ** operator as another exponent operator.
Addition between Apples and Bananas is undefined and will result in an exception. 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.
Commutative operators can be defined using <=> so that redundant function
definitions aren't necessary.
Apple a + Banana b <=> Strawberry (a + b)
This example will match both (Apple 1) + (Banana 2) and
(Banana 2) + (Apple 1).
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.
For functions, currently the entire function is evaluated before take. This means that infinite functions are still impossible.
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 + "."
The print statement actually calls the function show on whatever it's
displaying. This allows you to define custom instances of show:
show(Dog d) = "A dog named " + d
print Dog "Max"
List comprehensions allow quick, easy construction of lists by iterating over a collection.
[for i in [1..10], j in [1..20], i * j, i > 5, j < 9]
This list comprehension 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.
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 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.
Simply run scotch to enter interactive mode. Entering expressions into the
interpreter will cause them to be immediately evaluated, and the resulting
value (if any) will be displayed.
Variable/function definitions and module imports can also be used and will persist until the end of the session.
Running scotch modulename or scotch file.sco will execute a Scotch module.
The -i flag will execute the module, then enter interactive mode.
Running the interpreter with the -v flag will run it in verbose mode. In this
mode, the AST representation of every expression is output before the
expression is evaluated.
Typing -v during interactive mode will toggle verbose mode.
Use scotch "1 + 1" -e to evaluate a Scotch expression. Note that most
evaluations do not produce output; you'll need to use a print statement to
display the results.
This can also be used with the -i flag to continue in interactive mode after
evaluating the expression.