Chapter 3
Numeric Types & Operations
IntroductionIn this chapter we will start to discuss data types. To keep things simple (F# has many data types, which we will explore in detail), we will focus our discussion on numeric types only. This will provide a good introduction to F#’s typing system, and will provide a context for discussing type-related constructs. What are Data Types?Data types provide a way for us to tell F# how to treat and handle given data elements. When you associate an element with a data type, you’re defining the range of values that the element can assume and the operations in which the element can participate. From the perspective of data types, programming languages are divided into two broad categories: · Dynamically-typed, a.k.a., weakly typed, languages · Statically-typed, a.k.a., strongly typed languages While a discussion of the pros and cons of each category is beyond the scope of this book, we need to note that F# is a strongly typed language. This means that every data element, value, and expression has a valid, well-known type that is fixed when the source code is compiled, i.e., its type cannot vary at run time. Even though F# is a strongly typed language, F# source code only sometimes requires explicit type declarations for symbols. This is because F# employs type inference, which is a mechanism by which F# deduces data types based on implicit contextual information in the source code. Numeric LiteralsNumeric literals in F# are just like numeric literals in most other languages – literal numbers like 4 and 7.1. You can tell exactly what type a numeric literal is from how it is written. In most cases, the letter(s) at the end of the number will determine the type. F# can represent individual bytes, small integers, large integers, floating point values of varying precision, etc. It even supports direct expression of Decimal and Big number types found in .NET languages, but not available in many other languages. Table 3.1 below summarizes each of F#’s numeric data types, along with the F# keyword you can use to define them. Also shown is the type symbol that you can use to force a literal to be interpreted explicitly. The Comment column describes each type’s internal representation.
Table 3.1 When you suffix a numeric constant with one of the type designation symbols, as shown above, you’re instructing the compiler to treat the literal as that exact type. For example, F# normally interprets the literal 100 to be a 32-bit integer. If we want to treat the value as a short (16-bit value), we would write 100s. Of course, F# supports a variety of other data types, including Booleans, characters, strings, records, etc. We will explore each of these data types in subsequent chapters. Expressing Numbers in Alternate BasesF# supports expressing numbers in alternate bases including hexadecimal, octal and binary. Expressing a number in an alternative base does not change the type of the number - it simply changes its form. HexTo express a number in hex, prefix the value with 0x or 0X, e.g., 0x7F, 0XAA. OctalTo express a number in octal, prefix the value with 0o, (a zero followed by a lowercase “o”), e.g., 0o123 (kind of weird). BinaryTo express a number in binary, prefix the value with 0b or 0B, e.g., 0b1001, 0B1100. Variables vs. Value BindingIn many functional programming languages, including F#, we make a semantic distinction between variables and values. In imperative and OO languages, we normally think of variables as labels for computer memory areas that change over time. When the assignment statement “a = 3” is executed in most languages, the memory storage location called ‘a’ has its bits changed to the bits for the value 3. This is standard operating procedure in imperative and OO systems. In contrast to variables, F# prefers the terminology values or value bindings. In F#, when you execute “let a = 3”, you have to think of the symbol, ‘a’, as being a pointer that is changed to point to the new value, ‘3’. Executing the statement changes the pointer, not the memory storage it points to. The old value that ‘a’ pointed to was not overwritten! (If it is not bound to any other symbol, the memory where it was stored will be garbage collected at some point.) We thus say ‘a is bound to the value 3’, not ‘a is changed to the value 3’. The difference between variables and value binding is that a value is considered constant (read-only – never overwritten), whereas a variable is expected to change (be overwritten). The concept of value binding supports one of the bedrock tenets of functional programming: no side effects. One way to achieve “no side effects” is to discourage (as in F#) or prevent (as in other functional languages like Haskell) a value from changing during assignment. You may be wondering how on earth you can write “real” software without side-effects. I ask that you suspend disbelief for a short amount of time and trust that it’s possible, and that F# also provides mechanisms that enable “normal” variable-like behavior. let There be ValuesNow that we understand the difference between variables and values[1], it makes sense to discuss how to define values and initialize them. This is the role of the keyword let, one of the most oft-used keywords in F#. With respect to values, the let keyword performs two jobs: · let binds a symbol to a data value. · let introduces a new symbol scope. Let’s (no pun intended) take a look at an example: let n = 123 // n is a value. let binds the symbol n to the integer 123. If you wanted to test this using the F# Interactive Console, you would type let n = 123;; // Note the ;;[2] Note that in neither of these examples did we specify the type of the value n. F# uses contextual clues and type inference to “figure out” that n is an integer. In this case, the type system had it pretty easy, since the data 123 is an integer. Once symbols are bound, their values cannot change. The only way to “change” the value of a bound symbol is to assign it a brand new value. The following code assigns the symbol x to one value, then subsequently reassigns it to a new value. The old value is abandoned, as is the original symbol’s type. let x = 123;; // x is bound to 123, it’s type is integer. In the last line, the x that is declared on the left hand side of the = sign is considered to be a new symbol. It has the same name as a previously defined symbol and thus supersedes it. The old memory location referenced by the original x (456.0) still exists, but it is no longer controlled through x. This memory is essentially abandoned and will be reclaimed by the garbage collector. let supports a convenient syntax for assigning to multiple identifiers on a single line: let a, b = 100, 200 // a=100, b=200 UnitWhile we are discussing types, it makes sense here to introduce the data type unit. unit is a built-in type that only ever has one value, written like this: (). For example: let x = () F#’s unit type is akin to void in other languages – it is the “no value” indicator; however, since all F# constructs are ultimately expressions, we need a way to represent “expressions that have no real type to evaluate to” or alternatively, “expressions that evaluate to void.” Since void is not a valid value type, F# needs another mechanism to indicate “an expression whose ultimate value is of no interest” – this is the role of unit. From the MSDN documentation on unit: The value of the unit type is often used in F# programming to hold the place where a value is required by the language syntax, but when no value is needed or desired. An example might be the return value of a printf function. Because the important actions of the printf operation occur in the function, the function does not have to return an actual value. Therefore, the return value is of type unit. We will see unit used in a variety of contexts throughout the text. voidNote that F# also supports the keyword void. It is used when interoperating with other .NET languages such as C#. It plays no role in F# otherwise, and is not the same as unit. Understanding F#’s FeedbackRemember that F# only deals in expressions. This means that every complete programming construct has a data type and a value associated with it (after execution). When you enter the following expression in the F# Interactive Console 123 + 456;; you will see the following output (Figure 3.1): |
|
Example |
Comment |
Example |
Comment |
|
let a = 100 + 200 |
Addition |
let c = 123 * 456 |
Multiplication |
|
let b = 456 – 123 |
Subtraction |
let d = 100 / 3 |
Division. Since both parameters are integers, answer (33) is integer. |
|
let e = 100.0 / 3.0 |
Division. Since both parameters are float, answer (33.3…) is float. |
let f = 100.0 / 3 |
Error. Cannot mix float and integer. Use 100.0 / 3.0 or conversion operator. |
|
let g = 10 % 4 |
Modulus |
let h = 5.0 ** 3.0 |
Exponentiation (note the floats) |
|
let i = 1 + 2 * 3 (answer is 7) |
Default evaluation order of mathematical expression. |
let i = (1 + 2) * 3 (answer is 9) |
Parentheses ( ) override default evaluation order of mathematical expressions. |
Table 3.2
Through the Microsoft.FSharp.Core.Operators library, F# includes a number of useful mathematical operators. These include trigonometric functions such as cos, sin, tan, rounding functions such as floor and ceiling, etc. The operators in this library allow for overflow and underflow, and will throw exceptions in the face of such conditions. If you need mathematical operators that check for boundary conditions, see Microsoft.FSharp.Core.Operators.Checked.
F# also supports bitwise operators. These operators enable your F# programs to manipulate data at the bit-and-byte level. You can find the documentation of bitwise operators here. Table 3.3 below summarizes the bitwise operators and provides an example of their use.
|
Example |
Comment |
Example |
Comment |
|
let a = 1 &&& 2 |
Bitwise AND |
let d = 0xF <<< 1 |
Bitwise left-shift |
|
let b = 2 ||| 3 |
Bitwise OR |
let e = 2 >>> 3 |
Bitwise right-shift |
|
let c = 4 ^^^ 5 |
Bitwise XOR |
let f = ~~~5 |
Bitwise negation. Unary operator. |
Table 3.3
The bitwise operators can be used with the following data types: byte,
sbyte, int16, uint16, int32 (int), uint32, int64, uint64, nativeint, and
unativeint.
In addition to the traditional numeric data types and operators that you find in other languages, F# supports an interesting feature called units-of-measure. This is a mechanism by which you annotate floats and integers with statically-typed unit metadata, providing a way to ensure that your programs deal consistently with data such as weights, lengths, forces, etc.
To tap into the units-of-measure functionality, you first define one or more units that you wish to use, e.g., meters or seconds, in your program. To define new units of measure, you use the type keyword annotated with the Measure attribute[4]. In the following example, we define a new unit of measure, called m, that represents meters. We also define and a unit of measure called s that represents seconds:
[<Measure>] type m (* meters *)
[<Measure>] type s (* seconds *)
We can now use these measures to classify or decorate numeric values, and subsequently use these decorated values in calculations. For example, assuming that we’ve defined m and s above, we could use them as in the following manner (F# Interactive Console interaction shown):
> let distance =
123.5<m> // using meters
- let time = 5.0<s> // using seconds
- let speed = distance / time;; // mixing units
If we enter this code in the F# Interactive Console, F# provides the following feedback:
val distance : float<m> = 123.5
val time : float<s> = 5.0
val speed : float<m/s> = 24.7
F# seems to understand that the result, speed, is of type <m/s>, a unit it derived from the inputs. Take note, however, that F# does not understand the semantics of the units - it simply uses basic mathematical mechanics to ensure that the units are combined to be arithmetically consistent.
Measures are akin to data types in that they help qualify and constrain how a value is used. The compiler uses measure information to ensure that you don’t use a float<s> where you mean to use a float<m>. For example, given the previously defined units m and s, we can see that mixing them in a single expression can cause problems:
let a = 100<m>
let b = 200<m>
let c = 5<s>
let d = a + b //
OK, d = 300<m>
let e = a + c //
ERROR, The units of measure do not match
let f = c * c //
OK, f = 25<s^2>
Units of measure help to ensure the semantic consistency of calculations, e.g., you are using all metric units vs. English units, or US dollars vs. Euros. In the following example, we are using two measures - one for US dollars and one for Euros. Because we’ve decorated our values with measures, F# prevents us from making mistakes in our calculations:
[<Measure>] type usd (* US dollar *)
[<Measure>] type eu (* Euro *)
let salary = 100000<usd>
let raise = salary + 10000 // ERROR, int units do not match int<usd> units
let raise = salary + 10000<usd> // OK, units match
let raise = salary + 5000<eu> // ERROR, units do not match
We can also define new units of measure based on previously defined units of measure. For example:
[<Measure>] type
kg (* kilograms *)
[<Measure>] type N = (kg *
m)/(s^2) (* Newtons *)
[<Measure>] type Pa =
N/(m^2) (* Pascals *)
The F# units-of-measure system also supports conversion constants (and functions, but we will leave that discussion for when we discuss functions in detail). These enable us to define conversions and conversion ratios between different measurement systems. The following example defines a conversion constant describing the relationship between centimeters and inches:
[<Measure>] type cm (* centimeters *)
[<Measure>] type inch (* inches *)
let cmPerInch = 2.54<cm/inch> (* conversion constant *)
Once defined, you can use conversion constants quite naturally. For example, given the definition of cm, inch and cmPerInch above, we can use them in calculations:
let myHeightInches =
72.0<inch>
let myHeightCm = myHeightInches * cmPerInch
Note that I used 72.0 (a float) vs. simply 72 (an int) as myHeightInches. This is because I want to use myHeightInches in a calculation involving cmPerInch. Since cmPerInch is a float, I must ensure that all of the numbers used in calculations alongside cmPerInch are floats as well. Otherwise, F# will report (rather opaque and confusing) type-related errors. For example, if we change the lines above to:
let myHeightInches =
72<inch> // Note the absence of .0
let myHeightCm = myHeightInches *
cmPerInch
We get the following error:
error FS0001: The type 'float<cm/inch>' does not match the type 'int<'u>'
This 'u is not a typo. It’s how F# indicates generic types. We’ll discuss generics in a subsequent chapter.
Note that units of measure are compile-time constructs only. They help the compiler to enforce the semantic integrity of calculations, but they play no role at run-time, like real types do.
The F# PowerPack (FSharp.PowerPack.dll) is a library from Microsoft that contains additional components and tools for building F# applications. I mention the F# Power Pack here because it ships with units of measure, e.g., physical constants, that you may find useful when building your F# applications. I will refrain from using the F# Power Pack and other add-on libraries in this book, so as not to confuse you regarding what is core vs. what is from additional libraries.
To display values in an F# program, or to format them in the F# Interactive Console, you can use the printf and printfn[5] functions. These functions accept 0 or more format specifiers and a corresponding number of parameters, and format the result as a string. For example:
let city =
"Boston"
let temp = 63.5
printfn "The mean temperature for %s is %f" city temp
Executing this code, we get:
The mean temperature for Boston is 63.500000
Note that the parameters are not separated by commas. They are separated only by white space.
The format specifiers that you can use in for printf and printfn are the same ones found in the .NET String.Format method. Remember that you can also use the functions from the F# Printf module.
· F# supports all kinds of literal numeric constants, numeric types, and numeric operations on values.
· F# prefers the concept of “value binding” to “variable assignment.” We will sometimes slip and use the imperative term. In this text. We think you will understand what we mean.
· You bind a symbol to a value using the keyword let and the equals sign, e.g., let x = 3.
· F# uses the special type unit to designate “no value.” F# supports the keyword void for compatibility with other .NET languages, but that is not the same as unit.
· You can explicitly control a numeric literal’s value type by using type designators, e.g., let x = 3.0f. This also implicitly determines the type of the symbol, x.
· In F#, values are immutable by default. You can use the keyword mutable to make a value changeable. The destructive assignment operator <- is used to change the value of a previously bound identifier, e.g., x <- 5.
· F# supports all the arithmetic operations and bitwise operations on numeric values that you are familiar with from other languages.
· F# supports using parentheses to force the order of mathematical evaluation.
· F# supports units of measure, which you won’t find in most other languages. Units of measure enable you to annotate integer and floating point values with units, e.g., distances, weights, temperatures, etc. You can work with unit-rich values, and the compiler will help ensure that you don’t accidentally mix unit types, e.g., dollars and Euros. Units of measure are compile-time constructs. Units of measure have no impact at runtime. They are just for compile time checking.
[1] I may inadvertently use the term variable in some parts of the text. This comes from 20 plus years of developing software. Please consider the term synonymous with symbol when I use it.
[2] I will use both forms of example throughout the text. Keep in mind that expressions ending in ;; are from the F# Interactive Console.
[3] Even pure functional languages, e.g., Haskell, allow for side effects, although they do so differently than allowing values to be destructively assigned. We’ll discuss exactly how they realize state changes in a later chapter.
[4] We are using the standard .NET attribute model here. The only thing that is “F#-ish” about it is the angle bracket syntax. We will fully cover F#’s definition and use of attributes in a subsequent chapter
[5] printf and printfn are the same except for that printfn forces a newline at the end of its output stream.
We welcome your feedback. If you have comments or questions about this chapter, please feel free to e-mail us at