Expressions and Conditionals - Core Scala - Learning Scala (2015)

Learning Scala (2015)

Part I. Core Scala

Chapter 3. Expressions and Conditionals

This chapter focuses on Scala’s expressions, statements, and conditionals. The term expression as used in this book indicates a unit of code that returns a value after it has been executed. One or more lines of code can be considered an expression if they are collected together using curly braces ({ and }). This is known as an expression block.

Expressions provide a foundation for functional programming because they make it possible to return data instead of modifying existing data (such as a variable). This enables the use of immutable data, a key functional programming concept where new data is stored in new values instead of in existing variables. Functions, of course, can be used to return new data, but they are in a way just another type of expression.

When all of your code can be organized (or conceptualized) into a collection of one or more hierarchical expressions that return values using immutable data will be straightforward. The return values of expressions will be passed to other expressions or stored into values. As you migrate from using variables, your functions and expressions will have fewer side effects. In other words, they will purely act on the input you give them without affecting any data other than their return value. This is one of the main goals and benefits of functional programming.

Expressions

As noted earlier, an expression is a single unit of code that returns a value.

Let’s start out with an example of a simple expression in Scala, just a String literal on its own:

scala> "hello"

res0: String = hello

OK, that’s not a very impressive expression. Here’s a more complicated one:

scala> "hel" + 'l' + "o"

res1: String = hello

This example and the previous example are valid expressions that, while implemented differently, generate the same result. What’s important about expressions is the value they return. The entire point of them is to return a value that gets captured and used.

Defining Values and Variables with Expressions

We have seen that expressions cover both literal values (“hello”) and calculated values. Previously we have defined values and variables as being assigned literal values. However, it is more accurate to say that they are assigned the return value of expressions. This is true whether the expression is a literal (e.g., 5), a calculation, or a function call.

Given this, let’s redefine the syntax for defining values and variables based on expressions.

Syntax: Defining Values and Variables, Using Expressions

val <identifier>[: <type>] = <expression>

var <identifier>[: <type>] = <expression>

Because literal values are also a kind of expression, these definitions are more encompassing and accurate. It turns out that expressions are also a good foundation for defining most of Scala’s syntax. Look for the term “<expression>” in future syntax notations to indicate where any expression may be used.

Expression Blocks

Multiple expressions can be combined using curly braces ({ and }) to create a single expression block. An expression has its own scope, and may contain values and variables local to the expression block. The last expression in the block is the return value for the entire block.

As an example, here is a line with two expressions that would work better as a block:

scala> val x = 5 * 20; val amount = x + 10

x: Int = 100

amount: Int = 110

The only value we really care about keeping is “amount,” so let’s combine the expressions including the “x” value into a block. We’ll use its return value to define the “amount” value:

scala> val amount = { val x = 5 * 20; x + 10 }

amount: Int = 110

The last expression in the block, “x + 10,” determines the block’s return value. The “x” value, previously defined at the same level of “amount,” is now defined locally to the block. The code is now cleaner, because the intent of using “x” to define “amount” is now obvious.

Expression blocks can span as many lines as you need. The preceding example could have been rewritten without the semicolons as follows:

scala> val amount = {

| val x = 5 * 20

| x + 10

| }

amount: Int = 110

Expression blocks are also nestable, with each level of expression block having its own scoped values and variables.

Here is a short example demonstrating a three-deep nested expression block:

scala> { val a = 1; { val b = a * 2; { val c = b + 4; c } } }

res5: Int = 6

These examples may not indicate compelling reasons to use expression blocks on their own. However, it is important to understand their syntax and compositional nature because we will revisit them when we cover control structures later in this chapter.

Statements

A statement is just an expression that doesn’t return a value. Statements have a return type of Unit, the type that indicates the lack of a value. Some common statements used in Scala programming include calls to println() and value and variable definitions.

For example, the following value definition is a statement because it doesn’t return anything:

scala> val x = 1

x: Int = 1

The REPL repeats the definition of x but there is not actual data returned that can be used to create a new value.

A statement block, unlike an expression block, does not return a value. Because a statement block has no output, it is commonly used to modify existing data or make changes outside the scope of the application (e.g., writing to the console, updating a database, connecting to an external server).

If..Else Expression Blocks

The If..Else conditional expression is a classic programming construct for choosing a branch of code based on whether an expression resolves to true or false. In many languages this takes the form of an “if .. else if .. else” block, which starts with an “if,” continues with zero to many “else if” sections, and ends with a final “else” catch-all statement.

As a matter of practice you can write these same “if .. else if .. else” blocks in Scala and they will work just as you have experienced them in Java and other languages. As a matter of formal syntax, however, Scala only supports a single “if” and optional “else” block, and does not recognize the “else if” block as a single construct.

So how do “else if” blocks still work correctly in Scala? Because “if .. else” blocks are based on expression blocks, and expression blocks can be easily nested, an “if .. else if .. else” expression is equivalent to a nested “if .. else { if .. else }” expression. Logically this is exactly the same as an “if .. else if .. else” block, and as a matter of syntax Scala recognizes the second “if else” as a nested expression of the outer “if .. else” block.

Let’s start exploring actual “if” and “if .. else” blocks by looking at the syntax for the simple “if” block.

If Expressions

Syntax: Using an If Expression

if (<Boolean expression>) <expression>

The term Boolean expression here indicates an expression that will return either true or false.

Here is a simple if block that prints a notice if the Boolean expression is true:

scala> if ( 47 % 3 > 0 ) println("Not a multiple of 3")

Not a multiple of 3

Of course 47 isn’t a multiple of 3, so the Boolean expression was true and the println was trigggered.

Although an if block can act as an expression, it is better suited for statements like this one. The problem with using if blocks as expressions is that they only conditionally return a value. If the Boolean expression returns false, what do you expect the if block to return?

scala> val result = if ( false ) "what does this return?"

result: Any = ()

The type of the result value in this example is unspecified so the compiler used type inference to determine the most appropriate type. Either a String or Unit could have been returned, so the compiler chose the root class Any. This is the one class common to both String (which extends AnyRef) and to Unit (which extends AnyVal).

Unlike the solitary “if” block, the “if .. else” block is well suited to working with expressions.

If-Else Expressions

Syntax: If .. Else Expressions

if (<Boolean expression>) <expression>

else <expression>

Here is an example:

scala> val x = 10; val y = 20

x: Int = 10

y: Int = 20

scala> val max = if (x > y) x else y

max: Int = 20

You can see that the x and y values make up the entirety of the if and else expressions. The resulting value is assigned to max, which we and the Scala compiler know will be an Int because both expressions have return values of type Int.

Some wonder why Scala doesn’t have a ternary expression (popular in C and Java) where the punctuation characters ? and : act as a one-line if and else expression. It should be clear from this example that Scala doesn’t really need it because its if and else blocks can fit compactly on a single line (and, unlike in C and Java, they are already an expression).

Using a single expression without an expression block in if..else expressions works well if everything fits on one line. When your if..else expression doesn’t easily fit on a single line, however, consider using expression blocks to make your code more readable. if expressions without an else should always use curly braces, because they tend to be statements that create side effects.

if..else blocks are a simple and common way to write conditional logic. There are other, more elegant ways to do so in Scala, however, using match expressions.

Match Expressions

Match expressions are akin to C’s and Java’s “switch” statements, where a single input item is evaluated and the first pattern that is “matched” is executed and its value returned. Like C’s and Java’s “switch” statements, Scala’s match expressions support a default or wildcard “catch-all” pattern. Unlike them, only zero or one patterns can match; there is no “fall-through” from one pattern to the next one in line, nor is there a “break” statement that would prevent this fall-through.

The traditional “switch” statement is limited to matching by value, but Scala’s match expressions are an amazingly flexible device that also enables matching such diverse items as types, regular expressions, numeric ranges, and data structure contents. Although many match expressions could be replaced with simple “if .. else if .. else” blocks, doing so would result in a loss of the concise syntax that match expressions offer.

In fact, most Scala developers prefer match expressions over “if .. else” blocks because of their expressiveness and concise syntax.

In this section we will cover the basic syntax and uses of match expressions. As you read through the book, you will pick up new features that may be applicable to match expressions. Try experimenting with them to find new ways to express relationships or equivalence through match expressions.

Syntax: Using a Match Expression

<expression> match {

case <pattern match> => <expression>

[case...]

}

MULTIPLE EXPRESSIONS ALLOWED BUT NOT RECOMMENDED

Scala officially supports having multiple expressions follow the arrow (=>), but this is not recommended because it may reduce readability. If you have multiple expressions in a case block, convert them to an expression block by wrapping them with curly braces.

Let’s try this out by converting the “if .. else” example from the previous section into a match expression. In this version the Boolean expression is handled first, and then the result is matched to either true or false:

