DEV Community

Aaron Christiansen
Aaron Christiansen

Posted on

Causes - a programming language feature concept

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

bytes ?<< 0
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
baweaver profile image
Brandon Weaver

It's possible to write the first part, haven't taken a shot at satisfying yet:

module Causation
  def self.included(klass)
    klass.extend(ClassMethods)
  end

  module ClassMethods
    def causes(&effect)
      @causation     = true
      @caused_effect = effect
    end

    def method_added(method_name)
      return unless @causation

      @causation = false

      effect = @caused_effect
      original_method_name = "#{method_name}_without_effect".to_sym

      alias_method original_method_name, method_name

      define_method(method_name) do |*args, &fn|
        original_result = send(original_method_name, *args, &fn)
        validation      = instance_exec(original_result, &effect)

        raise 'unintended!' unless validation
      end
    end
  end
end

class Something
  include Causation

  attr_reader :v

  def initialize(v) @v = v end

  causes { v.nil? }
  def boom(v = nil); @v = v end
end

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 a causes 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.

Collapse
 
aaronc81 profile image
Aaron Christiansen

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's sig. I thought of this idea when thinking about how Sorbet could be taken further.