Chapter 3. Types and Type casting

This chapter starts with explaining types, touching on the concept of type in the category theory very briefly. Then, it explains value and reference types and compares them in detail. Finally, it talks about equality, identity, and type casting.

This chapter will cover the following topics with coding examples:

  • Types
  • Value versus reference types
    • Value and reference type constants
    • Mixing value and reference types
    • Copying
    • Value type characteristics
  • Equality, identity, and comparing
  • Type checking and casting

You may have heard that functional programming uses concepts of the category theory. This link is the reason why some people find functional programming closer to mathematics. In an upcoming chapter, we will talk briefly about the category theory so we are not going to dive into those concepts now. At this point, it is good to know that theoretically category refers to a collection that contains the following:

  • A collection of objects (types in Swift)
  • A collection of morphisms, each of which ties two objects together (functions in Swift)
  • A notion of composition of the morphisms (function composition in Swift)

We have already discussed functions and function composition and now we are going to explore types.

It is possible to categorize types in two different ways. The first is the concept of named types and compound types in Swift. The second is the categorization of types based on value versus reference.

Any type that we can give a name to while we define it is a named type. For instance, if we create a class named OurClass, any instance of OurClass will be of the OurClass type.

Function types and tuple types are compound types. A compound type may contain named types and other compound types. For instance, (String, (Double, Double)) is a compound type and in fact is a tuple of String and another tuple of the (Double, Double) type.

We can use named types and compound types in type annotation, identification, and aliasing.

In previous chapters, we have seen that we can use Swift inference that infers the types unless we want to specify the type explicitly. We annotate the type in case we need to specify the type explicitly.

Also, we did not talk a lot about reference versus value types and type casting. In the following sections of this chapter, we will explore these concepts.

Value versus reference types

In Swift, there are two kinds of types: value and reference.

Value type instances keep a copy of their data. Each type has its own data and is not referenced by another variable. Structures, enums, and tuples are value types; therefore, they do not share data between their instances. Assignments copy the data of an instance to the other and there is no reference counting involved. The following example presents a struct with copying:

struct ourStruct {
    var data: Int = 3
}

var valueA = ourStruct()
var valueB = valueA // valueA is copied to valueB
valueA.data = 5 // Changes valueA, not valueB
print("(valueA.data), (valueB.data)") // prints "5, 3"

As seen from the preceding example, changing valueA.data does not change valueB.data.

In Swift, arrays, dictionaries, strings, and sets are all value types.

On the other hand, reference type instances share the same copy of the data. Classes and closures are reference types so assignment only adds a reference but does not copy the data. In fact, initialization of a reference type creates a shared instance that will be used by different instances of a reference type such as class or closure. Two variables of the same class type will refer to a single instance of the data, so if we modify the data in one of the variables, it will also affect the other variable. The following example presents a class with referencing:

class ourClass {
    var data: Int = 3
}
var referenceA = ourClass()
var referenceB = referenceA // referenceA is copied to referenceB
referenceA.data = 5 // changes the instance referred to by
  referenceA and referenceB
print("(referenceA.data), (referenceB.data)") // prints "5, 5"

As seen from the preceding example, changing referenceA.data also changes referenceB.data as they refer to the same shared instance.

This fundamental difference between value and reference types can have a huge impact on our system architecture. In functional programming, it is recommended to prefer value types over reference types as it is easier to trace and reason about value types. As we always get a unique copy of data and the data is not shared among instances, we can reason that no other part of our program is going to change the data. This feature of value types makes them especially helpful in multithreaded environments where a different thread will be able to change our data without informing us. This can create bugs that are very hard to debug and fix.

To be able to use this feature in Swift with classes, we can develop immutable classes using only immutable stored properties and avoiding exposing any APIs that can alter state. However, Swift does not provide any language mechanism to enforce class immutability the way it enforces immutability for struct and enum. Any API user can subclass our provided class and make it mutable unless we define them as final. This is not the case with struct, enum, and tuples as basically we cannot subclass them.

Value and reference type constants

Constants behave differently if they are value or reference types. We will be able to change the variables in a constant class but we cannot change them for structs.

Let's examine the following example:

class User {
    var name: String
    init(name: String) {
        self.name = name
    }
}

let julie = User(name: "Julie")
let steve = User(name: "Steve")

struct Student {
    var user: User
}

let student = Student(user: julie)
student.user = steve // compiler error - cannot assign to
  property: 'student' is a 'let' constant

In this example, we have a class named User and two constants that point to the instance of the class. Also, we have a Student struct that has a variable of the User type.

We create student using the Student structure. If we try to change the user variable in student, the compiler gives us an error telling that student is a constant even though we defined user as a variable.

So we cannot change any variable in struct if we instantiate it as a constant. In other words, let student = Student(user: julie) makes the whole struct immutable.

Let's try the same operation with classes. In the following code, we change the name of steve, which is defined as a constant. The compiler does not give us an error and accepts this assignment.