scala> val x = 10; val y = 20

x: Int = 10

y: Int = 20

scala> val max = x > y match {

| case true => x

| case false => y

| }

max: Int = 20

The logic works out to the same as in the “if .. else” block but is implemented differently.

Here is another example of a match expression, one that takes an integer status code and tries to return the most appropriate message for it. Depending on the input to the expression, additional actions may be taken besides just returning a value:

scala> val status = 500

status: Int = 500

scala> val message = status match {

| case 200 =>

| "ok"

| case 400 => {

| println("ERROR - we called the service incorrectly")

| "error"

| }

| case 500 => {

| println("ERROR - the service encountered an error")

| "error"

| }

| }

ERROR - the service encountered an error

message: String = error

This match expression prints error messages in case the status is 400 or 500 in addition to returning the message “error.” The println statement is a good example of including more than one expression in a case block. There is no limit to the number of statements and expressions you can have inside a case block, although only the last expression will be used for the match expression’s return value.

You can combine multiple patterns together with a pattern alternative, where the case block will be triggered if any one of the patterns match.

Syntax: A Pattern Alternative

case <pattern 1> | <pattern 2> .. => <one or more expressions>

The pattern alternative makes it possible to prevent duplicated code by reusing the same case block for multiple patterns. Here is an example showing the uses of these pipes (|) to collapse a 7-pattern match expression down to only two patterns:

scala> val day = "MON"

day: String = MON

scala> val kind = day match {

| case "MON" | "TUE" | "WED" | "THU" | "FRI" =>

| "weekday"

| case "SAT" | "SUN" =>

| "weekend"

| }

kind: String = weekday

So far the examples have left open the possibility that a pattern may not be found that matches the input expression. In case this event does occur, for example if the input to the previous example was “MONDAY,” what do you think would happen?

Well, it’s more fun to try it out than to explain, so here is an example of a match expression that fails to provide a matching pattern for the input expression:

scala> "match me" match { case "nope" => "sorry" }

scala.MatchError: match me (of class java.lang.String)

... 32 elided

The input of “match me” didn’t match the only given pattern, “nope,” so the Scala compiler treated this as a runtime error. The error type, scala.MatchError, indicates that this is a failure of the match expression to handle its input.

NOTE

The message “… 32 elided” in the preceding example indicates that the error’s stack trace (a list of all the nested function calls down to the one that caused the error) was reduced for readability.

To prevent errors from disrupting your match expression, use a wildcard match-all pattern or else add enough patterns to cover all possible inputs. A wildcard pattern placed as the final pattern in a match expression will match all possible input patterns and prevent a scala.MatchErrorfrom occurring.

Matching with Wildcard Patterns

There are two kinds of wildcard patterns you can use in a match expression: value binding and wildcard (aka “underscore”) operators.

With value binding (aka variable binding) the input to a match expression is bound to a local value, which can then be used in the body of the case block. Because the pattern contains the name of the value to be bound there is no actual pattern to match against, and thus value binding is a wildcard pattern because it will match any input value.

Syntax: A Value Binding Pattern

case <identifier> => <one or more expressions>

Here is an example that tries to match a specific literal and otherwises uses value binding to ensure all other possible values are matched:

scala> val message = "Ok"

message: String = Ok

scala> val status = message match {

| case "Ok" => 200

| case other => {

| println(s"Couldn't parse $other")

| -1

| }

| }

status: Int = 200

The value other is defined for the duration of the case block and is assigned the value of message, the input to the match expression.

The other type of wildcard pattern is the use of the wildcard operator. This is an underscore (_) character that acts as an unnamed placeholder for the eventual value of an expression at runtime. As with value binding, the underscore operator doesn’t provide a pattern to match against, and thus it is a wildcard pattern that will match any input value.

Syntax: A Wildcard Operator Pattern

case _ => <one or more expressions>

The wildcard cannot be accessed on the right side of the arrow, unlike with value binding. If you need to access the value of the wildcard in the case block, consider using a value binding, or just accessing the input to the match expression (if available).

WHY AN UNDERSCORE AS A WILDCARD?

Using underscores to indicate unknown values comes from the field of mathematics, arithmetic in particular, where missing amounts are denoted in problems with one or more underscores. For example, the equation 5 * _ = 15 is a problem that must be solved for _, the missing value.

Here is a similar example to the one earlier only with a wildcard operator instead of a bound value:

scala> val message = "Unauthorized"

message: String = Unauthorized

