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?
Top comments (2)
It's possible to write the first part, haven't taken a shot at satisfying yet:
If you'd like I can write a bit more on how exactly this works, but the short of it is that
Causation
intercepts all added methods in a class. It'll ignore anything without acauses
call above it.The fun part was trying to get the block to evaluate in the context of the instance, and
instance_exec
ended up working for that one. Admittedly didn't know about that one before, so I'd have to read more into it to explain beyond that it allows injection of variables or state into the evaluation and still lets you rebind the block to execute in the instance.Now as far as the actual content of the article itself, you may enjoy ideas like Sorbet and other static typing implementations. They don't quite give the flexibility that this does as far as guarantees of output via arbitrary functions, but could inspire some other interesting ideas.
Awesome start at implementation!
I'm actually really enjoying Sorbet - I've already developed two tools for it (Sord, to generate signatures from YARD docs, and Parlour, a plugin framework). The
causes
syntax in this article was inspired by Sorbet'ssig
. I thought of this idea when thinking about how Sorbet could be taken further.