This tutorial is designed to get you acclimated with Juniper as a programming language. We hope that by the end of this guide, you'll feel comfortable with getting started with your first Juniper projects.
All coding samples can be found here:
Juniper is a functional reactive programming language tailored specifically for programming the Arduino chip.
The purpose of Juniper is to provide a functional reactive programming platform for designing Arduino projects. FRP's high-level approach to timing-based events fits naturally with Arduino, with which programming almost entirely revolves around reacting to realtime events. Additionally, since C++ is a very low-level language, Juniper is intended to have better readability on a higher level. Juniper transpiles to Arduino C++, which is then compiled to an Arduino executable.
We designed this language because we believed that FRP was better suited for the Arduino's problem space than C++, thanks to its alternative style and methodology of coding.
As is customary with coding tutorials for new languages, we're going to show to the Juniper "Hello World!" function. Except instead of a computer outputting "Hello World!", the Arduino chip, with an LED component attached, is going to blink the LED on and off every second.
Take a look at this coding sample here:
This is quite daunting if you've never worked in this language before (and even more so, if you've never worked in a function programming language before), so let's break it down, piece by piece.
At line 1, we have the keyword module. Modules are declared files used for storing portions of code. The equivalent in Python would be .py files, and in C++ would be namespaces. Every file used for code in Juniper is a module. At line 2, we have the open() declaration. open() is the equivalent to import in Python, or using in C++. It allows you to use to the declarations and definitions for other data types, functions, and variables that are exported by other modules. So Prelude, Io, and Time are references to other existing modules in the Juniper standard library, with resources used in this module. For almost every module you write you should open Prelude.
Moving onto lines 4-7, we declare certain variables to be used later in this module. let as a keyword allows for the declaration of a new variable. You are essentially saying, "Let this variable be set to this value as we do everything else." The word after the let is the name of the variable. On the right of the equals sign we have the initial value we are assigning to the variable. So, in this sample code, the variable boardLed is initially set to 13 (in this code, boardLed represents which pin on the Arduino chip the LED is connected to). tState is used as a place to record when the timer for the chip last emitted a value, and ledState is used to hold the current pin state of the LED (if it is on or off). These references will be automatically managed by the Time:every and Signal:foldP functions, which will be explained later. See the Datatypes section of this tutorial to learn more about the ref keyword and types used in this section.
At lines 9-18 we have our first function. As in other languages, functions are sets of procedures that take in values as arguments of input, and return a value at the end. The fun keyword denotes the beginning of a function declaration, and the next keyword is the new function name, followed by a set of parenthesis including function parameters (though in this coding sample none of our declared functions have parameters). After a colon, we have the return type of the function (note: same format as declaring variable types in this way!), an equals sign, and then within the next set of parentheses (or, if your function only has one expression, parentheses are not needed) the function definition. Juniper now supports full type inference, so explicitly giving the return type and parameter types is no longer necessary. Within this function, you can declare other variables using let, but the function's last expression must always be the return value of the function. In the case of our first sample function, we declare the function loop() with return type unit (see later in the tutorial for information about the unit type in the Datatypes section), with two new variable declarations within before a final system call for Io:digOut. The purpose of digOut is to do a write to a Arduino board pin of a given label, and the given write value. In this case, we pass boardLed as the pin of the LED, and an ledSig value for what we wish to write to it. This function also returns a unit, and this unit is returned by the loop() function.
Note: Many times in this tutorial you'll see or be using 'Io:' before an actual function name. This denotes that you are currently using a function from another module, in this case the Io Module. This applies to any module you create and open as well. Indicate the module name first, then a colon, then the function or variable you're using from that module.
The setup() function later also functions is a very similar fashion--it is intended to return a unit type, and the Io:setPinMode() function also returns a unit. This function is intended to set a pin on the Arduino chip as a location for input (like a button) or output (like a sound system or display). It takes in as arguments the relevant board pin and the intended pin mode (input(), output(), or inputPullup(), just as they work with regular Arduino programming), and sets the pin mode. While the unit type will be explained later, notice how the two example uses of it in this code are for functions that mutate the state of the Arduino chip, and need not return anything of actual value. The use case is similar to that of the void type in C++.
Let's now take a step back to look at Line 12, because this will be a crucial part of functional programming for the Arduino: folding in time. If you're not familiar with functional programming too much, folding is a common operation where you accumulate a single variable based on a function acting on an initial value for it, and each successive element in a list. For example, one can sum up a list of numbers by setting the function to an addition operation, the initial value to 0, and the list to said list of numbers. In functional reactive programming, we have the foldP operation, which folds based on a signal that updates by events, rather than a list of values. This way, as events happen, the accumulator variable changes by events in time. In this case, the function is set to a lambda (which will be explained in the next paragraph), the initial accumulator to ledState (our initial setting for power to the LED), and the signal to timerSig, which is defined as a timing event triggered every 1000 milliseconds a few lines earlier.
So, briefly, what is a lambda? Well, generally, we declare functions as their own entities, but we don't always have to! In functional programming, having access to first-class functions is a major benefit, where functions are also treated as values that can appear and disappear on the spot. We use the fn keyword to define a lambda, follow-up by a set of parentheses including the function parameters, and then the function body after an arrow (->) symbol. For example, in line 13, we pass a lambda function into the foldP, where the first argument is intended to be an event that traveled along the signal, and the second argument is the current accumulator. It returns the new accumulator value. Our lambda takes in the timer signal event (if there is a new one), and the last led pin state as lastState. It then returns Io:toggle(lastState), which simply returns the opposite digital state of the one given.
So, to sum up this use of foldP, given a timer signal that triggers an event every 1000 milliseconds, an initial value of the initial ledState pin state (which, in this example, is low voltage), and a lambda we build to simply toggle the pin state by events triggered, we fold in time by checking whenever this loop() function is called if a new event has been triggered by our timer signal (1000 milliseconds/1 second has passed). If it has, we toggle the pin state. At the end of our loop() function, we the do a digital write to our pin with this new ledState.
The setup() and loop() functions operate similar to the Arduino setup and loop functions. The setup() function is called once before anything else executes, and the loop function is executed inside of a tight while true loop. The loop function is therefore run endlessly until the Arduino is turned off. In our code every time the loop function is run the timer signal is recomputed. When a second has passed, the LED is blinked on or off, depending on the previous state.
And there you have it! Our first program! I hope we didn't scare you off, because this is quite the daunting Hello World example, but once you understand the methodology, and begin to get acclimated to functional programming on the Arduino platform, you'll find that as projects get bigger, the code becomes more intuitive to write than the C++ equivalent versions. The code is more readable, and events are handled more in line with how most of our minds work through these Arduino projects. It also composes really well, which means that very little will adding new components to a project require all-out restructuring of your code.
Further in this tutorial we'll go more in depth on the different concepts in this language, many of which that we've begun to explore in this first example project.
Note: Beginning in Juniper 2.2, the Signal:toggle function can be used in place of the Signal:foldP function, which significantly reduces the number of lines in this code. This change places it on par with the simplicity of the basic Arduino blink example. However, in the interest of learning, we have decided to keep the more complex foldP example in this tutorial.
This coding sample is very similar to our first Hello World program, except this time, our LED state responds to a button press instead of a fixed time. Instead of a full walkthrough of the code this time, we will instead simply observe the differences in code: