In this section of the tutorial we will define the core watch logic and
write the time transition functions. We begin by defining
pink and purpleBlue,
the colors used for the watch's gradient background. These numbers are
defined using hexadecimal constants. We also define types for months and
days of week. We then give an alias for a record type which will store
all the information about the clock's current time. We then define some basic
functions for determining if the current year is a leap year, a function
for computing the number of days in a month, a function to transition
from one month to the next, a function to transition from one weekday
to the next, and functions to obtain charlist representations of these
types.
funsecondTick(d : datetime) = {
let {month := month, day := day, year := year, hours := hours, minutes := minutes, seconds := seconds, dayOfWeek := dayOfWeek} = d
We now define a function called secondTick
which given an input datetime, returns a new
datetime advanced 1 second in time. This function
handles all the logic for seconds rolling into minutes, hours into days,
days into months, and months into years.
// To be expanded with Bluetooth Low Energy initialization
}
We now define a mutable global variable which will hold the current clock
state. We also define a setup function, which will be called once when the
CLUE board starts. It simply performs the various initializations required
by the Arcada and graphics libraries.
We now define a global variable to keep the state for
Time:every and the loop function which
will redraw the screen. The loop function begins by clearing the entire
screen with a vertical gradient of our desired color. Then a
we construct a value that holds a value every 1000 milliseconds
(1 second). When that signal holds a value, we transition the
clockState by using the
secondTick function. We then pipe this signal
to another signal processing function called Signal:latch.
The latch function always emits a value on its output
signal given by its current state. When it receives a value on its input signal,
it mutates its state to contain that value. Therefore the output signal passed
to Signal:sink will always hold a value.
/*
Function: sink
Applies the function on the given signal for the purpose of performing
side effects.
Type signature:
| ((a) -> unit, sig<a>) -> unit
Parameters:
f : (a) -> unit - The function to call
s : sig<a> - The input signal
Returns:
Unit
*/
funsink(f : (a) -> unit, s : sig<a>) : unit =
match s {
signal(just(val)) => f(val)
_ => ()
}
As you can see in the above snippet from the Signal module
in the Juniper standard library, the sink function will call
its input function if the input signal holds a value, and will otherwise return unit.
Therefore the purpose of sink is to perform side effects
if the input signal holds a value. Here we are we are extracting the current time,
computing the string representation of the various values, performing string concatenation
where appropriate, moving the cursor around and drawing the text.
Of particular interest are the CharList:i32ToCharList,
CharList:safeConcat, and Prelude:cast
functions.
i32ToCharList converts an integer to a charlist, where
the capacity (maximum size) of the charlist is polymorphic in the return type. The type
of this function is (int32) -> charlist<n>,
so we must specify a return type constraint at the callsite. In this case, the number
of characters in a minute is at most 2.
The safeConcat
function uses type level arithmetic to ensure that the concatenation of two
charlists always has enough capacity. Its type signature is
(charlist<aCap>, charlist<bCap>) -> charlist<aCap+bCap>
/*
Function: cast
Converts a number of one type to a number of another type.
Type signature:
| (a) -> b where a : num, b : num
Parameters:
x : a - The number to convert
Returns:
The number with converted type
*/
funcast(x : a) : b where a : num, b : num = {
var ret : b
#ret = (b) x;#
ret
}
The cast function is used to generically convert
between two numerical types. If the parameter and return type are both bound
by constraints in the program, it can be used to convert between the two types
without annotation from the programmer. Its type is
(a) -> b where a : num, b : num. As we can
see from the definition given in the Juniper standard library, its implementation is
very simple. There are two features introduced here: interface constraints, given after
the where keyword, and the use of
var. Interface constraints are used to add constraints to
generic type parameters. In this case, we insist that the type variables
a and b must be numerical types.
The var keyword is used to define uninitialized variables.
Frequently these variables will be mutated by some inline C++ code, as was done here.