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 :: KnownValue a => StkEl a
Ref :: KnownValue a => Var a -> StkEl a
(don't pay attention to the KnownValue
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.Common.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