scala> val status = message match {

| case "Ok" => 200

| case _ => {

| println(s"Couldn't parse $message")

| -1

| }

| }

Couldn't parse Unauthorized

status: Int = -1

In this case the underscore operator matches the runtime value of the input to the match expression. However, it can’t be accessed inside the case block as a bound value would, and thus the input to the match expression is used to create an informative println statement.

Matching with Pattern Guards

A pattern guard adds an if expression to a value-binding pattern, making it possible to mix conditional logic into match expressions. When a pattern guard is used the pattern will only be matched when the if expression returns true.

Syntax: A Pattern Guard

case <pattern> if <Boolean expression> => <one or more expressions>

Unlike regular if expressions, the if expression here doesn’t require parentheses (( and )) around its Boolean expression. Regular if expressions require the parentheses in order to simplify the job of parsing the full command and delineate the Boolean expression from the conditional expression. In this case the arrow (=>) handles that task and simplifies parsing. You can, however, add the parentheses around the Boolean expression if you wish.

Let’s use a pattern guard to differentiate between a nonnull and a null response and report the correct message:

scala> val response: String = null

response: String = null

scala> response match {

| case s if s != null => println(s"Received '$s'")

| case s => println("Error! Received a null response")

| }

Error! Received a null response

Matching Types with Pattern Variables

Another way to do pattern matching in a match expression is to match the type of the input expression. Pattern variables, if matched, may convert the input value to a value with a different type. This new value and type can then be used inside the case block.

Syntax: Specifying a Pattern Variable

case <identifier>: <type> => <one or more expressions>

The only restriction for pattern variable naming, other than the naming requirements already in place for values and variables, is that they must start with a lowercase letter.

You might be considering the utility of using a match expression to determine a value’s type, given that all values have types and they are typically rather descriptive. The support of polymorphic types in Scala should be a clue to a match expression’s utility. A value of type Int may get assigned to another value of type Any, or it may be returned as Any from a Java or Scala library call. Although the data is indeed an Int, the value will have the higher type Any.

Let’s reproduce this situation by creating an Int, assigning it to an Any, and using a match expression to resolve its true type:

scala> val x: Int = 12180

x: Int = 12180

scala> val y: Any = x

y: Any = 12180

scala> y match {

| case x: String => s"'x'"

| case x: Double => f"$x%.2f"

| case x: Float => f"$x%.2f"

| case x: Long => s"${x}l"

| case x: Int => s"${x}i"

| }

res9: String = 12180i

Even though the value given to the match expression has the type Any, the data it is storing was created as an Int. The match expression was able to match based on the actual type of the value, not just on the type that it was given. Thus, the integer 12180, even when given as type Any, could be correctly recognized as an integer and formatted as such.

Loops

Loops are the last expression-based control structure we’ll examine in this chapter. A loop is a term for exercising a task repeatedly, and may include iterating through a range of data or repeating until a Boolean expression returns false.

The most important looping structure in Scala is the for-loop, also known as a for-comprehension. For-loops can iterate over a range of data executing an expression every time and optionally return a collection of all the expression’s return values. These for-loops are highly customizable, supporting nested iterating, filtering, and value binding.

To get started we will introduce a new data structure called a Range, which iterates over a series of numbers. Ranges are created using the to or until operator with starting and ending integers, where the to operator creates an inclusive list and the until operator creates an exclusive list.

Syntax: Defining a Numeric Range

<starting integer> [to|until] <ending integer> [by increment]

Next is the basic definition of a for-loop.

Syntax: Iterating with a Basic For-Loop

for (<identifier> <- <iterator>) [yield] [<expression>]

The yield keyword is optional. If it is specified along with an expression, the return value of every expression that gets invoked will be returned as a collection. If it isn’t specified, but the expression is still specified, the expression will be invoked but its return values will not be accessible.

You can define for-loops with parentheses or curly braces. The difference between the two styles comes when using multiple iterators (or other valid for-loop items, as we’ll see), one on each line. With parentheses-based for-loops, each iterator line before the final one must end with a semicolon. With curly-braces-based for-loops, the semicolon after an iterator line is optional.

Let’s start out printing a simple week planner by iterating over the days of a week, from 1 to 7 (inclusive), and printing out a header for each one:

scala> for (x <- 1 to 7) { println(s"Day $x:") }

Day 1:

Day 2:

Day 3:

Day 4:

Day 5:

Day 6:

Day 7:

