Scala/Basic types

From Wikibooks, open books for an open world
Jump to navigation Jump to search

Static typing[edit | edit source]

Scala is a statically typed language. This means that when Scala programs are compiled, the compiler verifies that the operations on objects are valid according to their types, and if they are not, a message regarding the type error is given. A simple example of a program that is rejected due to type errors:

println(" " * 4.5) //ERROR: Required "Int" but found "Double".

The string method "*" is not defined for real numbers, so when the compiler is compiling the program, it fails to find any version of the "*" method on strings that takes a real number as argument, and it therefore outputs an error message. The error message depends on the compiler and its version, but generally includes the position, the specific type error, and other useful information.

Fixing the program by giving an integer value gives us the following program:

println(" " * 4) //Prints "    ".

Since the string method "*" is defined for integers, the compiler verifies that the types are valid, and accepts the program.

Type annotations and type inference[edit | edit source]

The type of values, variables and expressions can be indicated by using type annotations:

val someInteger = 3:Int
val someReal = 4:Double
//val someString = 5:String //ERROR: Required "String" but found "Int".

In the first line, we declare a value "someInteger" that is assigned the expression "3:Int". The colon followed by "Int" is a type annotation that specifies that 3 is of type "Int". The compiler verifies that this is valid during type-checking. In the second line, we declare a value "someReal" that is assigned the expression "4:Double". While "4" is normally interpreted as an "Int", the type annotation means that it is instead interpreted as a "Double". In the third line, we declare a value "someString" that we assign the expression "5:String". However, "5" cannot be interpreted as a "String", and the type-checking fails with an error message from the compiler.

In the above example, the type annotation for "3" was not strictly required. Type annotations are often optional, and can be placed multiple places:

val someInt = 4 + 6
val someInt2:Int = 4 + 6
val someInt3 = (4 + 6):Int
val someInt4:Int = (4 + 6):Int
val someInt5:Int = (4 + 6:Int):Int

The reason type annotations are often not required is because Scala supports type inference, which means that it can often infer the types based on the different elements. Type annotations can be used as a guide to the type inferrer, for instance when it is having difficulty inferring a type and or when it has inferred the wrong type.

Examples of places where type annotations are required include the arguments of function definitions and the return types of recursive functions.

Numeric types[edit | edit source]

Boolean[edit | edit source]

The "Boolean" has only two possible values: true and false. The keywords "true" and "false" are the literals for "Boolean":

val thisIsTrue = true
val thisIsFalse = false

There are several operators on boolean values. The most common unary operator is complement or negation, which in Scala is implemented by the "!" method:

println(!true) //Prints "false".
println(!false) //Prints "true".

Two of the most common binary operators are conjunction, or "and", and disjunction, or "inclusive or". In Scala, conjunction and disjunction are implemented by respectively the "&&" and "||" methods:

println(true && true) //Prints "true".
println(true && false) //Prints "false".
println(false && true) //Prints "false".
println(false && false) //Prints "false".
println(true || true) //Prints "true".
println(true || false) //Prints "true".
println(false || true) //Prints "true".
println(false || false) //Prints "false".

Other binary operators include equal (implemented by "=="), not equal (implemented by "!=") and exclusive or (implemented by "^").

The Boolean type is often useful for making decisions. A simple example is the if-then-else expression that uses an expression of type Boolean to decide which branch to evaluate:

val result = if (true && (false || true) && !false) 3 else 5
println(result) //Prints "3".

Integral[edit | edit source]

The integral types describe numbers that are discrete, ie. they do not have any fractions. The different integral types vary in the range of numbers they can represent. They include "Byte", "Short", "Int", "Long", "BigInt" and "Char". The below table describes the range of discrete numbers each type can represent:

Type Lowest number Biggest number
Byte -27 27 - 1
Short -215 215 - 1
Int -231 231 - 1
Long -263 263 - 1
BigInt -infinity infinity
Char 0 216 - 1

Byte, Short, Int and Long[edit | edit source]

"Byte", "Short", "Int" and "Long" are used to represent numbers or data. They support simple arithmetic operators such as negation, addition, minus, multiplication, division and modulus:

println(-2) //Prints "-2".
println(2 + 5) //Prints "7".
println(2 - 5) //Prints "-3".
println(2 * 5) //Prints "10".
println(2 / 5) //Prints "0".
println(2 % 5) //Prints "2".
println(-(2:Byte)) //Prints "-2".
println((2:Short) + (5:Int)) //Prints "7".
println((2:Byte) - (5:Long)) //Prints "-3".

Note that arithmetic operations involving "Byte", "Short" and "Int" but not "Long" are converted to "Int", while operations involving "Long" and any of the other types are converted to "Long". Modulus can give negative values, and division and modulus are designed such that the equality "b * (a/b) + (a%b) == a" generally holds, even for negative "a" and "b". The second argument to division or modulus may not be 0:

