Skip to content
bendmorris edited this page May 15, 2011 · 22 revisions

Scotch Programming Language

This is the official documentation for the Scotch programming language. Visit our IRC channel on freenode at #scotch, or the development channel #scotch-dev.

What's New: Version 0.3.1 (development)

  • Hash objects
  • "take" from infinite functions

What's New: Version 0.3.0

  • 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

License

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.

Data Types

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

null is a reserved word, representing nothing.

Numeric data

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 or 2(1)
  • Division: 2 / 1
  • Exponent: 2 ^ 2
  • Remainder: 2 % 1 or 2 mod 1
  • Equality: 2 == 2
  • Greater than: 3 > 2
  • Less than: 2 < 3
  • Inequality: 1 != 2 or 1 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.

Boolean values

The reserved words true and false represent the boolean values.

Boolean operations include and (or &), or (or |), and not.

Strings

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

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

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.

Files

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.

Procs

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

Threads

Using the thread keyword followed by a proc will execute the proc concurrently, creating a lightweight thread.

Modules

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

Term rewriting

In Scotch, expression rewriting rules can be defined using both lazy and eager strategies.

Lazy definition

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.

Eager definition

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.

Temporary definition

a + b where a = 1, b := 2

where creates a temporary variable definition, valid only for the preceding expression.

Function definition

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.

Calling a function

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)

Higher order functions/partial application

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.

Advanced expression definitions

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

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.

Other Expressions

Skip

skip does nothing.

If

if true then "yes" else "no"

Case

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

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)

Value conversion

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

Input/output

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 comprehension

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]

Standard Library

std.lib

The std.lib module contains basic, common functions. This module is automatically imported, so its definitions are always available.

std.math

Contains various useful mathematical functions.

std.decimal

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")

std.fraction

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")

std.units

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

std.unit

A very simple unit testing framework.

The Scotch Interpreter

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)
Clone this wiki locally
You can’t perform that action at this time.