Tranquility — a better smart contract programming language
As of today Solidity remains the de-facto standard for smart contract development on Ethereum. Despite its success, every time I used it I wished a better language existed.
There aren’t many languages being actively developed that target the EVM. Those that exist (e.g. Vyper) sacrifice already limited flexibility to increase safety. But we programmers like to have our cake and eat it too. I believe safety and flexibility should be increased together.
The biggest problems with Solidity right now are in my opinion:
- the lack of efficient abstractions,
- not enough safety,
- the difficulty of debugging.
So how do we improve it? In this blogpost I would like to explore the idea of creating a new programming language, Tranquility, that learns from Solidity’s mistakes and improves on all fronts.
NOTE: Tranquility is at an early stage. The article does not describe the current state of the language, only the design thoughts that come into it.
Abstracting the complexity away
Custom data types
Contracts in Solidity are in many ways similar to classes. Unfortunately they are loaded with all of the specifics of Ethereum and this makes them unsuitable for implementing abstractions. Solidity also offers structs and libraries, yet structs only encapsulate data, and libraries only encapsulate behaviour.
Those limitations greatly reduce the possibility of code reuse across projects. This leads to absurd situations, for example, if you wanted to have a smart contract that represents two tokens you would essentially need to duplicate the code for each of the tokens.
We believe it is possible to do better, by adding a new abstraction to live alongside contract
. Introducing storage
— the way to manage contract data in Tranquility.
storage Counter {
value: Uint get () {
return value
} increment (by: Uint) {
value += by
}
}
This little thing looks deceptively similar to a contract, but the differences are key to understanding its power:
- Members are private, methods are public. You can only interact with a storage using the public methods that it specifies. Storage also can’t inherit, so you can be sure that you are seeing the entire thing just by looking at the declaration.
- Storage does not know about the blockchain. Storage does not have access to
msg
, time, block hash or other contracts. This allows for easy testing and composition of storages — you can be sure that storage only manipulates data that it has full control over. - Storage can be composed. What this means is that you can wrap existing
storage
classes to restrict or add new behaviour. You can implement a counter that can only increment by 42, or one that also counts all of the times it has been called, but you will never have to touch the underlyingCounter
implementation. This solves the problem of code reuse across data structures and projects. - Storage encapsulates checks and guarantees behaviour. This feels natural to any programmer that knows OOP, yet is completely impossible to achieve in Solidity. Any user of the implementation of
Set<T>
below can be sure that this collection will not contain any duplicates, and thatindices
andarray
can only be manipulated using theSet<T>
methods. - Generics. Yes you guessed it, storage can be parameterised with types.
storage Set<T> {
indices: Map<T, Uint>
array: Array<T> use array.get
use array.length has (value: T): Boolean {
return indices[value] != 0
} add (value: T) {
if (has(value)) {
array.push(value)
indices[value] = array.length()
}
} remove (value: T) {
let index = indices[value]
if (index != 0) {
let last = array.last()
array[index - 1] = last
indices[last] = index
indices[value] = 0
}
}
}
Storage can of course be trivially used in contracts, which (in contrast to storage
) have the ability to access the blockchain.
contract FriendCollector {
friends: Set<Address> use friends.get
use friends.has
use friends.length becomeFriend (msg: Message) {
friends.add(msg.sender)
}
}
Free running functions and events
In Solidity and Java all things must reside in contracts / classes. This restriction is not only unnecessary, but also harms code reuse and destroys separation of concerns.
All things that are not part of the contract, that is events, pure functions (not contract methods) and storage (as discussed before) should be freed and allowed to live a life of their own.
This comes with a set of advantages:
- writing reusable code is easier, because you don’t need to use inheritance or wrap your utils in a library. Just call a pure function directly,
- functions and storage become easy to test, because they no longer require contract deployment and do not concern themselves with any contract stuff,
- the module system (described below) makes it explicit about where things come from.
Better modules
Because functions, events, storages and contracts are all top-level declarations in Tranquility, we need to update our module system to reflect that. Fortunately a few simple rules will enable us to have a robust solution.
// file src/utils.tq
function add (a: Uint, b: Uint) {
return a + b
}event Foo {
value: Uint
}storage Always42 {
get () {
return 42
}
}
We have just created a utils file and now we want to use those utils in a smart contract. Nothing gets easier than this because all we have to do is say from utils import ...
. We can import any top level declaration from a module, but we need to be explicit about it, so that it is obvious where a given thing is declared.
// file src/main.tq
from utils import add, Foo, Always42contract Main {
always42: Always42 getMagicNumber () {
emit Foo(1337)
return add(always42.get(), 27)
}
}
This module system is simple to explain and simple to use, but at the same time very powerful and robust.
Safety by default
SafeMath
Have you heard about SafeMath? This is a third party library that can help you prevent overflow and underflow errors in your computations. We believe a good programming language for Ethereum should have it by default. And this should not only cover uint256
but all numerical data types.
This is why in Tranquility every mathematical operation will automatically throw in the case of overflow / underflow.
Inline Assembly
Why do programmers even use assembly in Solidity? Let’s look at the most popular smart contract library OpenZeppelin. Here are the use-cases:
Those are pretty common and should not require stepping down and writing machine code. In order to reduce assembly usage in smart contracts Tranquility aims to include a robust standard library, that simplifies common tasks present when developing on Ethereum.
Testing and debugging
Compiled, but also interpreted
In order to facilitate better programmer experience we believe that a language targeting EVM should also have a standalone interpreter that does not run on the blockchain or even on the EVM. This is crucial to allow quick and painless debugging of the logic and provide better tooling for the user.
Having an interpreter also means a functioning REPL and online playgrounds that require no installation and run very fast.
We all want a good debugging experience. Setting up a debugger, especially for the EVM, can be a lot of work. Some issues can be solved trivially if we just re-run the code with a few print
calls here and there. Yet there is no print
in Solidity.
Since we are creating a new language we shouldn’t forget about adding this crucial piece of functionality.
let a = 3
print(a)
// emits a Log { message: String } event when on chain
// calls console.log when interpreted
Testing
Testing is one of the fundamental things in software development. Tranquility strives from the very beginning to provide a better testing experience.
Because the language is interpreted writing unit tests is very simple:
function add (a: Uint, b: Uint) {
return a + b
}[#test]
function testAdd () {
let result = add(1, 2)
expect(result).toEqual(3)
}
When the development of Tranquility progresses, special care has to be taken to ensure parity between the compiled and interpreted code.
Closing thoughts
Other languages
Vyper is nice, but lacks abstractions and adds restrictions. Others are far from being production ready and do not offer the order of magnitude improvements necessary to justify switching.
We firmly believe that improving on a single front, while resulting in a better language, is not enough to make the language worth switching to. Only by improving on all fronts, providing a better syntax, more flexibility, more security, better abstractions, better testing and developer experience a language stands a chance of having a user base.
I want to learn more
Great. It is still early days for Tranquility. If you want to get involved hop over to the github repository where you can explore the language for yourself.
We’d love to hear your feedback! Thanks for reading.