Overview
Copper is a simple imperative language statically typed with type inference and genericity.
- Simple Example
- Literals and Symbols
- Multiple Return Values
- OOP Style
- Block Closures
- Genericity
- Variadic Arguments
- Order of Definitions Is Not Significant
Simple Example
const Integer = Int32 struct Point var x : Integer var y : Integer function initialize (x: Integer, y: Integer) self x = x self y = y return self end function translate (dx: Integer, dy: Integer) self x += dx self y += dy end end function main var pt : Point pt initialize (3, 5) pt translate (1, 0) end
Literals and Symbols
Copper 3.0 changes the way it resolves literals and introduce symbols.
Copper has many kind of integers (signed/unsigned, 8/16/32/64 bits) and strings (8 or 16 bits). Before 3.0, integer literals were just 32 bit signed integers with automatic conversion when needed and there were two different syntaxes for 8 and 16 bit character strings.
Now the type of literal depends on the expected type where the expression is evaluated. For instance in a function call argument:
function f (x: Int32) ... function g (x: Unsigned8) ... f (12) // 12 is a 32 bit signed integer g (12) // 12 is a 8 bit unsigned integer function MessageBoxA (msg: *[]Unsigned8) // ASCII string function MessageBoxW (msg: *[]Unsigned16) // UTF-16 string MessageBoxA ("hello world") // The string is encoded with 8 bit characters MessageBoxW ("hello world") // The string is encoded with 16 bit characters
It becomes more interesting with symbols.
Symbols are just identifiers to constants starting with a quote, but they are resolved in the namespace of the expected type. The typical use is the enumeration values:
stype MouseButton 'left // 0 'middle // 1 'right // 2 endAs symbols are defined in the namespace of the type, it's possible to define another enumeration with same symbols:
stype Alignment 'left // 0 'right // 1 'center // 2 'justify // 3 endNow, depending on the expected type, the compiler resolves the symbol diffently
var button : MouseButton var align : Alignment button == 'right // 'right is evaluated to 2 align == 'right // 'right is evaluated to 1
It greatly reduces the number of identifiers in the global namespace and reduces the length of identifiers: no need of prefixes or uppercase, no need to specify the type explicitely (except where no type is expected). The source code is less verbose.
Symbols are not limited to enumerations, they can be used in structures and sub-types as well.
stype LineNumber : Int32 'first = 0 'last = -1 // special value ... struct LinkList var first : *Item var last : *Item 'empty = { nil, nil } // An empty link list ...
Actually, the combination of sub-types and literals+symbols makes the character type useless in the language: Copper 3 does not have any builtin character type, it is defined in the library. See How the character type is defined.
Multiple Return Values
function sort (x, y) if x > y return y, x else return x, y end end function main var min, max = sort (51, 15) end
Returning multiple values is often useful to return an error code and a value:
var error, file = open (...)
This way you don't need to use a special value to say that there is an error and retrieving the error in a second call forcing the callee to store a state.
Another advantage is to swap values, no need to use a temporary variable:
x, y = y, x
OOP Style
The language is not object oriented but it provides some features to facilitate object oriented designs.
- Type inheritance.
- Contextual functions (methods).
- Create kind-of objects by embedding everything needed inside a type definition.
struct Point var x : Int32 var y : Int32 meta function new (x, y) var pt = Point allocate // Allocate a point on the heap pt x = x pt y = y return pt end function resize (factor) self x += scale (self x, factor) self y += scale (self y, factor) end static function scale (value, factor) return value * factor end end function test (x, y, f) var pt = Point new (x, y) pt resize (f) end
- It creates a new function available from the Point meta-type.
- It creates a resize (self, x, y) function that applies only when the first argument is a pointer to a Point.
- It creates a scale regular function this is visible only inside the Point structure so it does not need to have a long name with a prefix.
Block Closures
A block closure is an additional argument passed to a function. The function can evaluate this block using the yield statement.
// Iterates between start and limit function range (start: Integer, limit: Integer) var i = start var n = limit while i <= n yield i // Call the block with i as parameter i ++ end end function main var sum = Integer 0 // Call the range function with start, limit and the block range (1, 10) do x sum += x end return sum end
Block closures provides a better abstraction: they allow to enumerate sequence's elements without knowing the implementation. It also reduces the code: an enumeration is done with one operation instead of the three usual steps (initialization, test end of iteration, advance to next element). Closures are very efficient, they are implemented as inline functions, there is no overhead at execution.
This system is very simple and flexible:
- Very concise syntax
range (1, 10) do x ... end
- Not limited to a single variable
dictionary eachKeyAndValue do key, value list eachIndexAndValue do index, value
- It is not limited to iterators:
function readFile (name) var file = fopen (name, "r") if file notNil yield handle file fclose end end function main var filename = String "dummy.txt" readFile (filename) do file // Use file here end end
- An object can have multiple iterators
file eachLine do line file eachChar do char
Genericity
A limited genericity is implemented.
Optional Argument Types
The type is optional when declaring the arguments of a function.
function max (x, y) if x > y return x else return y end end
This function will work for any type of x and y as long as x > y is valid (the operators can be overloaded).
Parametric Structures
Structures can have parameters:
struct Vector (T) var items : *[] T // Pointer to an array of T var size : Size ... end
Passing Types as Arguments
Types can be passed as regular arguments.
function new (T) var obj = malloc (T size) : *T return obj end function test var point = new (Point) end
As types are intended for the compiler only, they are removed from the signature when exported Types are not real values as in some true object oriented languages such as Smalltalk or Ruby.
Variadic arguments
Functions can have a variable number of arguments.
function sum(...) var sum = Integer 0 each_extra do x sum += x end return sum end
Passing variadic arguments to another functions is really easy, the three dots represent a tuple:
function sum_plus_3(...) return sum(1, ..., 2) end
Variadic functions without type is very useful:
function in (x, ...) each_extra do value if x == value return true end end return false end
This function works with any type. Code will be generated for each different number and size of arguments.
The in function is a very interesting case as LLVM can optimize it very smartly:
if x in (1, 2, 5)
will first first translated by Copper with a function like this:
function in (x:Int32, a1: Int32, a2: Int32, a3: Int32) if x == a1 return true end if x == a2 return true end if x == a3 return true end return false end
and it will be then fully inlined and optimized by LLVM to something like this:
if bit_test (0b100110, x)
i.e. testing the bit number x of 0b100110 which is done with a single machine instruction.
Order of Definitions Is Not Significant
No need of forward declarations, you can group definitions logically, not in an order forced by the limitations of the compiler. Even imports can be located anywhere in the source file.
You can write for instance your program in a top down approach: write main first, then sub-function, then sub-sub-functions, ...