Looking inside traits (Part 2)

The Rust programming language

Journey so far

In our earlier dispatch we talked about the proposal and our ambitious intent to present a working prototype to what our customer perceived as pressing challenge that the software will solve. We started with modelling the domain with type ecosystem of Rust. We modelled a contract as struct and types of a contract as enum to express the variety more declaratively. We started thinking about modelling the behavior for the contracts. There were two options ahead of us –

1. Define behavior as trait
2. Define behavior as methods of the type

We continue from this point onwards on understanding the impact of those choices.

Traits

If you are of the type who learn new concept by simplifying them first, let us have a high-five so are we. In simpler way of saying – Traits are kind of interface from the world of C# or such equivalent object-oriented language. Now you get it why we stated an implementation of trait will look like –

 

impl enforce_contract for contract {

   …

}

 

Let us put the equivalent in a language like C#

 

public struct contract : enforce_contract {

   …

}

 

Other than the syntactic sugar the prime difference between the way a type implements the interface is in the order of occurrence of the type and interface. In C# the type appears first and the type’s contract i.e., the interface appears afterwards thus easing into an interpretation that you would have heard multiple times that interface is a contract of assured behavior implemented by a type; which emphasizes the type centricity.

Rust on the other hand focuses on the behavior and assurance of the behavior instead of the emphasizing the type. Trait is in-fact interpreted as behaviors that are shared by different types. Instead of emphasizing on a contract a type adheres to Rust by using traits allow us to model a type very early and evolve by adding new behaviors that are good abstractions across an implemented code.

In case you are lost by reading the above paragraph let us break down the evolution of C# code and play one frame at time.

Time clip # 1

In the beginning customer was not much interested in the impact and wanted to capture the details of contracts in a system. This will naturally lead to a type definition similar to

 

public class contract {

   public string supplier {get;set;}

   public DateTime enforced_date {get;set;}

   public DateTime expiry_date {get;set;}

   public uint contract_value {get;set;}

}

 

Time clip # 2

Now true to the object-oriented principle the type needs to deal with its own state and should define behavior which modifies the state.

This can be easily implemented by bringing in the change like this –

 

public class contract {

   public string supplier {get;set;}

   public DateTime enforced_date {get;set;}

   public DateTime expiry_date {get;set;}

   public uint contract_value {get;set;}

   

   public bool is_active() {

       if(DateTime.Now > expiry_date) {

           return false;

       } else {

           return true;

       }

   }

}

 

There was this class called logistics_trip which was created earlier with a definition that looks like –

 

public class logistic_trip {

   public int ladden_weight {get;set;}

   public long origin {get;set;}

   public long destination {get;set;}

   public DateTime start {get;set;}

   public DateTime end {get;set;}

   

   public bool is_active() {

       if(DateTime.Now > end) {

           return false;

       } else {

           return true;

       }

   }

}

 

Please notice the similarity for the is_active method. Now our developers have packaged these base types to a library and many versions have shipped with much of proprietary code implementations.

Time clip # 3

We now introduce after many versions introduce a third-party vendor to whom we share the library and not the code. We want them to use the types but not modify it in their code. An example usage of the code above would be like

 

using customer.sourcelibrary.types;

 

contract shipping_contract = new contract();

logistic_trip stage_material_shipping = new logistic_trip();

 

if(shipping_contract.is_active()) {

   if(stage_material_shipping.is_active()) {

       …

   }

   …

}

 

public class some_new_type {

   public int bulk_close(time_bound instance) {

       if(instance.is_active()) {

           //Implementation

       }

   }

}

 

The namespace customer.sourcelibrary.types is assumed to be the core library with contract and logistic_trip type. The code sample is random sample and not meant to be part of a single type or namespace. Crux of the scenario above is when the third-party vendor intends to define a method that could accept both contract and logistic_trip as parameter to a method in the example above method is named as bulk_close. The third-party vendor wishes to define an interface named time_bound and make both contract and logistic_trip implement the interface.

For the third-party vendor this is not possible without modifying the source library which we created. Thus, leading to the next snap shot which typically introduces debt in code. The debt which cannot be lowered without refactoring the source implementation.

Time clip # 4

Create two different methods with differing signatures to handle the situation

 

public class some_new_type {

   public int bulk_close(contract instance) {

       if(instance.is_active()) {

           bulk_close(true);

       } else {

           bulk_close(false);

       }

   }

 

   public int bulk_close(logistic_trip instance) {

       if(instance.is_active()) {

           bulk_close(true);

       } else {

           bulk_close(false);

       }

   }

 

   public int bulk_close(bool currently_active) {

       if(currently_active) {

           bulk_close(true);

       }  else {

           bulk_close(false);

       }

   }

}

 

 

We had to write such a code because we could not do something like this –

 

public interface time_bound {

   public bool is_active();

}

 

contract : time_bound

 

logistic_trip: time_bound

 

This is simply not possible in languages like C# or for that reason Java. Thus, introducing the tech debt and for situations like ours where we are evolving the model along the way as we learn or discover new concepts it becomes increasingly taxing because we need to refactor code.

Trait on the other hand simplifies the evolution for us so that our Time clip #4 could be written as

 

pub trait time_bound {

   fn is_active(&self) -> bool;

}

 

 

impl time_bound for contract {

   …

}

impl time_bound for logistic_trip {

   …

}

 

 

This will work even if the struct and its implementation have evolved in a different crate (library) than that of the third-party vendor’s crate (library).

Now did you notice the simple difference in philosophy by changing the order of type and trait the impact it has on evolutionary modelling of types.

This was very powerful concept for us and along the way we also observed how the reference to &self makes it more javascriptstyle binding to a type at runtime. This characteristic of Rust language made us evolve the enforce_contract trait have a assess_impact method which helped us achieve the goal we set for ourselves in mind and we could implement the assess_impactmethod on many types. Now for any event which has list of contracts we could simplify and distribute the implementation of impact in case of failure to execute the contract. So much so that same code could live from the early stage of proposal flow into latter stages of development and eventually in production.