Aaron Christiansen

Causes - a programming language feature concept

2019-08-15

Introduction

A typical programming language’s standard library is often filled with methods which clearly complement each other.

A clear and common relationship is between an operation method, and a predicate method which becomes true if that operation has taken place. For example, immediately after calling the clear method on a Ruby Array instance, the empty? predicate becomes true. There are plenty of other ways to mutate an array such that it satisfies the empty? predicate, but clear is guaranteed to satisfy it every time with just one method call. To summarise: calling clear causes empty? to be true.

We’ve now established that there’s a relationship between these methods. What if we could declare relationships like this to the language, and what advantages would this bring to both the programmer and the language itself?

Proposing a language which includes causes

Let’s take a look at a theoretical language which allows relationships like this to be declared. We’ll call these relationships causes, as calling one method causes another predicate to become true.

I’ll be writing Ruby-style pseudocode, as implementing complex functionality like this using Ruby’s metaprogramming functionality seems feasible.

Here’s how the empty? and clear methods could be declared in the Array class, with a cause given for clear:

class Array
  # ...other definitions...

  def empty?
    length.zero?
  end

  causes { empty? } # <-- Here's our defined cause
  def clear
    each do |item|
      delete(item)
    end
  end
end

This code would associate the empty? and clear methods, specifying that an invocation of clear causes the predicate empty? to become true.

This could also be extended to more complex causes relying on the arguments passed to the method. For example, if an item has just been appended to an array (using << in Ruby), then it must be the last item in that array.

A definition of this cause could look like:

cause { |item| last == item } 
def <<(item)
  # ...array append logic...
end

Here, the predicate is more than a simple method call - the last method is invoked and has its result compared with the item to append.

What is gained from doing this?

Thinking of a method’s causes seems like yet another hurdle when writing code, so why bother?

Effortless idempotency

Because it is specified exactly what will happen when they’re called, simple methods with causes can be called idempotently. That is, if their effect has already happened, it shouldn’t happen again.

Suppose you have an array of bytes called bytes supplied by a user, and you’re about to process it in a way which requires a null terminator to be at the end. The user may or may not have included this null terminator. In traditional Ruby code, this could be done like so:

bytes << 0 unless bytes.last == 0

Introducing an idempotent call syntax into our language, where we add a ? prefix to the method’s name, we could do this:

bytes ?<< 0

This is possible because, by specifying what a method causes formally, we know exactly what << will do and therefore can determine if it is required to call it. This particular code snippet would evaluate the predicate specified in the cause for <<; recall that this predicate is |item| last == item. If this predicate is false, i.e. the last item is not 0, then << would be executed.

Bringing declarative programming to a procedural language

If it is known what condition a method satisfies, then there is also the power to flip this information around. Given a condition we want to satisfy, we can find a method which causes it.

Suppose we have a variety of sorting algorithms defined on the language’s array type, each with a cause specified such that the array satisfies a sorted? predicate after invoking them. To sort an array instance, we can write code that asks the language to satisfy the sorted? predicate, using a satisfy keyword:

array = [4, 3, 7, 2]
satisfy array.sorted?
p array # => [2, 3, 4, 7]

The language is aware of which methods will satisfy the sorted? predicate and can select one accordingly, perhaps using an intelligent interpreter which tries each candidate method over time and establishes which method is faster for particular instances.

Extra assertions for free

Each cause could optionally act as a postcondition assertion, where it runs after the associated method has been invoked. This is similar to the contract features built into Eiffel or D. For instance, in the clear and empty? example, the language will verify that the list really is empty when clear returns, and throw an assertion error if not. This could help catch bugs earlier.

Easier-to-read code

Each cause can act as documentation, showing a programmer reading the code what implications a method will have. In addition, because causes can act as assertions, it is guaranteed that these implications are true.

Conclusion

I think that implementing causes in a language could be a clean, unintrusive way to make code more expressive and easier to read. What do you think?