The curly braces in the loop’s expression (really a statement here because there isn’t a yield keyword) are optional because there is only a single command, but I added them to make this look more like a traditional Java/C “for” loop.

However, what if what I really need is a collection of these “Day X:” messages? Then I can reuse them in other ways, or print them out as many times as I need. The yield keyword is the solution. I can convert the iterated statement into an expression that returns each message instead of printing it out, and add the yield keyword to convert the entire loop into an expression that returns the collection:

scala> for (x <- 1 to 7) yield { s"Day $x:" }

res10: scala.collection.immutable.IndexedSeq[String] = Vector(Day 1:,

Day 2:, Day 3:, Day 4:, Day 5:, Day 6:, Day 7:)

The Scala REPL’s printout is more complicated than we have seen. This one is reporting that res10 has the type IndexedSeq[String], an indexed sequence of String, and is assigned a Vector, one of the implementations of IndexedSeq. Because of Scala’s support for object-oriented polymorphism, a Vector (a subtype of IndexedSeq) can be assigned to an IndexedSeq-typed value.

In a way you can consider this for-loop to be a map, because it takes the expression of rendering the day to a String and applies it for every member of the input range. We have used this to map the range of numbers from 1 to 7 into a collection of messages of the same size. Like other sequences, this collection can now be used as an iterator in other for-loops.

Let’s try it out by creating a for-loop that iterates over the sequence we built and printing each message, this time all on the same line instead of on their own lines. Again we only have a single command in the iterated expression, so this time we will leave off the curly braces because they are not necessary here:

scala> for (day <- res0) print(day + ", ")

Day 1:, Day 2:, Day 3:, Day 4:, Day 5:, Day 6:, Day 7:,

Iterator Guards

Like a pattern guard in a match expression, an iterator guard (also known as a filter) adds an if expression to an iterator. When an iterator guard is used, an iteration will be skipped unless the if expression returns true.

Syntax: An Iterator Guard

for (<identifier> <- <iterator> if <Boolean expression>) ...

Here is an example of using iterator guards to create a collection of numbers that are multiples of 3:

scala> val threes = for (i <- 1 to 20 if i % 3 == 0) yield i

threes: scala.collection.immutable.IndexedSeq[Int] = Vector(3, 6, 9, 12, 15, 18)

An iterator guard can also appear on its own line, separate from the iterator. Here’s another example for-loop with separate iterator and iterator guards:

scala> val quote = "Faith,Hope,,Charity"

quote: String = Faith,Hope,,Charity

scala> for {

| t <- quote.split(",")

| if t != null

| if t.size > 0

| }

| { println(t) }

Faith

Hope

Charity

Nested Iterators

Nested iterators are extra iterators added to a for-loop, multiplying the total number of iterations by their iterator count. They are called nested iterators because adding them to an existing loop has the same effect as if they were written as a separate nested loop. Because the total number of iterations is the product of all of the iterators, adding a nested loop that will iterate once will not change the number of iterations, whereas a nested loop that does not iterate will cancel all iterations.

Here is an example of a for-loop with two iterators:

scala> for { x <- 1 to 2

| y <- 1 to 3 }

| { print(s"($x,$y) ") }

(1,1) (1,2) (1,3) (2,1) (2,2) (2,3)

scala>

Because the product of the two iterators is six iterations, the print statement is called six times.

Value Binding

A common tactic in for-loops is to define temporary values or variables inside the expression block based on the current iteration. An alternate way to do this in Scala is to use value binding in the for-loop’s definition, which works the same but can help to minimize the size and complexity of the expression block. Bound values can be used for nested iterators, iterator guards, and other bound values.

Syntax: Value Binding in For-Loops

for (<identifier> <- <iterator>; <identifier> = <expression>) ...

In this example I will use the “left-shift” binary operator (<<) on an Int to compute the powers of two from zero to eight. The argument to the operator is the number of times to “shift” the number leftwards by one bit, effectively mupltiplying it by two. The result of each operation is bound to the value “pow” in the current iteration:

scala> val powersOf2 = for (i <- 0 to 8; pow = 1 << i) yield pow

powersOf2: scala.collection.immutable.IndexedSeq[Int] = Vector(1, 2, 4, 8,

16, 32, 64, 128, 256)

The “pow” value is defined and assigned for each iteration in the loop. Because that value is yielded by the for-loop, the result is a collection of the “pow” value from each iteration.