steve.name = "Steve Jr." 
steve.name // prints "Steve Jr." 

Even though we defined steve as a constant, we could change the name variable as it was a class.

From the preceding examples, we have seen that we can change the value of a variable on a constant that is an instance of a class (reference type), but we cannot change the value of a variable on a constant that is an instance of a struct (value type).

As steve is an instance of a reference type, it refers to the instance of User. When we change name, we are not actually changing what steve is, which is a reference to User. We change the name that we made mutable by defining it as a variable. This is not the case for our student constant as it is a value type. Defining it as a constant makes its variables constant too.

This property of reference types makes them hard to track and since we are defining them as constants, it is not going to make them immune to changes. To be able to make them immutable, we will need to define their properties as constants.

Mixing value and reference types

In real-world problems, we may need to mix reference types with value types. For instance, we may need to have a reference to class in struct like our previous example or we may need to have a struct variable in class. How would we reason about the assignments and copying in these circumstances?

Let's examine the following example:

class User {
    var name: String
    init(name: String) {
        self.name = name
    }
}
let julie = User(name: "Julie")

struct Student {
    var user: User
}

let student = Student(user:julie)
student.user.name // prints "Julie"
let anotherStudent = student
julie.name = "Julie Jr."
anotherStudent.user.name // prints "Julie Jr."

In this example, we have a User class, a Student struct that has the user variable. We define a constant, student with julie, which is of the class type. If we print student.user.name, the result will be julie.

Now if we define anotherStudent and copy student to it by assignment, changing the name of Julie will change the name of anotherStudent too.

We would expect anotherStudent to have a copy of student but name has been changed. It is changed because the user variable is of the User type, which is class and therefore a reference type.

This example presents the complexity of using reference types in value types. To avoid these complications, it is recommended to avoid using reference type variables inside value types. If we need to use reference types in our value types, as we have stated before, we should define them as constants.

Copying

Assignment operations on value types copy values from one value type to another value type. There are two types of copying in different programming languages, shallow and deep copying.

Shallow copying duplicates as little as possible. For instance, a shallow copy of a collection is a copy of the collection structure, not its elements. With a shallow copy, two collections share the same individual elements.

Deep copying duplicates everything. For instance, a deep copy of a collection results in another collection with all of the elements in the original collection duplicated.

Swift does the shallow copying and does not provide a mechanism for deep copying. Let's examine an example to understand shallow copying:

let julie = User(name: "Julie")
let steve = User(name: "Steve")
let alain = User(name: "Alain")
let users = [alain, julie, steve]

In the preceding example, we created a new User named alain and added three users to a new array named users. In the following example, we copy the users array to a new array named copyOfUsers. Then we change the name of one of our users in the users array as follows:

let copyOfUsers = users
users[0].name = "Jean-Marc"

print(users[0].name) // prints "Jean-Marc"
print(copyOfUsers[0].name) // prints "Jean-Marc"

Printing users and copyOfUsers will show us that changing name of Alain to Jean-Marc in the users array has changed the name of Alain in copyOfUsers to Jean-Marc too. The users and copyOfUsers are arrays, and we would expect assignment expression to copy the values from users to copyOfUsers as arrays are value types but, as we have seen from the preceding example, changing the name of user in one array changed the username in the copied array. There are two reasons for this behavior. First of all, User is a type of class. So it is a reference type. Secondly, Swift does the shallow copying.

Shallow copying does not provide a distinct copy of an instance as we have seen in this example. Shallow copying duplicates the references to the same elements of the instance. So again, this example presents complications with using reference types in value types as Swift does not provide deep copying to overcome these complications.

Copying reference types

Two variables can point to the same object so changing one variable changes the other too. Having lots of objects point to the same data can be useful in some circumstances, but mostly we will want to modify copies so that modifying one object doesn't have an effect on the others. To make this work, we need to do the following:

  • Our class should be of the NSObject type
  • Our class should conform to the NSCopying protocol (which is not mandatory but makes our intent clear for our API user)
  • Our class should implement the copy(with: NSZone) method
  • To copy the object, we will need to call the copy() method on the object

Here's an example of a Manager class that conforms fully to the NSCopying protocol:

class Manager: NSObject, NSCopying {
    var firstName: String
    var lastName: String
    var age: Int
 
    init(firstName: String, lastName: String, age: Int) {
        self.firstName = firstName
        self.lastName = lastName
        self.age = age
    }
 
    func copy(with: NSZone? = nil) -> AnyObject {
        let copy = Manager(firstName: firstName, lastName: lastName,
          age: age)
        return copy
    }
}

The copyWithZone() function is implemented by creating a new Manager object using the information of current Manager. To test our class, we create two instances and copy one instance over the other as follows:

let john = Manager(firstName: "John", lastName: "Doe", age: 35)
let jane = john.copy() as! Manager

jane.firstName = "Jane"
jane.lastName = "Doe"
jane.age = 40

print("(john.firstName) (john.lastName) is (john.age)")
print("(jane.firstName) (jane.lastName) is (jane.age)")

