dType — Decentralized Type System & Functional Programming on Ethereum
In my previous article, A Vision of a System Registry for The World Computer, I talked about the vision behind dType, a Decentralized Type System for Ethereum. Now, we are going to explore the protocol in more detail and see how a global functional programming protocol can be built on top of it.
dType aims to bring consensus on data types and foster interoperability. You can read our draft ERC proposal and you can leave feedback in the corresponding GitHub issue. Make sure you check out our proof of concept implementation.
Typing in Ethereum Now
We know what a type system is: a system that allows you to assign a specific type to a variable, expression, function, etc. and performs a compile and/or run-time check to see if that rule is respected. This eliminates a set of bugs determined by improperly connecting various interfaces in your code.
Solidity is a statically typed language. Aside from providing elementary types such as string
or address
natively, it also allows the developer to construct complex ones, using struct
.
What happens now, is that each developer is free to make his own complex types, depending on his needs. This is awesome! But what happens if you want to build the even more awesome World Computer, where you have projects neatly cooperating with each other? You end up with a lot of projects where each defines its own types in their own encapsulation. To make any two projects interoperate requires a deeper knowledge of those projects and maybe even source code changes.
Decentralized Typing with a Global Registry
This is where a Decentralized Type System comes in handy. The community can discuss and reach consensus on what types to create and use and everyone can benefit from using these well known and tested types. It becomes easier to see what contracts and external functions use a certain type and easier to interconnect the projects and analyze blockchain data.
The Decentralized Type System that we are proposing contains a dType Registry contract, that has a reference to all accepted types and the contracts that implement them.
For example, for registering some of the standard value types from Solidity into the system, the following format is used:
{
"contractAddress": <address of the uint256 Type Contract>,
"typeChoice": 0,
"source": <bytes32 SWARM hash for source files>,
"name": "uint256",
"types": []
}
{
"contractAddress": <address of the string Type Contract>,
"typeChoice": 0,
"source": <bytes32 SWARM hash for source files>,
"name": "string",
"types": []
}
If we want to register complex types, an example would be:
{
"contractAddress": "0x105631C6CdDBa84D12Fa916f0045B1F97eC9C268",
"typeChoice": 0,
"source": <a SWARM hash for source files>,
"name": "myBalance",
"types": [
{"name": "string", "label": "accountName", "relation": 0},
{"name": "uint256", "label": "amount", "relation": 0},
]
},
{
"contractAddress": "0x91E3737f15e9b182EdD44D45d943cF248b3a3BF9",
"source": <bytes32 SWARM hash for source files>,
"name": "myToken",
"types": [
{"name": "address", "label": "token", "relation": 0},
{"name": "myBalance", "label": "balance", "relation": 0},
]
}
A Type Contract stores the address to the deployed Type Library that contains the actual type definition, along with helper functions for structuring and destructuring data and higher-order functions (HOFs), such as map
, filter
, reduce
.
pragma solidity ^0.5.0;
pragma experimental ABIEncoderV2;
library myBalanceLib {
struct myBalance {
string accountName;
uint256 amount;
}
function structureBytes(bytes memory data) pure public
returns(myBalance memory balance);
function destructureBytes(myBalance memory balance) pure public
returns(bytes memory data); function map(
myBalance[] memory balanceArr,
function(myBalance memory) external pure
returns (myBalance memory) callback
)
view
public
returns (myBalance[] memory result);
}
The Type Contract also stores addresses of other optional contracts. For example, we can have a Type Storage Contract, that stores data entries for that type. These data entries can be aggregated across different projects.
Now, developers can just use the Type Library in their own contracts. Or, they can go a step further and use this system to also keep their data. While the motivation for the first option is clear: data standardization and consistency across Ethereum, the second option of using a global storage system is a different pattern compared to how things are currently done. However, a global storage system allows us to do some interesting things, as you will see below.
Introducing Pure Function Contracts/Libraries
To dive into the benefits of this protocol, I need to present another important component.
Aside from the type libraries and storage contracts, developers can create contracts or libraries with pure functions, that know how to interact with the chosen types. We are separating business logic from the state. And this is the first step to a general functional programming system on Ethereum.
If you register these pure functions in the dType Registry and tell the Registry what type of inputs and outputs the function has, you can start doing some awesome things. You can have automation in the system.
We can use one or more registered types:
pragma solidity ^0.5.0;
pragma experimental ABIEncoderV2;contract myBalanceAndTokenFunctions {
using myTokenLib for myTokenLib.myToken;
function doubleBalance(myTokenLib.myToken memory myTokenData)
public
pure
returns(myTokenLib.myToken memory myNewTokenData);
}
Registering the doubleBalance
function type in the dType Registry can be done like this:
{
"name": "doubleBalance",
"types": [
{"name": "myToken", "label": "myTokenData", "relation":0}
],
"lang": 0,
"typeChoice": 4,
"contractAddress": <myBalanceAndTokenFunctions address>,
"source": <bytes32 SWARM hash for source files>,
"outputs": [
{"name": "myToken", "label": "myNewTokenData", "relation":0}
]
}
Connecting State with Logic
Let’s say you want to run some pure
functions on one of the data entries that a type has and then upsert the result in the function’s output type storage contract.
You can watch a detailed explanation of how this can be done here: https://youtu.be/pcqi4yWBDuQ. The run()
function code can be found in our PoC.
To summarize:
- you call
run()
with thetypeHash
corresponding to the pure function you want to use, along with thedataHashes
corresponding to the function’s inputs; eachdataHash
references a storage entry - dType knows how to get each input data because it knows the input type and can retrieve the storage contract address
- dType also has the contract/library address where the pure function is located
- dType can upsert the result of the computation in the storage contracts corresponding to each of the final output types.
Functional Programming
What functional programming is, benefits and drawbacks are discussed in detail in a variety of online resources. It has become a widely known and used paradigm of programming that allows developers to create scalable projects.
Even though Solidity is not a functional programming language in itself, we can still benefit from some of the features by changing our coding patterns.
We are now thinking about a building block as a smart contract with an encapsulated object that contains state changing functions that are only understood from within. This is more akin to Object Oriented Programming and poses interoperability and scalability issues. Not necessarily for an individual project, but for a global Ethereum System.
Functional Programming for Ethereum
Some of the benefits of using functional programming paradigms can be taken advantage of also in Ethereum.
Deterministic, pure
functions are easier to test
We are separating the business logic from storage. Auditing and testing of pure
function libraries is easier because they do not have side effects and their output is deterministic. You can even run your test suite on the Mainnet! Which usually never happens within current projects.
Most of the code that you will be writing will be of this nature. Lowering the difficulty of testing means more chances of writing secure code.
Reusable Storage Pattern
The storage pattern used for dType itself and all the Type Storage contracts can be the same. This lowers the cost of building, testing and auditing the code.
High Risk, Smaller Surface
The code with the highest risk of containing bugs will be the core that translates the results of chaining pure
functions into state changes.
However, this core is a general pattern that we can all contribute to, test and audit. Therefore, it has the potential to be the most secure piece of the puzzle.
Interesting Side Effects
When you have aggregated and transparent data, categorized on each type, anyone can plugin in and do awesome things with that data — from classical automation tools to ones based on Artificial Intelligence.
We get a step closer to the promise of a real World Computer.