Advanced Equations

It is useful to discuss some of the more advanced programming constructs that Insight Maker's equation engine supports. These advanced constructs are particularly useful for developing complex logic for Macros or Agent Based Modeling.

Basic Programming

Defining Variables

Insight Maker allows the definition and modification of variables (in the programmatic sense, not in sense of "Variable" primitives) using the "<-" operator. For instance, the following code will create two variables — x and xSquared — which will be 20 and 100 respectively at the end of the code's evaluation.

x <- 10
xSquared <- x^2 # xSquared = 100
x <- x*2 # x = 20

Insight Maker uses block scoping, so variables first declared within a block will not be accessible outside that block.

Comments

Insight Maker supports several forms of comments. Comments are parts of equations that will not be evaluated by the equation engine:

  • "#" will comment out the rest of the line:
    1+2^3 # this is ignored
  • "//" will comment out the rest of the line:
    1+2^3 // this is ignored
  • "/*" and "*/" will comment out a region:
    1+/* this is ignored */2^3

Defining Functions

Insight Maker supports several forms for function definition. One is a short form that is shown below:

myFn(a, b, c) <- sin((a+b+c)/(a*b*c))

And the second is a longer form that can create multi-line functions:

function myFn(a, b, c)
  numerator <- a+b+c
  denominator <- a*b*c
  sin(numerator/denominator) # the last evaluated expression is returned from a function, you may also use the 'return' statement
end function

Functions also support default parameter values:

takePower(a, b = 2) <- a^b

takePower(2, 1) # = 2
takePower(3, 3) # = 27
takePower(3) # = 9

If Then Else Statements

In addition to Insight Maker's standard ifThenElse() function, a multi-line form is also supported:

if x > 10 then
  # do something
else if x < 5 then
  # do something else
else
  # some other action
end if

You may have as many "else-if"clauses as desired and the final "else" clause is optional.

While Loops

While loops allow the repetition of expressions multiple times until as long as a logical condition is satisfied. For example:

x <- 10
while x < 20
  x <- x +x/10
end loop
x # = 21.436

For Loops

For loops repeat some code a fixed number of times. For instance:

total <- 0
for x from 1 to 10
  total <- total + x
end loop
total # = the sum of the numbers from 1 to 10

An optional "by" control parameter can be specified to change the step to some other value than one:

total <- 0
for x from 1 to 9 by 2
  total <- total + x
end loop
total # = the sum of the odd numbers from 1 to 9

A special form of the for loop, the for-in loop, exists for iterating across all elements of a vector:

total <- 0
for x in {1, 2, -5, 10}
  total <- total + x
end loop
total # sums up the elements in the vector

Returning Values

A return statement may be used to return values from functions or equations. If a return statement is not used, the last evaluated expression will automatically be returned.

For example:
1+1
2+2
3+3 # 6 will be returned by this equation

Or:

1+1
return 2+2 # 4 will be returned by this equation
3+3 # this won't be evaluated

Or:

if 10 > 20 then
  1*2
else
  2*2 # 4 is returned from both the if-statement and then returned for the expression overall
end if

Error Handling

Insight Maker uses an exception mechanism to handle errors. For instance, say you attempted to access an element of a vector that does not exist:

{1, 4, 9}{5} # There is no element '5' in the vector!

When this occurs, Insight Maker throws what is called an "exception". An exception is basically an error that will propagate up through the model ultimately aborting the simulation unless something handles the error. Handling the error is known as "catching" the error. You can catch errors in your equations using a Try-Catch block. An example of a Try-Catch block is below:

Try
  x <- getVector()
  mean(x)
Catch err
  0
End Try

What this equation does is to first attempt to execute the code finding the mean of the vector. If that code executes successfully, everything between the Catch and the End Try are skipped and not evaluated. But, something very interesting happens if an error occurs when we attempt to calculate the mean.

Supposed the getVector function returns a vector without any elements. The mean of an empty vector is undefined and Insight Maker throws an exception when this occurs. Normally, this would terminate your simulation. However, when an exception occurs in a Try-Catch block, the exception can be "caught" by the second part of the block.

If an exception occurs in our example, the exception is assigned to the variable immediately following the Catch. In this case the variable is err and if we tried to take the mean of an empty vector, err might be assigned the string "Must have at least one element to determine the mean of a vector.". Next the code following the Catch is executed. In our example here, this code is simple and just returns 0, but it could be more complex. Thus, our example attempts to calculate the mean of a vector; and if it cannot calculate the mean, it returns 0 instead.

In addition to Insight Maker's many built-in exceptions you can also create your own exceptions using the Throw keyword. For example, we could do the following:

x <- getVector()
If x.length() = 0 then
  throw "The length of x must be greater than 0."
end if

Your custom exceptions will be handled just like regular Insight Maker exceptions.

Destructuring Assignment

Insight Maker supports destructuring assignment. Though the name is fancy, the concept is simple. Basically, destructuring assignment provides a straightforward way to assign the elements of a vector to a set of variables. For example:

x, y <- {10, 20}
x # = 10
y # = 20

Functional Programming

Functional programming is an approach to programming that focuses on the use of functions rather than variables and procedural logic. Insight Maker supports functional programming as its functions are first class objects that can be created on the fly, assigned to variables, and returned as the results of other functions.

For instance, we could take the built-in Mean() function, assign it to a variable and then apply it to a set of numbers:

myFunction <- mean
myFunction(1, 2, 3) # = 2

Similarly, we could use the Map function with a vector of functions to calculate summary statistics for a set of data values:

{Min, Median, Max}.Map(x(7, 5, 8, 1, 6)) # = {1, 6, 8}

