Skip to content

LanguageIntroduction

Scott Wisniewski edited this page Apr 1, 2012 · 44 revisions

Language Introduction

Immutability and Lazy Evaluation

Like Haskell, Eddie is a pure functional programming language. All local variables and fields declared in Eddie are immutable and may not be modified once they are set. Also, Eddie code uses non-strict, or lazy evaluation. Expressions in Eddie will not be evaluated until a value is necessary. For example, given this code:

function AllIntegers(int min) :: [T] 
{
    return min : AllIntegers(min + 1);
}
function Foo<T>(x :: [T]) :: (T, T) 
{
    return (x.Head, Last(x.Tail));
}
function Last<T>(x :: [T]) :: T {
    switch (x) {
        case [v]
            return v;
        case _ :: rest
	    reutrn Last(rest);
        default
            throw new ArgumentNullException("x");
}

A call to Foo(AllIntegers(0)).Item(0) will return the value 0, rather than overflow the stack.

Types declared in other CLR supported languages may, of course, be mutable. Eddie programs can consume them. Their use, however, must be wrapped inside the IO monad.

List Types

Eddie supports immutable singly linked list types. List types are declared by placing an element type name inside square braces:

x :: [int] = ...;

Elements can be appended to a list using the cons operator (:) like so

var y = 2 : x;

The empty list is always represented by the value null, thus the following would create a list of length 1:

var z = "Hello" : null;

Lists may also be created using list literal syntax:

var a = [1,2,3,4,5];

List types are covariant, thus the following code is legal (and will succeed at runtime):

[object] y = ["Hello", "World"]
var z = 1 : y;

Eddie also supports list comprehensions, using a syntax similar to Haskell:

var primePairs = [ (x, y) | x <- 0.., x.IsPrime(), y <- 0.., y.IsPrime()];

Tuple Types

Eddie supports tuple types. They are declared by placing a list of types inside parenthesis, like so (int, int). Tuple types must declare at least 2 elements. There are no single element tuple types (expressions of the form (x) are always classified as scalar values). Instances of tuples are constructed by placing a comma delimited list of values inside parens:

var x = (0, y, "Hello");

Like list types, tuple types are covariant.

var x :: (object, object) = ("Hello", "World");

Function Types

Function types are declared using the -> operator. Void returning functions, such as [char]->void are represented at runtime using one of the System.Action delegate types. Value returning functions such as int->[char] are represented using one of the System.Function delegate types. Multi-argument function types are specified by placing the list of types on the right hand side of the -> operator in parenthesis. For example the expression (int, int)->int always describes the type Func<int,int,int> rather than the type Func<Tuple<int, int>, int>. To specify a single argument function type taking a tuple as an argument, the tuple type must be surrounded in parenthesis (((int, int))->int).

Function types obey the "Liskov Substitution Principal". That is, they are contravariant in their argument types and covariant in their return types.

Function types that take no arguments may use void on the right-hand-side of the -> operator. For example void->void corresponds to the type System.Action, and void->int corresponds to the type System.Func<int>.

Named delegate types (other than System.Func<...> or System.Action<...>) are represented in Eddie using the name of the delegate type, not function type syntax.

Unlike C#, Eddie assigns a default type to lambda expressions where possible. For example, given the following:

var x = () => "Hello";

The type of x will be void->[char]. The Eddie compiler will allow any function type to be cast (via an explicit conversion) to and from a named delegate type with a compatible signature.

Classes

Being a citizen of the CLR, Eddie supports object oriented programing. Classes are defined using the class keyword, and have syntax similar to C#:

class Point3D
{
    m_x :: int;
    m_y :: int;
    m_z :: int;
    public Point3D(x :: int, y :: int, z :: int)
    {
        m_x = x;
        m_y = y;
        m_z = z;
    }
}

Fields and Constructors

Because Eddie is a pure functional programing language, all class fields are immutable. However, in order to properly suport object-oriented encapsulation fields may be explicitly initialized inside constructors, as show in the example above. However, any attempts to:

  1. Assign a field more than once.
  2. Read a field before it has been assigned to.

will result in compile-time errors. These restrictions are also enforced dynamically, so attempts to read a non-initialized field outside of a constructor will result in an error. Note that unlike C#, fields in Eddie do not have default values. Fields may also not be declared using the val modifier. To eagerly evaluate the values stored in fields you must use the eval operator.

Monads

Classes may declare one or more optional monad declarations, by specifying a list of type names before the class definition. They are used to indicate that instances of the type should be wrapped inside the provide monad. Similar to attributes, the suffix Monad may be omitted from the type. For more details see the section on Monad Comprehensions below.

By default, all classes are sealed and have either internal or private accessibility. To allow a class to be inherited it must be marked as either abstract or virtual. By default a virtual or abstract class can only be inherited by a class marked with the Pure attribute (that is a class written in Eddie code and not declared to be part of the IO monad). To allow a class to be overridden by any type (including types written in other CLR languages), it must be declared to be part of the IO monad:

//Can be overridden by code any language.
io abstract class Foo
{
}

//Can only be override by classes written in Eddie and
//not declared in the IO monad.
abstract class Bar
{
}

//A sealed type that cannot be inherited from.
class Baz
{
}

The "sealed by default" behavior is mainly driven by performance considerations. Enforcing that abstract or virtual types are overridden by pure types requires adding extra run time checks to a type's constructor. With sealed types those checks can be omitted.

All class members have a default accessibility of private. Eddie also supports the public, private, protected, internal, protected or internal, and protected and internal accessibility modifiers.

Eddie also supports static classes, mainly to allow exposing extension method libraries written in Eddie to other CLR languages.

##Identifiers

Eddie defines 2 kinds of identifiers: names and symbols. Names are defined using a similar lexical grammar to identifiers in C#. Symbols are a generalization of operator symbols, such as *, +=, etc. Symbols can be used to introduce new operators into the languge. Generally, names can be used in any syntactic context expecting an identifier whereas symbols may only be used in an expression context. However a symbol may be quoted using back quotes, allowing it to be used anywhere a name can. For example:

operator `&&`(l :: bool, r :: bool) :: bool
{
    if (!l) {
        return false;
    }
    else {
        return r;
    }
}

Please see LexicalGrammar for more details.

Functions

Functions are declared in Eddie using the function keyword. Unlike C#, the return type of a function is specified after the function's parameter list using the :: syntax. If a function does not declare a return type, then void is assumed. Eddie does not support Hindley-Milner type inference. Similar to classes, functions and parameter declarations may be preceded by a list of Monad declarations (see below).

function add(x:: int, y:: int) :: int
{
    return x + y;
}

io function WriteLine(msg :: [char])
{
   //...
}

Functions may be declared top-level, or as members of classes. Member functions may be declared static, virtual, abstract, or sealed. Virtual or abstract members, however, can only be declared inside a class also marked as virtual or abstract. Overriding members are declared using the overrides keyword. Top-level functions are implictly static and may not specify the static modifier or any inheritance modifiers (virtual, sealed, or abstract).

By default, member functions have private accessibility, and top-level functions have internal accessibility.

Extension Methods

Eddie also supports extension methods, using a syntax similar to C#. However, Eddie allows extension methods to be defined in any context where a function can be defined and is not limited to only top-level static classes.


enum ExpressionKind
{
    Add,
    Subtract,
    Multiply,
    Divide,
    Negate,
    Identifier
}

function IsBinary(this x :: ExpressionKind) :: bool
{
}

class Foo 
{
    static function IsUnary(this x :: ExpressionKind) :: bool
    {
    }

    static function Blaa(x :: ExpressionKind)
    {
        if (x.IsBinary()) {
            //...
        }
        else if (x.IsUnary()) {
            //...
        }
    }
}

To enable compatibility with external libraries, extension methods defined in top-level static classes are brought into scope (as extension methods) when the class's containing namespace is imported. Otherwise extension methods use normal scoping rules (the method can be invoked using extension method syntax any place the method is in scope). Also, because top-level functions in Eddie are emitted to metadata as members of static classes, non Eddie code can consume top-level extension methods.

Operators

Eddie allows custom operators to be defined using the operator keyword. The name of an operator can either be a name or a quoted symbol. Operators can be either unary or binary, and declare their precedence using the Eddie.Runtime.CompilerServices.PrecedenceAttribute attribute. Unary operators may be declared either prefix or postfix using the Eddie.Runtime.CompilerServices.PrefixAttribute or Eddie.Runtime.CompilerServices.PostfixAttribute.

[Precedence(12)]
operator `||` (left :: bool, right :: bool) :: bool
{
    if (left) {
        return true;
    }
    return right;
}

All binary operators are infix. In an expression context, operator symbols may be used without quotes. Given the definition above,the following code could be written:

if (x || y) {
}

Binary operators may also be declared as member access operators using the Eddie.Runtime.MemberAccessAttribute attribute. The second operand of a "member access operator" must be a unary function type. The first operand must be any type that is implicitly convertible to the lambda parameter type of the second argument. For example the following code can be used to define a "lifting nullable" version of the . operator:

[MemberAccess]
operator `?.`<T, R>(x :: T, y :: T->R) :: R where R : class
{
    if (x != null) {
        return y(x);
    }
    return null;
}

Interfaces

Algebraic data types

Like Haskell, Eddie supports algebraic data types. They provide a simple and concise definition of POD types for use in functional programs, and are defined using the case class syntax:

case class Maybe<T>
{
    Just(T Value);
    Nothing();
}

The constructors used for algebraic data types inject symbols into the class's containing namespace. For example, to create instances of the Maybe<T> type defined above you would write code like the following.

var x = Just(2);
var y = Nothing<int>();

The use of case classes, however, is somewhat limited. They may not declare members other than case constructors, and methods may only be defined on them using extension methods. Furthermore, they cannot be safely versioned. As a result, their use in externally visible types is heavily discouraged. Attempts to do so will result in warnings being issued by the compiler. They are, however, extremely effective as internal data structures used by functional programs.

Arguments to case constructors can be used to infer type parameters for the containing case class. This works similarly to generic method parameter inference except that the parameters may be associated with either the type, or the constructor. If a generic type parameter cannot be inferred from the constructor arguments, then the constructor invocation must supply them. Generic parameters associated with the case class must be listed before any parameters associated with the constructor. It is not possible to partially infer generic parameters. Either all of the parameters (including both class and constructor parameters) must be inferred or they all must be specified.

var x  = Just("Hello");
var y = Just<object>("Hello"); 

case class Foo<T>
{
    Bar<R>(R X);
};

var z = Bar<int, [char]>("2");

Each case constructor will introduce a nested sealed type (inside the case class) that derives from the containing case class. These types may be referred to in Eddie code by qualifying the constructor name with the name of the containing type. Each case constructor parameter then becomes a public readonly property on the associated nested class. For example:

var x = (o as Maybe<int>.Just).Value;
var y = o as Maybe<int>.Null;

New derived types from the case class may not be introduced, either by Eddie programs, or by external assemblies. This is enforced by the compiler by marking the constructor on the abstract base class generated for the algebraic data type as compiler controlled. Consequently, case classes may not be declared either abstract or virtual.

Case class constructors, however, may be marked either virtual or abstract, and one constructor may inherit from another. An abstract constructor cannot be invoked directly, but may be used in the declaration of derived constructors and in pattern-matching expressions. Here are a few simple examples:

namespace Syntax
{
    case class Expression
    {
        abstract BinaryExpression(Expression Left, Expression Right);
        Add(Expression Left, Expression Right) : BinaryExpression(Left, Right);
        Subtract(Expression Left,Expression Right): BinaryExpression(Left, Right);
        Constant<T>(T value);
        Identifier(Symbol s);
    }
}

By default all case constructors are sealed and may only be inherited from if they are marked virtual or abstract. Note that a case constructor may only be inherited from by a constructor declared in the case class. Case classes may not be inherited from, and may not specify either a base class nor implement any interfaces (they may, of course, conform to duck interfaces). Parameters in derived constructors with the same name as a corresponding parameter in a base constructor will introduce a property into the derived class that shadows the corresponding property in the base class.

Case classes insert a name into the scope that defines them for each declared constructor. Importing the namespace or type that contains the case class will also bring all of it's constructors into scope. Those names may also be accessed using the name of the qualifying scope. In the event of a conflict within a namespace, constructor names can be disambiguated using the name of the defining case class. The Eddie compiler will not perform overload resolution over constructor names. If a type name in an imported scope conflicts with a constructor name, it will shadow the declared constructor by name. If the compiler sees such a conflict at the point where the constructor is defined it will emit a warning. If the compiler encounters 2 constructors with the same name in scope at the same time, any non-qualified references to the constructor will result in a compiler error.

import Syntax;

Syntax.Add(Syntax.Expression.Constant(2), Constant(4));

Type-qualified constructor names are overloaded to refer to either the constructor name or the the name of the class that is generated for it. The symbol the name refers to depends on the context in which it is used. In any context requiring a type name, the type-qualified constructor name shall refer to a type. In all other contexts it shall refer to the constructor . It's use without arguments in such cases shall be classified as a method group. Note that these rules imply that static members or nested types of constructor classes cannot be accessed by Eddie code. This is ok, however, because constructor classes will not contain such members.

Imperative Return Syntax

Although Eddie is a pure functional programming language it eschews the tranditonal functional programing concept of implictly defined return values. Instead, keeping with it's C# roots it requires the programmer to "explictly declare his intent" for an expression to yield a function's return value using the return statement.

Syntatically it is similar to the "imperative return statement" used in most C-derived languages, and allows code like the following to be written:

IO function foo(x :: int) :: int
{
    if (x > 10) {
        if (x == 1) {
            return 1;
        }
        Console.WriteLine("Hello");
    }
    Console.WriteLine("World");
    return -1;
}

Despite this seemingly imperative feature, Eddie is still a "fundamentalist" functional programming language. The code above will behave as if it was written functionally:

IO function foo(x :: int) :: int
{
    var tmp = if (x > 10) {
        yield if (x == 1) {
            yield (true, 0);
        }
        else {
            Console.WriteLine("Hello");
            yield (false, default(int));
        }
    };
    var tmp2 = if ( tmp.Item(0)) {
        yield tmp;
    }
    else {
        Console.WriteLine("World");
        yield (true, -1);
    };
    return tmp2.Item(1);
}

Such a transformation is necessary to enable some features to work correctly, including "transparent monad comprehensions". In many cases, however, the compiler is free to emit imperative function returns as an optimization.

Yielding Values from Blocks

As alluded to previously, blocks in Eddie may yield values. Like function return values, the value yielded from a block must be sepecified explicitly using the yield keyword. A yield statement alway associates a value with the most lexically enclosing block. If one path through a block yields a value, then all paths through the block must yield a value.

For example, while the this code is legal:

var x = if (x > 10) {
    yield 2;
}
else {
    yield 1;
};

The following is not:

if (x > 10) {
    yield 2;
}
Console.WriteLine("Foo Bar");

It is also an error to yield a value from a block and subsequently ignore that value (the block must be used in an expression context). Code like the the following will result in a compile time error:

if (x > 10) {
    yield 2;
}
else {
    yield 4;
}

Pattern Matching

Pattern matching in Eddie looks similar to the C switch statement except that cases specify patterns instead of values.

switch(expr) {
    case foo
        return 0;
    case x : xs
        return 1;
    case [a, b, _, c]
        return a*b+c;
}

Unlike C there is no colon after the case expressions. They are not labels, and you cannot jump between them. There is also no concept of fall-through, although you can stack patterns on top of each other. Multiple patterns may also be seperated using commas.

switch (expr) {
    case 0 : x : _
    case x : [1, 2 ,3] 
        return x;
    case null, 12345, "Hello"
        return 0;
    default
        return 2;
}

Patterns may introduce variables, which are bound to the appropriate pattern element and can be used in the body of the following case block. Like all local variables in Eddie, pattern variables support type inference. For example given this statement:

object v = //...;
switch (v) {
    case x : xs
        Console.WriteLine(typeof(x));
	Console.WriteLine(typeof(xs));
}

The type of "x" is object and the type of "xs" is [T].

The special pattern variable _ can be used to denote an unbound pattern element. For example, with the code below no variable is introduced to refer to v.Head or v.Tail.Tail, while a variable y is bound to the expression v.Head.Tail.

switch (v) {
    case _ : y : _
        Console.WriteLine(typeof(y));
}

Pattern variables may also specify types. For example, the code below can be used to match v against a function of type int->int.

switch (v){
    case x :: int->int
}

Pattern variable types may also be generic, if the dynamic type of the matched value could be used as a substitution for the generic type.

var tuple = (1, 2, 3);
switch (v) {
    case (a, b, c) :: all<A,B,C> ((A,B,C))
        "Yep it matched."
}

Patterns are matched in their declared order, from top-to-bottom, left-to-right. However, simple instances of the switch statement, such as the one below, will be optimized to use the MSIL .switch instruction.

switch (v) {
    case 0
        return "Zero"
    case 1
        return "One"
    default
        return "Other"
}

Pattern matching is done using the dynamic type of an object. That allows code like the following to be written:

switch (v) {
    case 0
        return "an integer, with the value 0"
    case 1
	return "an integer, with the value 1"
    case x :: System.String
	return System.String.Format((System.String)"The string '{0}'.", x);
    case "Hello world"
        return "Hello World";
    case x :: all<T> ([T])
	return "A list of stuff";
}

Pattern Forms

Eddie supports the following pattern forms.:

  1. Cons patterns (x : xs)
Patterns of the form 
`pattern : pattern` can 
be used to match any object that is implicitly convertible to a list, optionally binding 
symbols to the head and tail of the list. A cons pattern will match a list of arbitrary size, 
provided it contains at least one element. Like cons expressions, cons 
patterns are right associative. That is, the pattern 
`a : b : c`, is equivalent to the 
pattern `a : (b : c)`. 
  1. List patterns ([a, b, c])
Patterns of the form `[pattern, ..., pattern]` can also be used to 
match an object that is implicitly convertible to a list. Unlike cons patterns, however, a list pattern 
only matches lists of a particular length. For example the pattern `[]` will match an 
empty list, but not `[1,2,3]`. Similarly, the pattern `[(x, y), (a,b), _]`
will  match a list of 2 elements tuples with length 3. Note that the `[]` 
pattern is just a synonym for the `null` pattern.
  1. Anonymous patterns _
A pattern that matches any object, but does not bind a name to the
object. The identifier `_` is a keyword, and can only be used in the 
context of a pattern. Variables may use `_` in their names, however, and 
like other keywords `_` can be escaped using "@". That is, `foo_bar`, 
`_baz`, and `@_` may be used to name Eddie symbols, but `_` may not. 
  1. Named patterns x
A pattern of the form `id` matches an object of any type, and binds the 
provided identifier to it.
  1. Tuple patterns ((x, y, z)).
A pattern of the form `(pattern, ..., pattern)` matches any object implicitly convertible to a tuple 
with an arity matching that of the pattern and whose elements match each provided sub-pattern.
  1. Integer literal patterns (1234).
An integer literal pattern matches any object that is implicitly convertible to type `int`
(`System.Numerics.BigInteger`), and has a value (after conversion) that is equal to the literal value. 
For example, given this code:

```eddie
class Foo {
    private int m_x;
    public static implicit operator (x::Foo) :: int {
        return x.m_x;
    }

    public function Stuff() :: [char] {
        switch (this) {
            case 0
                return "Zero";
            case Foo {m_x = 0}
                return "Not possible";
            default
                return "Not zero";
        }
    }
}
```

The results of evaluating `Stuff()` will never return a value 
`"Not possible"` becase the first pattern will always match such objects.
  1. Floating point literal patterns (3.14)
A floating point literal pattern matches any object that is implicitly convertible to type `double` and 
has a value after conversion that is equal to the provided value.
  1. String literal patterns ("Hello World")
A string literal pattern matches any object that is implicitly convertible to `[char]` (including `System.String`) and has a 
value after conversion that is:

* The same length as the string literal
* Element-wise equivalent to the string literal.

String literal patterns always use an ordinal case insensitive comparison.
  1. Character literal patterns ('c')
A character literal pattern matches any object that is implicitly convertible to type `char` and 
has a value after conversion that is equal to the provided value. Character literal patterns 
always use an ordinal case insensitive comparison.
  1. Bool literal patterns (true)
A boolean literal pattern matches any object that is either:

* Implicitly convertible to bool with a value, after conversion, equal to the pattern value.
* Implicitly convertible to a type usable in a conditional context, where the appropriate operator returns true 
(`true` literals invoke `operator true` and `false` literals invoke `operator false`).
  1. Object patterns (BinaryExpression { Left = x, Right = y})
A pattern of the form `Type { Id = pattern, ..., Id = pattern}` matches
any object that is implicitly convertible to the provided type, and whose 
members (after conversion) match the provided member patterns. For 
example, the pattern `FooBar {A = x : xs, B = (a, b, c)}` will match an 
object convertible to `FooBar` if the 'A' and 'B' members 
of the converted object match the patterns `x : xs` and `(a,b,c)` 
respectively. Member patterns may reference either field or property 
members. 

A null reference will not match an object pattern. Similarly, if
the result of converting an object to the specified type returns null, 
the object will not match the Object member even if no member patterns
are specified. For example, given the following class definition:

```eddie
class Bar
{
    public static implicit operator (x :: Bar) :: Foo {
        return null;
    }
}
```

The code below will always return a value of -1:

```eddie
function Foo(x :: Bar) :: int{
    switch (x) {
        case Foo {}
            return 0;
        default
            return -1;
    }
}        
```
  1. Constructor Patterns (Bar(x, y))
Constructor patterns are similar to object patterns, but are used only for algebraic data-types. They 
are always of the form `ConstructorName(p1, ..., pn)`, although the constructor name may be qualified as 
necessary. An object matches a constructor pattern if it is implicitly convertible to the corresponding 
constructor class and each constructor parameters matches its corresponding sub-pattern.
  1. Typed patterns (x :: T)
A pattern of the form `pattern :: type` further restricts a pattern based 
on type. Only objects that are implicitly convertible to the provided type will 
match a typed pattern. The location in a complex pattern where a type is specified has
semantic signifigance. For example the pattern `(x : xs) :: [int]` matches
a list of integers with at least one element, where as the pattern 
`(_ :: int) :  xs` matches a list of any type, provided that it's first 
element is implictly convertible to `int`).
  1. Parenthesis
Patterns may optionally be placed inside parenthesis, in the same manner 
as an expression. The parens have no meaning, other than to syntatically 
group the pattern inside them.
  1. Null Pattern (null)
The `null` pattern matches any null reference.
  1. default Pattern
The default pattern is similar to the `default` label in a C switch 
statement. It matches any object. It must be spcified as the last pattern 
in a switch statement and works using the `default` keyword.

Integer Types

The int type allows arbitrarily large integers, and maps to the System.Numberics.BigInteger structure. To convert an Eddie int to a primitive CLR integer type, you must explicitly cast it to one of the System.Byte, System.SByte, System.UInt16, System.Int16, System.UInt32, System.Int32, System.UInt64, System.Int64, System.UIntPtr, or System.IntPtr, or System.Char types.

Monad Comprehensions

Blocks