The Cobra Programming Language

Introduction

This is an introduction to the Cobra programming language. It assumes you already know one or more high level languages such as Python, C#, Java, C++, Visual Basic, PHP, Ruby, etc.

Cobra is an imperative, high-level, object-oriented language with direct support for contracts, unit tests and compile-time nil tracking. It has namespaces, classes, interfaces, generics, methods, properties, indexers, variable number of args, overloading, exception handling and garbage collection. It has a high-level syntax with indented blocks, doc strings, list literals, dictionary literals, in operator, slicing, for expressions, assert, and more.

That may sound like a lot, but its complexity is in the same neighborhood as other high-level languages, most of which have been accumulating features since their initial release (for example, Python and C#).

Let's get started with "Hello, world.":

class Hello

    def main is shared
        print 'Hello, world.'

Cobra uses indentation to denote code structure since adept programmers do this anyway in languages that don't even require it (C#, Java, C++, etc.). In Cobra, one TAB is equivalent to one INDENT and by convention, tabs are displayed with a width of four characters. Cobra may support space-based indentation in the future, if it is determined that the TAB approach is too unfriendly to popular editors.

Cobra is supported on both Microsoft(R) Windows(R) as well as Posix (Mac, Linux, etc.) via Microsoft .NET and Novell Mono, respectively. At the command line, on Windows:

> cobra hello.cobra
Hello, world.

And on Posix:

$ cobra hello.cobra
Hello, world.

A hello.exe is left after the execution and, if you want, you can compile a Cobra program without running it. In the examples below, the program is then run directly:

Windows:
> cobra -compile hello.cobra
> hello
Hello, world.
Posix:
$ cobra -compile hello.cobra
$ mono hello
Hello, world.

Now let's print the Fibonacci sequence:

class Fib

    def main is shared
        n = 10
        a = 0
        b = 1
        for i = 0 .. n
            print b
            save = a
            a = b
            b = save + b
1
1
2
3
5
8
13
21
34
55

Note that a statement like n = 10 is really a shorthand for n as int = 10. Cobra infers the type for a local variable from the value it is initialized with. The type is fixed from that point on--attempting to assign a string to n will give a compile-time error. Type inference allows for clean, quick coding while the fixed type enables better error checking and faster execution.

Let's keep the main small and break out Fib into more library-oriented code:

class Fib

    def compute(count as int) is shared
        a = 0
        b = 1
        for i = 0 .. count
            print b
            save = a
            a = b
            b = save + b

class Program

    def main is shared
        Fib.compute(10)

Now Fib.compute can be used from anywhere and the number of elements to compute can be passed as an argument. But its output is still going directly to the console instead of being returned to the caller. We can fix that by having it return a list:

class Fib

    def compute(count as int) as List<of int> is shared
        list = List<of int>()
        a = 0
        b = 1
        for i = 0 .. count
            list.add(b)
            save = a
            a = b
            b = save + b

class Program

    def main is shared
        i = 1
        for number in Fib.compute(10)
            print '[i]. [number]'
            i += 1
1. 1
2. 1
3. 2
4. 3
5. 5
6. 8
7. 13
8. 21
9. 34
10. 55

The List<of int> is an example of a generic where type arguments are passed to construct the final type. In this case, the type is spoken as "list of int". Generics increase readability, type safety, and sometimes, performance. To instantiate any class, whether generic or not, call it with parens ()s. Some classes will, of course, take arguments for their initialization.

Also, introduced in the above example is string substitution where any Cobra expression can be embedded in a string by surrounding it with square brackets. This turns string literals into a kind of mini-templating language.

And finally, the += operator is a shorthand for left = left + right and may also be more efficient.

Now let's take a more object-oriented approach by making the Fib class an actual list of the Fibonacci numbers, as opposed to a "computer of them":

class Fib
    inherits List<of int>

    def init(count as int)
        base.init(count)
        a = 0
        b = 1
        for i = 0 .. count
            .add(b)
            save = a
            a = b
            b = save + b

class Program

    def main is shared
        i = 1
        for number in Fib(10)
            print '[i]. [number]'
            i += 1

The above program makes Fib a subclass of List<of int> by stating inherits List<of int>. Then an initializer is declared for the class with def init. Although it looks like an ordinary method, init has some special rules:

Note there is no is shared on the init because it is intended for the object level, not the class level. In fact, most methods (properties, inits, etc.) are not shared.

The base.init(count) call is taking advantage of the fact that the List<of> base class provides an init(capacity as count) to set the initial capacity of the list. (The list can still grow in size past that capacity on any call to add.)

The .add(b) is an invocation on the current object. It is like obj.add(b), but with the obj assumed to be this. The leading period is a cue to anyone reading the code, and when writing the code, it is a cue to the IDE to provide autocompletion choices limited to the current class.

Coding for Quality

Cobra has language-level support for high quality coding via doc strings, unit tests, contracts, nil tracking and assert statements.

A doc string provides documentation for the declaration it is written with. Developers are more likely to write documentation in the first place if it can be put right with it describes. They are also more likely to maintain it. Here is an example:

class Utils
    shared
        def countChars(s as String, c as char) as int
            """
            Returns the number of instances of c in s.
            """
            count = 0
            for ch in s
                if c==ch
                    count += 1
            return count

A unit test validates that a class, property or method is working properly. It can also be viewed as a form of documentation because it provides examples of using what it validates. Here is the above example with a unit test added:

class Utils
    shared
        def countChars(s as String, c as char) as int
            """
            Returns the number of instances of c in s.
            """
            test
                assert Utils.countChars('', c'x')==0
                assert Utils.countChars('x', c'x')==1
                assert Utils.countChars('X', c'x')==0  # case sensitive
                assert Utils.countChars(' ! ! ', c'!')==2
            body
                count = 0
                for ch in s
                    if c==ch
                        count += 1
                return count

As with doc strings, unit tests are more likely to be written and maintained when they can be coupled with their respective declaration.

A contract describes what a method requires in order to be called and what it ensures when it returns to its caller. Each item in the contract is expressed as a boolean expression in the language so that it can be executed at runtime. Contracts help catch errors earlier when they are easier to diagnose. They also document a method in a rigorous fashion.

class Customer

    var _contacts as List<of Contact>

    get contacts from var

    def addContact(contact as Contact)
        require
            contact not in .contacts
            contact.name
            contact.customer is nil
        ensure
            contact.customer == this
            .contacts.count = old .contacts.count + 1
        body
            contact.customer = this
            _contacts.add(contact)

The above features can substantially improve the quality of a project when used from the beginning. However, they are all optional features and can be passed on when you are working on not-so-serious programs. The nil tracking feature is not optional, however.

Cobra calls it nil as do Smalltalk, Objective-C and LISP. Other languages call it NULL, Nothing, None and null. It's customary in most languages to be able to pass nil anywhere, or at least where instances of classes are expected. But in Cobra, nil can only be passed when allowed by a nilable type: one suffixed by a question mark (?). So, for example, the following code will generate a compile-time error because Utils.countChars was declared to take a String, not a String?:

Utils.countChars(nil, c'x')

The follow code does not fail at compile-time or run-time because the if statement proves that the call is safe:

class Foo

    def bar(s as String?)
        if s  # same as "if s is not nil"
            print Utils.countChars(s, c'x')

The upshot of nil tracking is that runtime exceptions about nil reference errors are almost entirely eliminated. That's not the case in C#, VB or Python which all throw exceptions for foo.bar when foo is nil. Furthermore, the Cobra compiler can report multiple errors at a times vs. only one at a time for languages that throw an exception at run-time. And like the other quality features described before, this one also serves as a form of documentation: at a glance you can tell which parameters permit nil and which do not.

Nilability applies to all types including return types, local variables and class variables.

The assert statement provides an easy way to verify the internal state of your code. If the verification fails, an exception is raised.

...
assert i>0, i

...
assert name

...
assert x<y, 'x=[x], y=[y]'

The exception includes the source code of the condition, file name, line number and the optional information provided as a second argument to the assert.

You might note that the same functionality can be provided by a library call such as Util.assert(cond, info), but then the info argument is always evaluated even when the assertion passes (and most of them do). Another valid approach would be an if statement with a throw, but assert is simply more convenient than that. Like Cobra's other quality-related features, the convenience is intended to encourage frequent usage in every day coding.

Where to next?

The How To programs are small Cobra programs the demonstrate specific tasks such as how to:

Your next stop should be How To.

See Also