The result will be as follows:

"John Doe is 35"
"Jane Doe is 40"

Value type characteristics

We have examined the notion of value types and reference types. We have looked into simple scenarios of value type versus reference type usage. We understand that using value types makes our code simpler and easier to trace and reason. Now let's look into the characteristics of value types in more detail.

Behavior

Value types do not behave. A value type stores data and provides methods to use its data. A value type can only have a single owner and it does not have deinitializers as there are no references involved. Some of the value type methods may cause the value type to mutate itself, but control flow is rigidly controlled by the single owner of the instance. As the code will only execute when directly invoked by a single owner and not from many sources, it is easy to reason about the value type code execution flow.

On the other hand, a reference type might subscribe itself as a target of other systems. It might receive notifications from other systems. This sort of interactions require reference types as they can have multiple owners. It's unnecessarily difficult to develop value types that perform side effects on their own in most of the cases.

Isolation

A typical value type has no implicit dependencies on the behavior of any external system. Therefore, a value type is isolated. It interacts only with its owner and it is easy to understand how it interacts in comparison to a reference type's interactions with multiple number of owners.

If we access a reference to a mutable instance, we have an implicit dependency on all its other owners and they could change the instance at any time without notifying us.

Interchangeability

As a value type is copied when it is assigned to a new variable, all of those copies are completely interchangeable.

We can safely store a value that is passed to us, then later utilize this value as if it were a new value. It will not be possible to compare the instance with another instance using anything but its data.

Interchangeability also means that it does not matter how a given value was defined. Two value types are equal by all means if comparing them via == results in equality.

Testability

There is no need for a mocking framework to write unit tests that deal with value types. We can directly define values indistinguishable from the instances in our applications.

If we use reference types that behave, we have to test the interactions between the reference type that we will test and the rest of the system. This typically means a lot of mocking or extensive setup code to establish the required relationships.

In contrast, value types are isolated and interchangeable, so we can directly define a value, call a method, and examine the result. Simpler tests with greater coverage yield a code that is easier to change and maintain.

Threats

While the structure of value types encourages testability, isolation, and interchangeability, one can define value types that diminish these advantages. Value types containing code that executes without being called by its owner are generally hard to track and reason about, and should often be avoided.

Also, value types containing reference types are not necessarily isolated. Using reference types in value types should generally be avoided as they are dependent on all other owners of that referent. These kinds of value types are also not easily interchangeable as the external reference might interact with the rest of the system and cause some complications.

Using value and reference types

The Swift Programming Language (Swift 3.0) byApple Inc. has a section on comparing structs (value type) and classes (reference type) and how to prefer one over the other. It is highly recommended to read that section to understand why we prefer one over the other. Although we touched on the topic briefly in Chapter 1 , Getting Started With Functional Programming in Swift, we will explore this topic further as the distinction between reference and value types is very important in functional programming.

In object-oriented programming, we model real-world objects as classes and interfaces. For instance, to model an Italian restaurant with different types of pizzas, we may have a pizza object and subclasses of it such as margherita, napoletana, or romana. Each of these pizzas will have different ingredients. Different restaurants may make them slightly differently, and whenever we read their recipes in different books or websites, we may understand it differently. This level of abstraction enables us to refer to a specific pizza without caring about how other people really imagine that pizza. Whenever we talk about that pizza, we do not transfer it, we just refer to it.

On the other hand, in our Italian restaurant, we will need to provide bills to our customers. Whenever they ask for the bill, we are going to provide real information about quantity and prices. Anyone has the same perception about quantities, prices in dollars, and in fact values. Our customers can calculate the invoice total. If our customers modify the bill, it is not going to modify the source that we used to provide the bill. No matter if they write something on the bill or spill wine on it, the value and bill total amount is not going to change. The preceding example presents a simple real-world usage of reference versus value types. Value types and reference types have their own usages in the Swift programming language and in web, mobile, or desktop application programming.

Value types enable us to make architectures clearer, simpler, and more testable. Value types typically have fewer or no dependencies on the outside state, so there's less that we have to consider when reasoning about them.

Also, value types are essentially more reusable because they are interchangeable.

As we use more value types and immutable entities, our system will become easier to test and maintain over time.

In contrast, reference types are acting entities in the system. They have identity. They can behave. Their behavior is often complex and hard to reason about, but some of the details can usually be represented by simple values and isolated functions involving those values.

Reference types maintain state defined by values, but these values can be considered independently of the reference type.

Reference types perform side effects such as I/O, file and database operations, and networking.

Reference types can interact with other reference types, but they generally send values, not references, unless they truly plan to create a persistent connection with the external system.

It is important to use value types (enums, tuples, or structs) as much as possible unless we need to create a shared mutable state. There are cases where we have to use classes. For instance, when we work with Cocoa, many APIs expect subclasses of NSObject so we have to use classes in these cases. Whenever we need to use classes, we avoid variables; we define our properties as constants and avoid exposing any APIs that can alter states.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset