Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

sorting-room: Implement exercise to teach about type-conversion and type-assertion #1723

Merged
merged 14 commits into from Sep 30, 2021
Merged
25 changes: 24 additions & 1 deletion config.json
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,6 @@
"concepts": [
"arithmetic-operators",
"numbers",
"type-conversion",
"conditionals-if",
"comparison"
],
Expand Down Expand Up @@ -222,6 +221,20 @@
],
"status": "beta"
},
{
"name": "Sorting Room",
"slug": "sorting-room",
"uuid": "411f4f1e-7ee2-4f11-955d-8af86da69c2a",
"concepts": [
"type-assertion",
"type-conversion"
jmrunkle marked this conversation as resolved.
Show resolved Hide resolved
],
"prerequisites": [
"interfaces",
"string-formatting"
],
"status": "wip"
},
{
"name": "Chessboard",
"slug": "chessboard",
Expand Down Expand Up @@ -1816,6 +1829,11 @@
"slug": "errors",
"uuid": "a960da78-c940-4725-8069-929f42564cdb"
},
{
"name": "Interfaces",
"slug": "interfaces",
"uuid": "a492060c-11e7-448e-bdbd-6a907cec9493"
},
{
"name": "Iteration",
"slug": "iteration",
Expand Down Expand Up @@ -1886,6 +1904,11 @@
"slug": "time",
"uuid": "4673000c-7822-4252-88c5-723f47d6ad06"
},
{
"name": "Type Assertion",
"slug": "type-assertion",
"uuid": "601911e6-ecb8-4e37-bc11-395aaa2be3ce"
},
{
"name": "Type Conversion",
"slug": "type-conversion",
Expand Down
27 changes: 27 additions & 0 deletions exercises/concept/sorting-room/.docs/hints.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Hints

## 1. Describe simple numbers

- use `fmt.Sprintf` to format

## 2. Describe a number box

- get the result from the `Number()` method
- use `fmt.Sprintf` to format

## 3. Implement a method extracting the number from a fancy number box

- use a type assertion to check if this is a `FancyBox`
- get the `string` from the `Value()` method
- use `strconv.Atoi` to convert the `string` to an `int` (we can throw away the error since we want 0 if it cannot be converted to an `int`)

## 4. Describe a fancy number box

- use `ExtractFancyNumber` to get the `int` value
- convert the `int` to a `float64`
- use `fmt.Sprintf` to format

## 5. Implement `DescribeAnything` which uses them all

- either use type assertions (eg. `i.(someType)`) or a type switch (`switch v := i.(type)`)
- remember to convert `int` to a `float64`
78 changes: 78 additions & 0 deletions exercises/concept/sorting-room/.docs/instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Instructions

Jen is working in the sorting room in a large factory.
The sorting room needs to process anything that comes into it by categorizing it with a label.
Jen is responsible for things that were pre-categorized as numbers and needs a program to help her with the sorting.

Most primitive values should get straight-forward labels.
For numbers, she wants strings saying `"This is the number 2.0"` (if the number was 2).
Jen wants the same output for integers and floats.

There are a few `Box` interfaces that need to be unwrapped to get their contents.
For a `NumberBox`, she wants strings saying `"This is a box containing the number 3.0"` (if the `Number()` method returns 3).
For a `FancyNumberBox`, she wants strings saying `"This is a fancy box containing the number 4.0"`, but only if the type is a `FancyNumber`.

Anything unexpected should say `"Return to sender"` so Jen can send them back where they came from.

## 1. Describe simple numbers

Jen wants numbers to return strings like `"This is the number 2.0"` (including one digit after the decimal):

```go
fmt.Println(DescribeNumber(-12.345))
// Output: This is the number -12.3
```

## 2. Describe a number box

Jen wants numbers to return strings like `"This is a box containing the number 2.0"` (again, including one digit after the decimal):

```go
fmt.Println(DescribeNumberBox(numberBoxContaining{12.345}))
// Output: This is a box containing the number 12.3
```

## 3. Implement a method extracting the number from a fancy number box

Jen needs a helper function to extract the number from a `FancyNumberBox`.
If the `FancyNumberBox` is a `FancyNumber`, extract its value and convert it from a `string` to an `int`.
Any other type of `FancyNumberBox` should return 0.

```go
fmt.Println(ExtractFancyNumber(FancyNumber{"10"}))
// Output: 10
fmt.Println(ExtractFancyNumber(AnotherFancyNumber{"one"}))
// Output: 0
```

## 4. Describe a fancy number box

If the `FancyNumberBox` is a `FancyNumber`, Jen wants strings saying `"This is a fancy box containing the number 4.0"`.
Any other type of `FancyNumberBox` should say `"This is a fancy box containing the number 0.0"`.

```go
fmt.Println(DescribeFancyNumberBox(FancyNumber{"10"}))
// Output: This is a fancy box containing the number 10.0
fmt.Println(DescribeFancyNumberBox(AnotherFancyNumber{"one"}))
// Output: This is a fancy box containing the number 0.0
```

NOTE: we should use the `ExtractFancyNumber` function!

## 5. Implement `DescribeAnything` which uses them all

This is the main function Jen needs which takes any input (the empty interface means any value at all: `interface{}`).
`DescribeAnything` should delegate to the other functions based on the type of the value passed in.
More specifically:

- `int` and `float64` should both delegate to `DescribeNumber`
- `NumberBox` should delegate to `DescribeNumberBox`
- `FancyNumberBox` should delegate to `DescribeFancyNumberBox`
- anything else should result in `"Return to sender"`

```go
fmt.Println(DescribeAnything(numberBoxContaining{12.345}))
// Output: This is a box containing the number 12.3
fmt.Println(DescribeAnything("some string"))
// Output: Return to sender
```
68 changes: 68 additions & 0 deletions exercises/concept/sorting-room/.docs/introduction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Introduction

## Type Conversion

Go requires explicit conversion between different types.
Converting between types (also known as **type casting**) is done via a function with the name of the type to convert to.
For example, to convert an `int` to a `float64` you would need to do the following:

```go
var x int = 42 // x has type int
f := float64(x) // f has type float64 (ie. 42.0)
```

## Converting between primitive types and strings

There is a `strconv` package for converting between primitive types (like `int`) and `string`.

```go
import "strconv"

var intString string = "42"
var i int = strconv.Atoi(intString)

var number int = 12
var s string = strconv.Itoa(number)
```

## Type Assertions

Interfaces in Go can introduce ambiguity about the underlying type.
A type assertion allows us to extract the interface value's underlying concrete value using this syntax: `interfaceVariable.(concreteType)`.

For example:

```go
var input interface{} = 12
number := input.(int)
```

NOTE: this will cause a panic if the interface variable does not hold a value of the concrete type.

We can test whether an interface value holds a specific concrete type by making use of both return values of the type assertion: the underlying value and a boolean value that reports whether the assertion succeeded.
For example:

```go
str, ok := input.(string) // no panic if input is not a string
```

If `input` holds a `string`, then `str` will be the underlying value and `ok` will be true.
If `input` does not hold a `string`, then `str` will be the zero value of type `string` (ie. `""` - the empty string) and `ok` will be false.
No panic occurs in any case.

## Type Switches

A **type switch** can perform several type assertions in series.
It has the same syntax as a type assertion (`interfaceVariable.(concreteType)`), but the `concreteType` is replaced with the keyword `type`.
Here is an example:

```go
switch v := i.(type) {
case int:
fmt.Println("the integer %d", v)
case string:
fmt.Println("the string %s", v)
default:
fmt.Println("some type we did not handle explicitly")
}
```
20 changes: 20 additions & 0 deletions exercises/concept/sorting-room/.meta/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"blurb": "Learn about type casting and type assertions in the sorting room.",
"authors": [
"jmrunkle"
],
jmrunkle marked this conversation as resolved.
Show resolved Hide resolved
"contributors": [
"junedev"
],
"files": {
"solution": [
"sorting_room.go"
],
"test": [
"sorting_room_test.go"
],
"exemplar": [
".meta/exemplar.go"
]
}
}
24 changes: 24 additions & 0 deletions exercises/concept/sorting-room/.meta/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Design

## Learning objectives

- How to convert from one type to another
- How to assert a type
- How to assert an interface type
jmrunkle marked this conversation as resolved.
Show resolved Hide resolved
- How to use a type switch

## Out of scope

- Defining new types

## Concepts

- `type-conversion`: how to convert between types
- `type-assertion`: how to assert an interface type has a particular concrete type

## Prerequisites

This exercise's prerequisites Concepts are:

- `interfaces`: what is an interface and how to use them
- `string-formatting`: how to format strings
68 changes: 68 additions & 0 deletions exercises/concept/sorting-room/.meta/exemplar.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package sorting
jmrunkle marked this conversation as resolved.
Show resolved Hide resolved

import (
"fmt"
"strconv"
)

// DescribeNumber should return a string describing the number.
func DescribeNumber(f float64) string {
return fmt.Sprintf("This is the number %.1f", f)
}

type NumberBox interface {
Number() int
}

// DescribeNumberBox should return a string describing the NumberBox.
func DescribeNumberBox(nb NumberBox) string {
return fmt.Sprintf("This is a box containing the number %.1f", float64(nb.Number()))
}

// FancyNumber holds an integer as a string
type FancyNumber struct {
n string
}

func (i FancyNumber) Value() string {
return i.n
}

type FancyNumberBox interface {
Value() string
}

// ExtractFancyNumber should return the integer value for a FancyNumber
// or 0 if any other type of FancyNumberBox is supplied.
func ExtractFancyNumber(fnb FancyNumberBox) int {
fancyNum, ok := fnb.(FancyNumber)
if !ok {
return 0
}

num, _ := strconv.Atoi(fancyNum.Value())
return num
}

// DescribeFancyNumberBox should return a string describing the FancyNumberBox.
func DescribeFancyNumberBox(fnb FancyNumberBox) string {
return fmt.Sprintf(
"This is a fancy box containing the number %.1f",
float64(ExtractFancyNumber(fnb)))
}

// DescribeAnything should return a string describing whatever it contains.
func DescribeAnything(i interface{}) string {
jmrunkle marked this conversation as resolved.
Show resolved Hide resolved
switch v := i.(type) {
case int:
return DescribeNumber(float64(v))
case float64:
return DescribeNumber(v)
case NumberBox:
return DescribeNumberBox(v)
case FancyNumberBox:
return DescribeFancyNumberBox(v)
default:
return "Return to sender"
}
}
3 changes: 3 additions & 0 deletions exercises/concept/sorting-room/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module sorting

go 1.14
43 changes: 43 additions & 0 deletions exercises/concept/sorting-room/sorting_room.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package sorting

// DescribeNumber should return a string describing the number.
func DescribeNumber(f float64) string {
panic("Please implement DescribeNumber")
}

type NumberBox interface {
Number() int
}

// DescribeNumberBox should return a string describing the NumberBox.
func DescribeNumberBox(nb NumberBox) string {
panic("Please implement DescribeNumberBox")
}

type FancyNumber struct {
n string
}

func (i FancyNumber) Value() string {
return i.n
}

type FancyNumberBox interface {
Value() string
}

// ExtractFancyNumber should return the integer value for a FancyNumber
// and 0 if any other FancyNumberBox is supplied.
func ExtractFancyNumber(fnb FancyNumberBox) int {
panic("Please implement ExtractFancyNumber")
}

// DescribeFancyNumberBox should return a string describing the FancyNumberBox.
func DescribeFancyNumberBox(fnb FancyNumberBox) string {
panic("Please implement DescribeFancyNumberBox")
}

// DescribeAnything should return a string describing whatever it contains.
func DescribeAnything(i interface{}) string {
panic("Please implement DescribeAnything")
}
Loading