Variables

A key concept common in almost every programming language is variables. Just like in mathematics where you might say 3x = 24 (where x is a variable equal to 8), variables in Shadow hold values. However, unlike in mathematics (where variables have fixed and sometimes unknown values), Shadow uses imperative assignment. This means that when a variable is assigned, its value (or what it points at) changes.

Numeric types

Key numeric type variables, which are examples of primitive variables, are listed with their sizes and ranges below:

Signed Types

Type

Size (Bytes)

Minimum Value

Maximum Value

Example Literal

byte

1

-128

127

64y

short

2

-32768

32767

1000s

int

4

-2147483648

2147483647

15

long

8

-9223372036854775808

9223372036854775807

0L

The rightmost column of this table shows example literals for each of these types. A literal is a concrete value stored into a variable. In Shadow, int literals are written as regular numbers (optionally marked with an i at the end), but literals for the byte, short, and long types are marked with an uppercase or lowercase y, s, or L, respectively, at the end of the literal.

In addition to integer types, Shadow also has two types of primitive variables for storing floating-point values like 98.6 or -204.418: double and float. Typically, double is the appropriate type for floating-point values, since it has much more precision than float. If you want to use literals of type float, write them ending in f or F, as in 98.6f. Floating-point values can be written in scientific notation for especially large or especially small numbers. For example 6.02E23 means 6.02 × 1023 .

Warning

Neither integer nor floating-point literals should be written with commas to separate multiples of 1,000 as is typically done in mathematics. For example, the number 3,238,461 must be written 3238461 in Shadow.

Unlike Java, Shadow has unsigned types for primitive variables as well. For example, the unsigned version of int is uint. Unsigned values can never be negative. Due to strict Shadow type-checking, explicit casting is needed if you want to store an uint in an int or vice versa. Exercise caution when using unsigned variables because their behavior can be unexpected when the value would normally become negative.

Note

There is no udouble or ufloat type!

Unsigned Types

Type

Size (Bytes)

Maximum Value

Example Literal

ubyte

1

255

128uy

ushort

2

65535

1000us

uint

4

4294967295

15u

ulong

8

18446744073709551615

0uL

As an example, a simple variable declaration of type int looks like this:

int age = 20;

The variable age is of primitive type int and holds the literal value 20. If you had a birthday and wanted to update your age, you could write the following line of code:

age = 21;

Now, the variable age is updated and holds the literal value 21. Notice that you would not write :

int age = 21;

This code would not compile because the age variable is already declared and cannot be declared twice. You are not trying to create a new age variable; you are simply changing its value.

The boolean type

Outside of the numeric primitive variables, there is a type called boolean. A boolean variable can hold one of two values – true or false.

For example,

boolean isBeautiful = true;

The variable isBeautiful is of primitive type boolean and holds the literal value true. It might seem strange to use a whole type just to hold true and false, but such a type can be useful when making choices or repeating an action, as discussed in Flow Control and Looping.

The code type

There is one other primitive type: code. Similar to char in Java, a code represents a single character. The declaration of a code variable is as follows:

code grade = 'd';

The variable grade is of primitive type code and holds the literal value 'd'. Make sure you put the character in single quotes in order for it to be recognized as a code.

If you’re familiar with Java, you may be wondering how code is different from char. It all comes down to Unicode, which is a collection of standards for encoding characters. Java uses the UTF-16 standard, meaning that each character is represented using two bytes, while Shadow uses UTF-8, which is a variable-size encoding. Even though a variable number of bytes are used, a single code variable always takes up four bytes in order to ensure the largest characters can be stored in it. When a group of characters is stored in a String, however, they only use as many bytes as needed.

Note

code characters do not have to be letters. They could be digits, punctuation, or special characters like '$'.

The String type

The String type is not a primitive type, but it is still fundamental to Shadow programming. While a code value holds exactly one character, a String can hold a list of characters. This list can be as short as no characters – what is called the empty String, written "" – or as long as millions of characters.