Anonymous Functions

Generally, when programming a function is given a name when it is created, and it can later be referred to using that name. In functional programming, a key tool are anonymous functions: functions created without a name. Anonymous functions are defined much the same way as regular functions, but without an explicit name. For instance, the following creates an anonymous function and assigns it to the variable f:

f <- function(x,y)
  Sqrt(x^2+y^2)
end function

f(3, 4) # = 5

There is also a shorthand syntax available for single line anonymous functions:

f <- function(x,y) Sqrt(x^2+y^2)

f(3, 4) # = 5

Anonymous functions are very useful when using functions like Map() and Filter(). For instance:

{1, 2, 3}.map(function(value)
  cos(value^2)
end function)

Closure

Closure is a key tool for functional programming. Closure is a bit of a technical concept which basically means that functions declared within a scope continue to have access that scope even after it has been released. Let us look at an example that uses closure to generate counter functions:

function MakeCounter()
  countTally <- 0
  function()
    countTally <- countTally+1
    countTally
  end function
end function

c1 <- MakeCounter()
c2 <- MakeCounter()

{c1(), c1(), c2(), c2(), c1()} # = {1, 2, 1, 2, 3}

Looking at this code we should recognize that countTally is a local variable to the MakeCounter function. Once the function is complete, the countTally variable goes out of scope, and we cannot access it outside the function. However, due to closure, the function we declare within the MakeCounter function still has access to the countTally variable even after MakeCounter has finished

Thus we can continue to use the countTally variable in this anonymous function when we call it later on. It is effect now a private variable that only the generated function can access. A new countTally variable is created for each call of MakeCounter, so we can keep track of separate counts. Closure is a powerful tool that has many uses for complex programs

The Elegance of Functional Programming

Functional programming techniques are really quite elegant for many practical programming uses. Take the following example which implements the Lotka-Volterra predator prey model using Insight Makers programming features. Euler's method is used to solve the differential equations. Due to the elegance of functional programming, once we have defined our system, the entire differential equation solver requires just a single loop containing only two lines of code!

state <- {
  Predator: 20,
  Prey: 560
}

derivatives <- {
  Predator: function(state) 0.0002*state.Prey*state.Predator-0.25*state.Predator,
  Prey: function(state) 0.25*state.Prey-0.008*state.Predator*state.Prey
}

startTime <- 0
endTime <- 20
timeStep <- 1

for t in startTime:timeStep:(endTime-timeStep)
  slopes <- derivatives.map(x(state))
  state <- state + slopes*timeStep
end loop

alert(state)

Object-Oriented Programming

Object-oriented programming is a technique where objects are defined in a program. These objects are generally collections of properties and functions that may manipulate the object or carry out some behavior based on the object's state. Insight Maker's equation engine supports what is known as prototype-based object-oriented programming. This is a flexible and powerful technique for building programs using objects.

Creating Objects

Object are based on named vectors. Let's take the following instance of a named vector as an example:

Person <- {
  firstName: "John",
  lastName: "Smith"
}

This "object", which is what we refer to named vectors as in this section, represents a person named John Smith. We can access this person's first and last name using the following syntax:

Person.firstName # = "John"
Person.lastName # = "Smith"

Since Insight Maker's equation engine is a functional language with first class functions, we can also assign functions to the properties of this object. For instance, we could add a function to return the person's full name:

Person <- {
firstName: "John",
lastName: "Smith",
fullName: function()
    "John Smith"
end function
}

We would then obtain the full name of the person object like so:

Person.fullName() # = "John Smith"

However, this function we wrote is not very smart. Our object already has all the information needed to find the person's full name, so repeating the name in the function is redundant. We can do better than this. To do so, we use a special variable: Self. When used in an object's function, Self refers to the object itself. Using this knowledge, we can rewrite our full name function to be smarter. In this new form, the full name function will give the correct full name even if we later change the object's first or last name.

Person <- {
firstName: "John",
lastName: "Smith",
fullName: function()
   self.firstName + " " + self.lastName
end function
}

Inheritance

Unlike some object-oriented languages, in Insight Maker each object is both a fully usable object and also a class definition other objects can inherit from. We use the new keyword to create instances of existing objects.

For example, we could create two new people objects like so:

chris <- new Person
chris.fistName <- "Chris"
john <- new Person

chris.firstName # = "Chris"
john.firstName # = "John"

You can also create multiple levels of inheritance. For instance, imagine we wanted to create a Student class. The Student class will be a subclass of the person class with two new properties: school and grade.

Student <- new Person
Student.grade <- 10
Student.school <- "Midfield High"

chris <- new Student # Chris is both a Student and a Person

Constructors

Constructors are functions that are called when a new instance of a class is created. Constructors are created by defining a property "constructor" in the object definition. If a constructor is available it will be called when a new instance of an object is created. For instance, the following is a constructor that makes it easy to create people with a given name:

Person <- {
  firstName: "John",
  lastName: "Smith",
  fullName: function() self.firstName+" "+self.lastName,
  constructor: function(first, last)
    self.firstName <- first
    self.lastName <- last
  end function
}

chris <- new Person("Chris", "McDonald")
chris.fullName() # = "Chris McDonald"

Parent

The object an instance inherits from is known as its "Parent". A variable by this name is available in the object's functions in order to obtain a reference to the object's parent. This is especially useful for stringing constructors together. For instance, we may want to create a Student subclass of Person which calls its parent's constructor:

Student <- new Person("","")
Student.grade <- 10
Student.school <- "Midfield High"
Student.constructor <- function(first, last, grade, school)
  self.grade <- grade
  self.school <- school
  parent.constructor(first, last)
end function