Clarity Smart Contract Language

Content

Clarity of Mind Foreword Introduction

Coding style

Here are some suggestions on Clarity coding style. The chapter is intentionally not called coding guidelines: the community leads the way. If coding style SIPs are ever ratified then they shall be covered here. Until then we can only make a best effort based on the experiences of long-time developers.

Pointless begins

Most functions take an invariant amount of inputs. Some developers therefore develop a tendency to overuse begin. If your begin only contains one expression, then you can remove it!

(define-public (get-listing (id uint))
    (begin
        (ok (map-get? listings {id: id}))
    )
)

And without:

(define-public (get-listing (id uint))
    (ok (map-get? listings {id: id}))
)

There actually is a runtime cost attached to begin, so leaving it out makes your contract call cheaper. The same goes for begin inside other invariant function expressions.

>> ::get_costs (+ 1 2)
+----------------------+----------+------------+
|                      | Consumed | Limit      |
+----------------------+----------+------------+
| Runtime              | 4000     | 5000000000 |
+----------------------+----------+------------+
3

>> ::get_costs (begin (+ 1 2))
+----------------------+----------+------------+
|                      | Consumed | Limit      |
+----------------------+----------+------------+
| Runtime              | 6000     | 5000000000 |
+----------------------+----------+------------+
3

Nested lets

The let function allows us to define local variables. It is useful if you would otherwise have to read data or redo a calculation multiple times. Variable expressions are actually evaluated in sequence which means that a later variable expression can refer to prior expressions. There is therefore no need to nest multiple let expressions if all you want to do is calculate a value based on some prior variables.

(let
    (
    (value-a u10)
    (value-b u20)
    (result (* value-a value-b))
    )
    (ok result)
)

Avoid *-panic functions

There are multiple ways to unwrap values, but unwrap-panic and unwrap-err-panic should generally be avoided. They abort the call with a runtime error if they fail to unwrap the supplied value. A runtime error does not give any meaningful information to the application calling the contract and makes error handling more difficult. Whenever possible, use unwrap! and unwrap-err! with a meaningful error code.

Compare the functions update-name and update-name-panic in the example below.

(define-public (update-name (id uint) (new-name (string-ascii 50)))
    (let
        (
            ;; Emits an error value when the unwrap fails.
            (listing (unwrap! (get-listing id) err-unknown-listing))
        )
        (asserts! (is-eq contract-caller (get maker listing)) err-not-the-maker)
        (map-set listings {id: id} (merge listing {name: new-name}))
        (ok true)
    )
)

(define-public (update-name-panic (id uint) (new-name (string-ascii 50)))
    (let
        (
            ;; No meaningful error code is emitted if the unwrap fails.
            (listing (unwrap-panic (get-listing id)))
        )
        (asserts! (is-eq contract-caller (get maker listing)) err-not-the-maker)
        (map-set listings {id: id} (merge listing {name: new-name}))
        (ok true)
    )
)

It is best to restrict the use of unwrap-panic and unwrap-err-panic to instances where you already know beforehand that the unwrapping should not fail. (Because of a prior guard, for example.)

Avoid the if function

To be honest, you should not actually avoid using the if function. But anytime you use it, ask yourself if you do really need it. Quite often, you can refactor the code and replace it with asserts! or try!. New developers often end up creating nested if structures because they need to check multiple conditions in sequence. Those structures become extremely hard to follow and are prone to error.

For example:

(define-public (update-name (new-name (string-ascii 50)))
    (if (is-eq contract-caller contract-owner)
        (ok (var-set contract-name new-name))
        err-not-contract-owner
    )
)

Can be rewritten to:

(define-public (update-name (new-name (string-ascii 50)))
    (begin
        (asserts! (is-eq contract-caller contract-owner) err-not-contract-owner)
        (ok (var-set contract-name new-name))
    )
)

Multiply nested if expressions can usually be rewritten this way. Just compare this:

(define-public (some-function)
    (if bool-expr-A
        (if bool-expr-B
            (if bool-expr-C
                (ok (process-something))
                if-C-false
            )
            if-B-false
        )
        if-A-false
    )
)

To this:

(define-public (some-function)
    (begin
        (asserts! bool-expr-A if-A-false)
        (asserts! bool-expr-B if-B-false)
        (asserts! bool-expr-C if-C-false)
        (ok (process-something))
    )
)

To match or not to match

match is a really powerful function, but a try! is sufficient in many cases. A commonly observed pattern is as follows:

(match (some-expression)
    success (ok success)
    error (err error)
)

For which the functionally equivalent simplification is nothing more than the function call itself:

(some-expression)

match unwraps the result of a response and enters either the success or failure branch with the unwrapped ok or err value. Immediately returning those values is therefore pointless.

Here is a real transfer function found in a mainnet contract:

(define-public (transfer (token-id uint) (sender principal) (recipient principal))
    (if (and (is-eq contract-caller sender))
        (match (nft-transfer? my-nft token-id sender recipient)
            success (ok success)
            error (err error))
        (err u500)
    )
)

Refactoring the if and match, we are left with just this:

(define-public (transfer (token-id uint) (sender principal) (recipient principal))
    (begin
        (asserts! (is-eq contract-caller sender) (err u500))
        (nft-transfer? my-nft token-id sender recipient)
    )
)