clarity-lang/reference

Isolating state mutation

Opened this issue · 0 comments

Clarity is brilliantly influenced by ideas from functional programming. This is a source for important benefits promised by the language, such as predictability, security, decidability, reliability and consistency. It is timely to review the current design to potentially make Clarity even better aligned with its promises.

Functional style programs are easier to reason about, and can more safely be refactored. They make static analysis and type inference less complicated, and are simpler to process for other programs including to lint, transpile, generate, or optimize.

Clarity builds on several concepts from functional programming. It has immutable datastructures: Lists, buffers, strings, and atoms like numbers cannot be mutated but creates new ones, with the previous version still unchanged. Most of Clarity's functions are pure: They have no side effects and deterministically produce the same output for same arguments, making them referentially transparent. Symbols are permanently bound to values; Once defined they do not change their value.

The exception is mutable state stored between transactions, defined with define-map and define-data-var. Mutating state makes the contract sensitive to execution order, cancelling the benefits of pure functions. @jnelson provides an example in stacks-network/stacks-core#1628 where each call to get-inc-foo returns a mutated global variable, confusing the intent and making the program more surprising:

(define-data-var foo uint 1)

(define-private (get-inc-foo) 
  (begin
     (var-set foo (+ 1 (var-get foo)))
     (var-get foo)))

(< (get-inc-foo) (get-inc-foo) (get-inc-foo))  ;; What does this evaluate to?

Mutating state is of course essential to a smart contract as a state machine. Other functions mutating state includes stx-transfer and contract-call to a public function, as well as minting and transfer of tokens.

How can state mutation be improved to support a more functional style? Mutating calls can benefit from being isolated, leaving the heavy lifting to pure functions. Clarity already have basic support for this approach, where the mutations typically are kept in the initially called public function. Proposal stacks-network/stacks-core#1393 is intended to allow serial mutations without relying on map iteration, lifting the mutating call by eliminating the need for a mutating mapper function. Similarly, the map-update suggested in stacks-network/stacks-core#1728 (comment) calls a read-only function that transform the state in the table.