Value binding within the definition of a for-loop makes it possible to centralize most of the loop’s logic inside the definition. The result is a more compact for-loop with an even more compact yield expression (if used).

While and Do/While Loops

In addition to for-loops Scala also supports “while” and “do/while” loops, which repeat a statement until a Boolean expression returns false. These are not as commonly used as for-loops in Scala, however, because they are not expressions and cannot be used to yield values.

Syntax: A While Loop

while (<Boolean expression>) statement

As a very simple example here is a while loop that decrements a number repeatedly until it is no longer greater than zero:

scala> var x = 10; while (x > 0) x -= 1

x: Int = 0

The “do/while” loop is similar but the statement is executed before the Boolean expression is first evaluated. In this example I have a Boolean expression that will return false, but is only checked after the statement has had a chance to run:

scala> val x = 0

x: Int = 0

scala> do println(s"Here I am, x = $x") while (x > 0)

Here I am, x = 0

The while and do/while loops may have their uses, for example if you’re reading from a socket and need to continue iterating until there is no more content to read. However, Scala offers a number of more expressive and more functional ways to handle loops than while and do/while loops. These include the for-loops we already covered as well as some new ones we’ll study in Chapter 6.

Summary

We covered if/else conditions, pattern matching, and loops in detail in this chapter. These structures provide a solid basis for writing core logic in Scala.

However, these three (or two) structures are just as important to learning Scala development as learning about the fundamentals of expressions. The namesake of this chapter—expressions and their return values—are the real core building block of any application. Expressions themselves may seem to be an obvious concept, and devoting an entire chapter to them has the appearance of being overly generous to the topic. The reason I have devoted a chapter to them is that learning to work in terms of expressions is a useful and valuable skill. You should consider expressions when writing code, and even structure your applications around them. Some important principles to keep in mind when writing expressions are (1) how you will organize your code as expressions, (2) how your expressions will derive a return value, and (3) what you will do with that return value.

Expressions, in addition to being a foundation for your code organization, are also a foundation for Scala’s syntax. In this chapter we have defined if/else conditions, pattern matching, and loops in terms of how they are structured around expressions. In the next chapter we will continue this practice by introducing functions as named, reusable expressions and defining them as such. Future chapters will continue this trend of defining concepts and structures in terms of expressions. Thus, understanding the basic nature and syntax of expressions and expression blocks is a crucial key to picking up the syntax for the rest of the language.

Exercises

While the Scala REPL provides an excellent venue for experimenting with the language’s features, writing more than a line or two of code in it can be challenging. Because you’ll need to start working with more than a few lines of code, it’s time to start working in standalone Scala source files.

The scala command, which launches the Scala REPL, can also be used to evaluate and execute Scala source files:

$ scala <source file>

To test this out, create a new file titled Hello.scala with the following contents:

println("Hello, World")

Then execute it with the scala command:

$ scala Hello.scala

Hello, World

$

You should see the result (“Hello, World”) printed on the next line.

An alternate way to execute external Scala files is with the :load command in the Scala REPL. This is useful if you want to stay in the Scala REPL while still using a text editor or IDE to edit your code.

To test this out, in the same directory you created the Hello.scala file, start the Scala REPL and run :load Hello.scala:

scala> :load Hello.scala

Loading Hello.scala...

Hello, World

scala>

NOTE

The :load command is a Scala REPL feature and not actually part of the Scala language. Scala REPL commands are distinguished from regular Scala syntax by their “:” prefix.

Now that you have the option of developing within the Scala REPL or in a separate text editor or IDE, you can get started with the exercises for this chapter.

1. Given a string name, write a match expression that will return the same string if nonempty, or else the string “n/a” if it is empty.

2. Given a double amount, write an expression to return “greater” if it is more than zero, “same” if it equals zero, and “less” if it is less than zero. Can you write this with if..else blocks? How about with match expressions?

3. Write an expression to convert one of the input values cyan, magenta, yellow to their six-char hexadecimal equivalents in string form. What can you do to handle error conditions?

4. Print the numbers 1 to 100, with each line containing a group of five numbers. For example:

5. 1, 2, 3, 4, 5,

6. 6, 7, 8, 9, 10

....

7. Write an expression to print the numbers from 1 to 100, except that for multiples of 3, print “type,” and for multiples of 5, print “safe.” For multiples of both 3 and 5, print “typesafe.”

8. Can you rewrite the answer to exercise 5 to fit on one line? It probably won’t be easier to read, but reducing code to its shortest form is an art, and a good exercise to learn the language.