In other words, the String type is used to hold arbitrary amounts of text. Note that code literals are marked with single quotes ('), but String literals are marked with double quotes ("), as show below.

String name = "Olivia"; // You must put the characters in double quotes

Unlike an int or double variable, a String variable holds a reference (a location in memory) pointing at an object. Thus, two or more different String variables could point at the same String object. Every reference type (which is any type other than the 12 primitive types) behaves in this way.

Declaring variables

The following short example program demonstrates basic principles for declaring and assigning variables, as well as some information on formatting output with Console.printLine().

import shadow:io@Console;

/* This is a short program that demonstrates how to the declare the variable
 * types defined above.
 */

class VariableExample
{
    public main( String[] args ) => ()
    {
        String restaurantName = "Taco Tuesday";
        boolean isHungry = true;

        String meal = "Meat and Bean Burrito";
        int quantity = 2;
        double price = 5.50;

        Console.printLine("I love eating at " # restaurantName # ".");
        Console.printLine("I would like " # quantity # " " # meal # "(s).");
    }

}

The output is as follows:

I love eating at Taco Tuesday.
I would like 2 Meat and Bean Burrito(s).

Let’s consider a few different Shadow features and conventions demonstrated by this program.

Camel case notation

String restaurantName = "Taco Tuesday";
boolean isHungry = true;

Note how these variables are named. For example, restaurantName is a String variable. We did not name it RestaurantName or restaurantname. Although using these names would not cause a compiler error, it is good programming practice to use camel case notation in which the first word in a variable name begins with a lowercase letter and the rest begin with uppercase letters. This practice is used in Shadow because variable names cannot contain spaces, so capitalization is used to make each word in the variable name distinct. The boolean variable isHungry is declared in the same way. In addition to using camel case notation, make sure your variable names are descriptive of their purpose. In this case, if this was a program for a restaurant, isHungry might be used to tell if a certain customer is hungry.

Warning

Your code will not compile if you have spaces in variable names, such as restaurant name.

More naming conventions

String meal = "Meat and Bean Burrito";
int quantity = 2;
double price = 5.50;

There are a few more key naming conventions for Shadow.

  • Starting a variable name with a digit will cause a compiler error, but using digits after the first letter is acceptable.

  • Single-word names should be all lowercase (price, meal, or quantity), but is not a compiler error to do otherwise.

  • Using other symbols in variable names (#, _, @, %, +, etc.) will cause a compiler error.

  • Variable names cannot be reserved words (see next section).

Formatting output

Console.printLine("I love eating at " # restaurantName # ".");
Console.printLine("I would like " # quantity # " " # meal # "(s).");

Console.printLine() is used to display text on the console. Literal text goes inside quotations marks (" "), but you are able to print variable values as well. If you want to join together text with the values of variables, you can use the cat operator (#) to do so. As seen above, "I love eating at " # restaurantName # "." joins together the String literal "I love eating at " with the contents of the variable restaurantName and finally joins the short String literal "." to the end of the result.

Later, you could change the value of restaurantName as follows:

restaurantName = "Taco Wednesday";

Then, using Console.printLine("I love eating at " # restaurantName # "."); will output I love eating at Taco Wednesday.

Reserved words

In Shadow, as with most programming languages, there are reserved words. Reserved words inherently have meaning in Shadow. For example, double is a reserved word because Shadow recognizes this as a primitive type – it has meaning. Thus, you will get a compiler error if you try to name a variable with a reserved word. See the chart below for a full list of reserved words in Shadow.

abstract

and

assert

boolean

break

byte

case

cast

catch

check

class

code

constant

continue

copy

create

default

destroy

do

double

else

enum

exception

false

finally

float

for

foreach

freeze

get

if

immutable

import

in

int

interface

is

locked

long

null

nullable

or

private

protected

public

readonly

receive

recover

return

send

set

short

singleton

skip

spawn

super

switch

this

throw

true

try

ubyte

uint

ulong

ushort

var

weak

while

xor

The var keyword

In all examples in this section, the variables are declared with an explicit type and name. Like C# (and similar to the auto keyword in C++11), Shadow provides a var keyword that can be used to declare local variables as long as they are assigned an initial value. This can be done because a variable’s type is generally obvious, and the compiler will not confuse a double value with a String.

var milesRun = 26.2;
var marathonCity = "Boston"

As you can see, milesRun is clearly a double, and marathonCity is a String. Going forward with the tutorials, variables will usually be declared using var in examples. Note that there is no type var! The variable milesRun has type double, but the programmer used var as a shorthand instead of stating the type explicitly.

Primitive casting

Shadow is a strongly typed language, which means that it usually isn’t possible to store values of one type into variables of a different type. For example, it’s not permitted to store a double value like 8.316 into an int variable. The compiler shows an error if you try to do this because you’d lose the digits after the decimal point.

There are exceptions to this rule. It’s legal to store a narrower type into a broader type. In other words, you could store an int value like -37 into a double variable. The double type is broader than the int type, so there’s no risk you’ll lose information.

There are, however, situations where you have a broader type and need to store it into a variable of a narrower type. In these situations, Shadow provides a tool called casting that allows you to change the type of an expression into a different type. By using the cast keyword, you are overriding the rules of Shadow, so there are risks that you will lose information or end up with a value whose meaning is different.

The syntax for casting is as follows:

cast<resultType>(expression)

The resultType is the type you want to change expression to.

Numeric casting

Consider the short segment of code below:

1
2
3
4
double a = cast<double>(8);
Console.printLine(w);
double b = 8;
Console.printLine(x);

Lines 2 and 4 both print 8.0 to the console. In both cases, we’re converting the int value 8 into the double value 8.0. On Line 1 we’re performing an explicit cast by using the cast syntax shown above, using the cast keyword. On Line 3, we’re using an implicit cast where Shadow automatically converts the int value into a double value, because it knows that no data will be lost.

Now, let’s look another example:

1
2
3
4
int x = cast<int>(8.7);
Console.printLine(y);

int y = 8.0;

On Line 1 of this code segment, we’re using an explicit cast to convert the double value 8.7 to an int value. Line 2 will print 8 to the console because the conversion truncates the value instead of rounding it.

Note

If you want to round a double value instead of truncating it, you must call its round() method before coverting to an int.

Line 3 of this code segment, however, doesn’t work. Mathematically, there’s no difference between 8.0 and 8, but for Shadow, 8.0 is a double, which can’t be stored into an int variable without an explicit cast. Storing a signed type into an unsigned type will also always require an explicit cast, and storing an unsigned type into a signed type will require one unless the signed type is large enough to hold the unsigned type without overflowing.

Casting code values

Both a code and an int are stored as 4-byte quantities inside the computer. However, the compiler treats these values differently, as either a character or an integer.

Consider the code below:

var digit = '7';
var number = cast<int>(digit);
Console.printLine(number);

In this example, we are casting the code '7' into an int and storing it in number. You might expect that number now holds the numeric value 7, but it actually holds 55. The character '7', when converted to a number, is 55. Programmers rarely need to know the numerical values of characters, but it is possible to look them up in Unicode reference documentation.

A similar issue applies when converting an int to a code as in this example:

var anotherNumber = 97;
var letter = cast<code>(anotherNumber);
Console.printLine(anotherNum2);

The character corresponding to the numeric value 97 is 'a', so a is what’s printed to the console. Always be careful and intentional when casting between primitive types.

Note

You may not cast a String to a code or vice versa.

It can be useful to remember that the numerical values of the uppercase Latin letters 'A' through 'Z' are sequentially numbered. Thus, the value of 'B' is larger than the value of 'A' by exactly 1, and the value of 'C' is larger than the value of 'A' by exactly 2. Similarly, the lowercase Latin characters 'a' through 'z' are also sequentially numbered (and are, perhaps strangely, larger than the values of the uppercase Latin letters). Finally, the numerical values of the digits '0' through '9' are also sequentially numbered. Using if statements, which will describe in a future tutorial, a programmer can employ this knowledge to see if a particular character belongs to one of these three categories.

public printCharacterType(code character)
{
    if(character >= 'A' and character <= 'Z')
        Console.printLine("Uppercase");
    else if(character >= 'a' and character <= 'z')
        Console.printLine("Lowercase");
    else if(character >= '0' and character <= '9')
        Console.printLine("Digit");
    else
        Console.printLine("Something else");
}

nullable and check

To conclude this section on variables, we’ll mention the nullable modifier and the associated check command. Many programming languages like C, C++, and Java allow reference types, including String, to be assigned the special value null, which means that the reference isn’t pointing at any object. However, those who are familiar with such languages will know that null can cause many unintended errors in a program, leading to a NullPointerException in Java, for example. Shadow deals with this issue with the nullable modifier. If a reference is marked as nullable, it means that it’s able to store the value null. For example:

nullable String word = null;

The variable word is a nullable String reference that contains null. However, what if we tried to write a similar assignment without the nullable keyword?

String word2 = null;

This assignment would cause a compiler error because word2 is a non-nullable reference. Although creating nullable references can circumvent some issues with using null, the goal is to have as few nullable references as possible – using them only when absolutely necessary.

Since you cannot store a nullable reference into a regular reference or call any methods on it, we use the check command to convert a nullable reference into a regular reference. The check command takes one nullable expression as a parameter and returns a non-nullable object. For example, consider the following lines of code:

nullable String hint =  "machine";
String mystery = check(hint);

What’s stored in the non-nullable String variable mystery? The literal value "machine". The check command took in the nullable variable hint and returned a non-nullable version of it. But what would have happened if hint had contained null? The program would have crashed with an error, displaying shadow:Standard@UnexpectedNullException on the console. Exceptions will be covered in a later tutorial where we will also show how to create a block of code to handle a nullable variable in a safe way.