Declarative Clarity

Although Clarity generally supports declarative programming, the code examples resort to imperative programming with statements mutating the state of the contract as side effect.

Would it be possible for Clarity to support a declarative programming style also for functions that determine the next state of the contract?

It can be beneficial to avoid or at least restrict side effects by keeping most functions pure. The Welcome to Clarity guide even recommends a naming convention calling out functions with side effects:

Functions that mutate data by convention terminate with an ! exclamation point.

Yet the Clarity tutorial defines functions that mutate the contract state as side effect:

(define-map tokens ((account principal)) ((balance int)))

(define-private (token-credit! (account principal) (amount int))
  (if (<= amount 0)
      (err "must move positive balance")
      (let ((current-amount (get-balance account)))
        (begin
          (map-set! tokens (tuple (account account))
                      (tuple (balance (+ amount current-amount))))
          (ok amount)))))

(define-public (mint! (amount int))
   (let ((balance (get-balance tx-sender)))
     (token-credit! tx-sender amount)))

In this case, token-credit! mutates the persistent tokens map as a side effect. How could this functionality be implemented in Clarity using a declarative style?

1 Like

I’m supportive of more declarative features in Clarity, but I’m not sure in this case how best to implement things like token transfers or contract data updates in general in a more declarative style. One approach would be to allow contracts to define something like transformation functions, which do not themselves mutate any state, but instead return transforms, which are then applied to the contract state. I’m not sure if it would offer much benefit for contract developers and I’d need to see some examples of it in use to be convinced.

2 Likes

A declarative style could make it harder for developers to get into trouble, while facilitating static analysis. I’ll work on some examples.

How does Clarity deal with side effects in an input function to map? As in extending the earlier example:

(define-private (credit-sender! (amount int))
  (token-credit! tx-sender amount))

(define-public (map-side-effects!)
   (map credit-sender! (list 1 2 3 4 5)))
1 Like

Any guidance on where to find quality tutorials or documentation that can help with writing Clarity contracts? You seem to have a strong grasp & it’d be beautiful to open up access to such pivotal know how :slight_smile: Thank you kindly for whatever possible. :pray:t5::fist:t5:

3 Likes

How does Clarity deal with side effects in an input function to map ? As in extending the earlier example …

It allows mutation to occur in map functions. Since map, filter, fold are the only methods of iteration in Clarity, mutation in those functions is the only mechanism for iteratively applying mutations.

1 Like

A variation is functions returning the transform as compound data, applied to the contract state by the caller. Updating the example above to this approach, the private function could return the transform, leaving to the public function to mutate the contract state:

(define-map tokens ((account principal)) ((balance int)))

(define-private (credit-token (account principal) (amount int))
  (if (<= amount 0)
    (err "must move positive balance")
    (let ((balance (get-balance account)))
      (ok { account: account,
            balance: (+ balance amount) }))))

(define-public (mint! (amount int))
  (let ((transformation (credit-token tx-sender amount)))
    (if (is-ok transformation)
      (map-insert tokens transformation)
      (err "Failed to mint"))))

This approach is simplified by built-in functions like map-insert taking an argument that combines the key and value, as suggested in issue 1334, where the return value from functions generating the transformation doesn’t have to be destructured before making the call to mutate the contract. Avoiding mutating side effects in lower-level functions benefits static analysis.

Higher order functions like map, fold and filter are staples of functional programming. They are preferably used with pure input functions. Avoiding mutating side-effects in their input functions makes it easier to reason about the code.

Let’s introduce new built-in functions that can mutate multiple entries in a persistent map, leaving the higher order functions to generate entries without mutating global state in their input functions.

For example, to mint tokens in a list of accounts, a new built-in function called say map-extend could insert a generated list of entries into a persistent map:

(define-map tokens ((account principal)) ((balance int)))

(define-private (init-token (account principal))
   { account: account,
     balance: 100 })))

(define-public (mint! (accounts (list 10 principal)))
  (let ((transactions (map init-token accounts)))
     (map-extend tokens transactions)))

Here transactions is a generated list of {account, balance} tuples that are inserted into the persistent tokens.

This lifts the mutation to the public function, benefiting static analysis and unit testing. There should also be a similar built-in function that can update multiple map entries.