Basics and Variables

This chapter will explain the basics necessary to write an Indigo contract and the syntax for variable manipulation.

Contract definitions

Breaking down the example

There is a new Example.hs source file for this chapter:

module Indigo.Tutorial.Basics.Example
  ( exampleContract
  , textKeeper
  ) where

import Indigo

exampleContract :: IndigoContract Integer Integer
exampleContract param = defContract do
  a <- new$ 1 int
  storageVar =: param + a

textKeeper :: IndigoContract MText MText
textKeeper param = defContract do
  storageVar =: param

here we can see the same exampleContract as before and a new one: textKeeper.

Let's ignore textKeeper for now and take a deeper look at exampleContract. Its very first line is:

exampleContract :: IndigoContract Integer Integer

This is a type signature: it says that we are going to define exampleContract, which is (aka has the type of, aka ::) an IndigoContract that takes an Integer parameter and has an Integer storage. Before every contract definition, we need to specify its type signature, it's always going to look like this:

<contract_name> :: IndigoContract <parameter_type> <storage_type>

Not that complicated, right?

Let's go to the second line:

exampleContract param = defContract do

this is the start of our contract definition, we define exampleContract and say that it takes a parameter variable (in this case we call it param), we then say the contract definition starts here defContract and we begin a code block with do.

This is even more standard than the first line, the only thing that may differ with other contracts here is the contract name (obviously) and the name of the parameter variable (in case we want to use something other than param).

The indented lines that follow are the actual contract code. In this case:

  a <- new$ 1 int
  storageVar =: param + a

these lines show both methods of direct variable manipulation: - we can create a new variable by using new$, on its right we put the expression we want to assign to the new variable (1 int) and on the left of the arrow <- the name we'll be referring to with (a) - we can set an existing variable to a new value by using =:, on its left we put the variable we want to set (storageVar) and on its right the expression whose result we want to assign to it param + a.

You may have noticed that we never defined storageVar; we don't need to because storageVar is a special variable that is always available in contracts.

You may have also noticed that we had to specify int after 1, this is always necessary because numeric types are otherwise ambiguous; a numeric literal, like 1, needs to be followed by either int, nat or mutez to clarify that its type is Integer, Natural or Mutez, respectively.

Making a new contract

Let's try to implement textKeeper (without cheating and looking at it!), we want it to be a contract that takes a string parameter (MText type) and has a string storage. It does one simple thing: it stores the parameter into its storage.

You can define it into the REPL, but it will fail when you'll give it the type signature (remember: the first line), because it tries to evaluate each line and it cannot find a definition for it. This is quickly solved by using a code block, like this:

:{
<multiple_lines_definition>
:}

If you've used the configuration file included in the previous chapter you will also be able to see if you are in a code block or not by the different color and text of the prompt.

After you have implemented your textKeeper you can check that it looks like the one from the Example.hs file:

textKeeper :: IndigoContract MText MText
textKeeper param = defContract do
  storageVar =: param

and you can use printAsMichelson or saveAsMichelson on it just as if you had loaded it from said file.

At this point you may be wondering why string's type is MText and numbers' types are Integer or Natural. The reason is that Indigo uses a Haskell type for each corresponding Michelson one. You can find more info and a complete list of types in the references documentation, that also explains how to create your own types from scratch.

On the other end you are probably curious about what are the expressions and operators available in Indigo beside +. If this is the case you are in luck, because it is exactly the topic of the next chapter!

Technical details: IndigoState and Var

The core of Indigo is encoded in its IndigoState type.

In short we want a state monad that stores a list corresponding to the stack. Each element of the list is the index of a variable (if any) referring to the corresponding stack element.

Also, we want it to be strongly typed and linked to the Lorentz type we'll eventually compile it to.

For starters we define a typed Var:

newtype Var a = Var Word

You can see that Var has a phantom type parameter, which is the type of the element in the stack that it refers to.

We then propagate this to StkEl, that represents an element on the stack with or without a Var associated with it:

data StkEl a where
     NoRef :: TypeValue a => StkEl a
     Ref :: TypeValue a => Var a -> StkEl a

(don't pay attention to the TypeValue constraint at this stage, it's a detail that will be explained later on).

We can now define the heterogeneous list of StkEl:

type StackVars (stk :: [Kind.Type]) = Rec StkEl stk

So now we could have a definition for IndigoState that looks like this:

newtype IndigoState stk a = IndigoState (State (StackVars stk, Word) a)

but we don't, because if we tried to connect two subsequent IndigoState expressions via >>=, we'd be forced to use the same stk for both. This is a problem because the second one should instead have an stk that depends on the result type of the first.

Hence we would like to have an IndigoState akin to the State monad above that also has an input and output stack types as its type parameters. Something that looks like:

newtype IndigoState inp out a = ...

and a bind operator with a type like this:

(>>=) :: IndigoState inp out a -> (a -> IndigoState out out1 b) -> IndigoState inp out1 b

such that it would glue together two expressions with matching types.

The problem is that this is not the bind for State and in fact it doesn't even match the bind from the Monad type class. Fortunately, there is something known as the indexed monad which is basically a State monad that returns the state of a new type everytime.

Getting closer to our case, we add specific types to obtain the actual definition of our IndigoState indexed monad:

newtype IndigoState inp out a =
  IndigoState {runIndigoState :: MetaData inp -> GenCode inp out a}

Although it may look scary, it's basically a state monad that consumes the references on the stack and returns new references, a value and the Lorentz code generated during its execution.

You can get more details in the Indigo.Internal.State module, where MetaData is the consumed type and GenCode is the resulting one. You'll see that MetaData also contains the number of Var allocated up to that point and that GenCode also contains the Lorentz code to remove everything allocated during its execution.

In this module you can also see the definition of the >>= operator that, with the RebindableSyntax extension enabled, can be used in do notation. This module however does not contain new and =:, introduced above, nor the code to lookup variables on the stack. These are located in specific modules and will be explained in the following chapters.

In Indigo we also use the BlockArguments extension to remove the $ before the do notation as well, which results in being able to use this syntax:

exampleContract param = defContract do

instead of:

exampleContract param = defContract $ do