This chapter will introduce the imperative statements that Indigo supports.
Remember: for a full list of the available operators and construct, please refer to reference page.
Getting control (statements)¶
Just as any other imperative language Indigo has common control statements, namely:
but it also has less common ones, like:
- and more...
Let's take a look at the example file included in this chapter:
module Indigo.Tutorial.Statements.Control ( controlContract ) where import Indigo data IncrementIf = IsZero Integer | HasDigitOne Natural deriving stock (Generic, Show) deriving anyclass (IsoValue) instance ParameterHasEntrypoints IncrementIf where type ParameterEntrypointsDerivation IncrementIf = EpdPlain controlContract :: IndigoContract IncrementIf Natural controlContract param = defContract do base <- new$ 10 nat result <- case_ param $ ( #cIsZero //-> \val -> do return (val == 0 int) , #cHasDigitOne //-> \val -> do checkRes <- new$ False while (val > base && checkRes == False) do val =: val / base remainder <- new$ val % base checkRes =: remainder == 1 nat return checkRes ) if result == False then storage =: 0 nat else storage += 1 nat storage :: HasStorage Natural => Var Natural storage = storageVar
This contract is fairly more complicated than the previous examples, but we can slowly take it apart and we'll come to understand it.
First of all we have a new data type
data IncrementIf = IsZero Integer | HasDigitOne Natural deriving stock (Generic, Show) deriving anyclass (IsoValue) instance ParameterHasEntrypoints IncrementIf where type ParameterEntrypointsDerivation IncrementIf = EpdPlain
if you don't remember or haven't read how one such thing is made please take a look at the types reference.
We can see that our contract takes an
IncrementIf as parameter, and since this
is a valid type to use
case_ with, we can use pattern-matching on it.
Except for declaring a
base variable, this is exactly what the contract code
param as argument and then defines a list of
possible patter-matches, in this case
#cHasDigitOne, for its
controlContract :: IndigoContract IncrementIf Natural controlContract param = defContract do base <- new$ 10 nat result <- case_ param $ ( #cIsZero //-> \val -> do return (val == 0 int) , #cHasDigitOne //-> \val -> do checkRes <- new$ False while (val > base && checkRes == False) do val =: val / base remainder <- new$ val % base checkRes =: remainder == 1 nat return checkRes )
We can also see that here
case_ is used to create a new variable
the use of the arrow
<-. This is not necessary to
case_, we could have avoided
this and have each branch
return nothing (aka
()), but in this case we want
to perform a check based on
param and have its result returned.
Let's look at the definition for the first branch, which is to say what follows
( #cIsZero //-> \val -> do return (val == 0 int)
Here we take the
Integer parameter of
IsZero, call it
val, and start a code
do that just returns
This is indeed so simple it could have been defined without the code-block (and
do) in a single line as:
\val -> return (val == 0 int)
Let's look instead at the isolated code for the second branch, the one for
, #cHasDigitOne //-> \val -> do checkRes <- new$ False while (val > base && checkRes == False) do val =: val / base remainder <- new$ val % base checkRes =: remainder == 1 nat return checkRes
Here we take the
Natural parameter of
HasDigitOne and we call it
we start the code-block.
This branch check that
val has at least a digit equal to
1 in base
this logic is implemented should be fairly simple to understand.
In it we can see the usage of
while that acts on a
Boolean expression and
has its own code-block.
An important thing to notice here is the scoping:
- we don't have any problem calling the input to this branch
valeven tho it has a different type and source than the
valfor the previous branch
- we can access
basebecause it is part of the outer-scope
- outside of the branch we cannot access anything declared in it, like
checkRes, because it gets destroyed as soon as we leave the branch
- the code-block of
whilehas an even inner scope, it can access both the outermost (e.g.
base) and the branch's (e.g.
checkRes) scope, but its declarations (e.g.
remainder) cannot be accessed outside of it
Indigo has automatic scope management and behind the scene will
DROP the values
associated with a scope as soon as this is no longer available.
Going back to the contract as a whole for just a second we can see, after
result is calculated, an example of the
if result == False then storage =: 0 nat else storage += 1 nat
Please note the indentation for used here:
if/then/else is not a code-block,
but a single statement, so we could have written it in a single line as well.
This contract is a bit bigger than usual and not much readable as-is, moreover, the situation would get worse if we kept adding to it, especially if there are repeated blocks of code.
For this reason Indigo supports functions and procedures, which are the argument of the next chapter.
Technical details: Imperative statements and scopes¶
The icing on the cake of Indigo are the imperative statements like
case_ and others that can be used in its code.
Let's just take
if_ from the
Indigo.Backend.Conditional module and explain
if_ has the following type:
if_ :: forall inp xs ys a b exc . ( IfConstraint a b , exc :~> Bool ) => exc -> IndigoState inp xs a -> IndigoState inp ys b -> IndigoState inp (RetOutStack a ++ inp) (RetVars a)
First of all it takes three arguments: a boolean expression and
Indigo code for
the true and false branch.
Let's take a closer look at the branches' code and the resulting one.
Branches can end up with different stack types, but we have to unify them somehow
to fit in the same output stack type.
This unification is obvious and comes from imperative languages, where there is
the notion of scopes.
If the execution leaves a scope all variables defined within it are destroyed.
The same thing happens here and this is why, as explained before, we defined
GenCode to also contain the Lorentz code able to return to the input stack.
This way constructs like
if_ can remove the variables and types pushed by a
branch, but there is still a feature missing.
As you can see by the return type this is defined by
RetVars, these (supported by the
IfConstraint) allow for the ability to
return something from that scope, without it getting destroyed.
You can find this defined in the
While what we described above is true for other statements as well,
specifically has one last bonus feature: replacing Haskell
syntax, thanks once again to
Technical details: IndigoM freer monad¶
It's time to complete the full picture of Indigo machinery.
IndigoState was explained, and theoretically, you could write Indigo code
in the bare
IndigoState monad, using
>>= operator with
and you would get all the benefits of automatic stack management.
However, one small drawback of this would be the necessity to specify
input and output stack types in type annotations in some tricky usages of
A more serious problem is that the fixed stack types in the annotations
take away the possibility to do arbitrary logic-preserving transformations to
the stack from the Indigo optimizer.
So, in order to hide these parameters from the Indigo programmer, we introduce the IndigoM monad.
First of all, let's define
data Program instr a where Done :: a -> Program instr a Bind :: Program instr a -> (a -> Program instr b) -> Program instr b Instr :: instr a -> Program instr a
This is what's called "operation style" freer monad.
There is a package on Hackage.
There are two things which may attract your attention:
Program resembles the
Monad interface and the
instr type parameter.
Program is basically a monad but one that keeps the whole flow
of execution as a datatype: every constructor either corresponds to
bind or just an effect.
instr type parameter represents a range of possible effects.
Let's say we have the following oversimplified GADT, which represents all possible statements in Indigo code:
data StatementF a where NewVar :: Expr x -> StatementF (Var x) SetVar :: Var x -> Expr -> StatementF ()
According to this GADT, we can just create new variables and set values to existing ones.
Program StatementF we could write simple programs in this monad,
the only thing which lacks for our convenience is a monad instance for
which actually can be straightforwardly defined, and then you can enjoy
instance Monad (Program instr) where return = pure (>>=) = Bind
Now, let's introduce the
if statement, which complicates the definition of
StatementF a bit:
data StatementF (freer :: Type -> Type) a where NewVar :: Expr x -> StatementF freer (Var x) SetVar :: Var x -> Expr x -> StatementF freer () If :: Expr Bool -> freer a -> freer a -> StatementF freer a
As you can see, a new
freer type parameter is added.
if statement, unlike simple
SetVar statements, contains two branches:
one for a case in which a guard expression is evaluated to true, and another
branch for false, both of which are sequences of statements.
However, for sequences of statements we already have
Program monad, and that's it:
we got mutually recursive construction,
StatementF is a building block of
Program, representing all possible statements, and on the other hand,
Program is used in
StatementF in statements, which contain other subprograms.
You could wonder: why just not to use
If you try to do so, you may experience difficulties with mutually recursive types, like
If :: Expr Bool -> Program (Statement (Program ...)) a -> Program (Statement (Program ...)) a -> StatementF a
To dispel your possible confusion, let's gather everything together, getting the resulting type:
newtype IndigoM a = IndigoM (Program (StatementF IndigoM) a)
which automatically derives its
Monad instance from
As a result, we've just built a "better" version of
IndigoState, which doesn't expose input and output stacks.
The full version of
StatementF can be found in the
module and of
Program in the
Let's discuss a bit how
IndigoM is connected with the rest of the machinery.
IndigoM can be traversed and converted into the typed version of Indigo,
that is to say,
You can take a look at the details of this conversion in the
module, that utilizes an additional datatype,
SomeIndigoState, that basically
hides the type of the output stack.
The idea of this conversion is straightforward: by having the input stack type and
Program, we can reconstruct the output stack type, by sequentially
IndigoState statements corresponding to
The main reason why this possible is that every statement doesn't depend on a specific input stack type, and can be executed against any stack.
IndigoM monad is a frontend for the Indigo language exposed to the user,
and this frontend representation is compiled to the internal, strictly typed,
This approach makes it possible to perform optimization behind the scene.
The only thing that remains to be mentioned is
IndigoContract param storage is just a type synonym for
Var param -> IndigoM ().