A Rosetta Stone Approach to learning inferno.
Learning a new syntax can often be challenging. We have tried to make our new inferno scripting language easy to understand but recognize often the best way to learn is to start with something you might be more familiar with. This post is to help those who know our existing scripting language transition to using the inferno language. We will be updating this post with more examples as time goes on.
Bit Setting Scripts
In OnPing, it is often necessary to extract a particular bit from a word of data. Here is an example in script for setting bit 0 in our original Structured Script.
Legacy Structured Script /* This script checks a bit labeled in 'bitToTest'
1.0 if bit is true
0.0 if bit is false
() if bit is not found */
bitToTest := 0;
valToCheckBitFor := latestInput(1);
if (isUnit(valToCheckBitFor)) then
output := valToCheckBitFor;
else
i := round(valToCheckBitFor);
if(isSet(i,bitToTest)) then
output := 1.0;
else
output := 0.0;
end_if;
end_if;
Here is the same example in Inferno.
Inferno Script
/* BitZero checks the bit value of a parameter and tells us whether it's equal to
0 bit or not. 1.0 if bit is true, 0.0 if bit is false, and () if bit is not found
bitParam - series of (a0): Parameter that we are checking the bit value of. */
let bitToTest = 0
in match ((latestValueBefore ?now bitParam)) with {
| Some x ->
let word = toWord32 (round x)
in if (testBit word bitToTest)
then Some 1.0
else Some 0.0
| None -> None }
match and Optional vs isUnit
So lets start out with things that these two scripts have in common. First of all, they both have a concept of a missing value. In OnPing, incoming data is treated as a stream and the user is asked to deal with the possibility of that stream producing no data. Our original language used a function called isUnit to do this, while Inferno uses a more traditional idea from functional programming of an Optional value. If the stream produces no data, then the function asking for the data will return None
otherwise, it will return Some x
. By using pattern matching, we are able to use the value directly.
Table of substitutes:
Purpose | Structured Script | Inferno | Snippet |
---|---|---|---|
Assigning a variable | x := 0.0 | let x = 0.0 | let x = 0.0 in x |
Retrieving an approximate value from the data stream. | input | valueAt | valueAt input0 ?now |
Retrieving a value before current time. | latestInput | latestValue | latestValue input0 |
Retrieving a value before a given time. | latestBefore | latestValueBefore | latestValueBefore ?now input0 |
Checking to see if a value is available. | isUnit | match with |
|
Dealing With Multiple Inputs
In our Structured Script language a script that has multiple inputs would look something like this:
Legacy Structured Script
// Variable assignments
oorah1h := latestBefore(now, 1);
yippi1h := latestBefore(now, 2);
yippi2h := latestBefore(now, 3);
yippi3h := latestBefore(now, 4);
yippi4h := latestBefore(now, 5);
yippi5h := latestBefore(now, 6);
// Conditional statement
if (isUnit(oorah1h)
|| isUnit(yippi1h)
|| isUnit(yippi2h)
|| isUnit(yippi3h)
|| isUnit(yippi4h)
|| isUnit(yippi5h)) then
output := ();
else
output := (oorah1h + yippi1h + yippi2h + yippi3h + yippi4h + yippi5h) `2;
end_if;
Now this sort of script can be handled as individual inputs or an array. Below we will show each version.
Inferno Individual Input Stylematch ( (latestValueBefore ?now yippiOne), (latestValueBefore ?now yippiTwo), (latestValueBefore ?now yippiThree), (latestValueBefore ?now yippiFour), (latestValueBefore ?now yippiFive), (latestValueBefore ?now oorahOne) ) with { | (Some u, Some v, Some w, Some x, Some y, Some z) -> truncateTo 2 (u + v + w + x + y + z) | _ -> 0.0 }
This style shows off the ability in Inferno to name your inputs. There is quite a bit of boiler plate in that version, which we did intentionally to make it match the Structured Script version. Let’s drop some of that boiler plate!
Inferno Input Arraylet inputsArray = [ yippiOne , yippiTwo , yippiThree , yippiFour , yippiFive , oorahOne ] in let total = Array.sum (Array.keepSomes [latestValueBefore ?now i | i <- inputsArray]) in total
Okay that code cleaned up really nicely! It also gives us the chance to talk about a few new constructs in Inferno. I am going to work through them 1 by 1.
First, Inferno has Arrays! Not only that, but you can take each of the input names and reference them together as an array. This is exactly what the inputsArray is.
Secondly, Inferno has what are known as array comprehensions. An array comprehension has a style that looks like this:
[latestValueBefore ?now i | i <- inputsArray]
The [ ...] lets you know the result of this thing is going to be an array also. Stuff to the right of the | are selectors to grab individual elements out of an array and assign names to them using the <- .
On the left of the | you build a function that takes these individual variables as inputs to produce an output for each element that is an input.
Functions and ?resolution
For the next example, we are going to rewrite the OneLocationCommLoss script step by step until we have a script that can be used for 1 or more locations. First, the original script...
OneLocation Comm LossonPingCommLossTimeIndex := 1; onPingCommLossTime := latestInput(onPingCommLossTimeIndex); if (isUnit(onPingCommLossTime)) then onPingCommLossTime := 1440.0; end_if; numberOfCommunicatingDevices := 1; deviceOneIndex :=2; deviceOneAcc := 0; WITH t FROM now - minutes(round(onPingCommLossTime)) TO now EVERY minutes(1) DO a := input(deviceOneIndex,t); IF not(isUnit(a)) THEN deviceOneAcc := deviceOneAcc + 1; END_IF; END_LOOP ; output := 0; IF deviceOneAcc == 0 then output := 1; end_if;
So this script is a workhorse in OnPing. We have a couple inputs onPingCommLossTime
and the tag that will represent a device. We loop over the tag at every minute for the comm loss period. If we get a poll at all then we output a 0 otherwise we output a 1.
Let's start by translating as faithfully as we can to inferno.
Inferno Direct Transfer, One Location Comm Losslet commLossTime = match latestValueBefore ?now commLossMins with { | Some x -> x | None -> 1440.0 } in let deviceInCommFail = fun deviceV -> let numberOfFoundPoints = Array.length (Array.keepSomes ([valueAt deviceV t | t <- Time.intervalEvery (Time.minutes 1) (?now - (Time.minutes (round commLossTime))) ?now])) in if numberOfFoundPoints > 0 then 0 else 1 in deviceInCommFail device
The above script should look pretty darn familiar. We are using some of the stuff discussed earlier, like matching and array comprehensions. But, we are adding something new! A function... deviceInCommFail
. The argument deviceV represents the input that is being passed and used to retrieve a data stream. By making this piece of the script into a function, we can start to add new features. The first feature we want to add is resolution checking.
let commLossTime = match latestValueBefore ?now commLossMins with { | Some x -> x | None -> 1440.0 } in let ?resolution = let minResolution = round (commLossTime / 3.0) in if minResolution < (resolutionToInt ?resolution) then toResolution minResolution else ?resolution in let deviceInCommFail = fun deviceV -> let numberOfFoundPoints = Array.length (Array.keepSomes ([valueAt deviceV t | t <- Time.intervalEvery (Time.seconds (resolutionToInt ?resolution)) (?now - (Time.minutes (round commLossTime))) ?now])) in if numberOfFoundPoints > 0 then 0 else 1 in deviceInCommFail device
So the concept of resolution is well explained in other documents. But one of the really important ideas in inferno is being able to manipulate it in the script. Here we check to make sure that the resolution of the script is smaller than the amount of time we are checking (in fact 3x smaller).
Then, we iterate across the the time interval in resolution blocks. This is nice for 2 reasons... First, we make sure we are using an appropriate approximation and second we can go fast and use that approximation well!
More Info
Obviously there are loads more things to review. For more information about Inferno check our other helpful documents.