//println(3 / 0) //ERROR: Throws an arithmetic exception.
//println(3 % 0) //ERROR: Throws an arithmetic exception.

Relational operators are also supported, such as less-than, less-than-or-equal, equal and not-equal:

println(3 < 5) //Prints "true".
println(3 <= 5) //Prints "true".
println(3 == 5) //Prints "false".
println(3 != 5) //Prints "true".

Other relational operators include greater-than and greater-than-or-equal, which are implemented by ">" and ">=" respectively.

Bitwise operators include "~", "&", "|" and "^", and bit-shift operators include "<<", ">>" and ">>>".

Literals can be declared by writing out the number. Alternative notations include octal and hexidecimal:

println(10) //Prints "10".
println(010) //Prints "8".
println(0x10) //Prints "16".
println(0X10) //Prints "16".

In the first line, "10" is printed. In the second line, we write the octal number "10" by prepending the number with "0", and "8" is printed, which is the corresponding decimal number. In the third lin,e we write the hexadecimal number "10" by prepending the number with "0x", and "16" is printed, which is the corresponding hexadecimal number. The fourth line is like the third line, except "0X" is used to prepend with instead of "0x". Note that numbers are always printed in decimal notation.

Apart from the prefix "0x" or "0X", hexadecimal literals consists of normal digits as well as the letters "a" through "f". There is no difference between lower case or upper case:

println(0xff) //Prints "255".
println(0xFF) //Prints "255".
println(0xA0) //Prints "160".
println(0x10aB) //Prints "4267".

"Long" literals are indicated by appending the number with "l" or "L":

println(100l) //Prints "100".
println(077L) //Prints "63".
println(0xFFL) //Prints "255".
//println(2147483648) //ERROR: integer number is too large.
println(2147483648L) //Prints "2147483648".

Integer overflows and underflows are what happens when somewhere in an integral expression, the number calculated becomes too large or too small for the integral type to hold:

println(2147483647) //Prints "2147483647".
println(2147483647 + 1) //Prints "-2147483648".
println(-2147483648 - 1) //Prints "2147483647".

In the first line, the maximum number for "Int" was printed. In the second line, the maximum number for "Int" plus one was printed, wrongly giving the minimum number for "Int". In the third line, the minimum for "Int" minus one was printed, wrongly giving the maximum number for "Int". Underflows and overflows do not generally cause an exception, but results in the wrong result.

The normal procedure to avoid overflows and underflows is to ensure that the integral type used is always large enough to hold the expression. This must be done at every step in the computation:

val wrongResult:Long = 24 * 60 * 60 * 1000 * 1000
println(wrongResult) //Prints "500654080".
val rightResult:Long = 24L * 60 * 60 * 1000 * 1000
println(rightResult) //Prints "86400000000".

In the first line, the integer literals in the expression are interpreted as "Int". Because "Int" is not big enough to contain all the intermediate results, overflows happen and give the wrong result. After the wrong result is computed, it is converted to "Long" and stored in the value "wrongResult". In the second line "wrongResult" is printed, giving the wrong number "500654080". In the third line, the first integer literal is indicated to be of type "Long". Since operators are left-associative, and expressions involving "Int" and "Long" has the type "Long", the type for the intermediate results is "Long", and is big enough to contain the resulting numbers, with no overflows happening. The result is stored in the value "rightResult". In the fourth line, "rightResult" is printed, giving the right number "86400000000".

BigInt[edit | edit source]

BigInt supports arbitrary precision, meaning that it can represent all discrete numbers given enough memory is available:

//val someNum = 100000000000000000000L //ERROR: integer number is too large.
val someNum = BigInt("100000000000000000000")
println(someNum) //Prints "100000000000000000000".
println(someNum + someNum * 2) //Prints "300000000000000000000".

In the first line, we try to declare a number using a "Long" literal, but the number is too big to be held in a "Long", and the compiler gives an error. In the second line, we declare the same number using a string, and assign it to the value "someNum". In the third line, we print the value "someNum". In the fourth line, we perform basic arithmetic with someNum and an integer literal of type "Int", and print the result.

The operations and memory use of "BigInt" is dependent on the size of the number it is representing, and generally performs slower than the other integral types. Since it has arbitrary precision, computation done purely using "BigInt" never results in integer overflow or underflow.

Char[edit | edit source]

"Char" is used first and foremost for representing characters in strings.

TODO: Expand this section.

Real[edit | edit source]

String[edit | edit source]

Special types[edit | edit source]

TODO: Write something about "Unit", "Any", "Nothing", etc. Maybe also "null". TODO: Discuss associativity.