In Swift, we can use classes, structures and enumerations, as well as options and results, all of which have different meanings for the code we write.

We have a lot of freedom in writing code, but we should consider choosing types and leveraging the type system so that our code accurately simulates the data of the domain we are using.

Libraries that place text may define various ways to align text, which can be expressed as integers, where 0 means left-aligned, 1 means right-aligned, and 2 means centered. But using integers to represent text alignment allows values that don’t make any sense – for example, what should the library do with the value 27? It makes sense to define possible values using enumerations to ensure that only valid values exist.

URLSession completion handler

) In Foundation, we found an API where the types used can cause some confusion. When our URLSession loads data from the URL, we must provide a completion handler with three parameters:

func dataTask(with request: URLRequest, completionHandler: @escaping (Data? , URLResponse? , Error?) -> Void) -> URLSessionDataTask`Copy the code

All three parameters are optional, so any or all of them can be present or absent. This means that, in theory, the completion handler must handle eight possible states. If we write parameters differently, we can see more clearly:

struct CallbackInfo {
    var data: Data?
    var response: URLResponse?
    var error: Error?
}
Copy the code

In fact, not all eight possible states can occur. We can read the documentation for hints on how to invoke callbacks, but the documentation doesn’t give us the assurance that compilers and type systems do.

We can think about which countries make sense for themselves. We can assume that if we get the data, then we will not receive errors. The other way around: if we receive an error, we have no data. We can model these two cases as enumerations:

enum CallbackInfo2 {
    case success(Data, URLResponse)
    case failure(Error)
}
Copy the code

But we are not sure that this enumeration covers all possible states. Of the eight possible states of the structure, CallbackInfo, there may be states that the CallbackInfo2 enumeration cannot express.

By reading the documentation of the data task approach, it is difficult to discern what might happen and what will never happen. Arguably, the enumeration-based method CallbackInfo2 can better explain to the user what needs to be handled.

On the other hand, we cannot say that enumerations are always better than optional arguments. If we are dealing with four or five options and all possible combinations are possible, then we must define a huge enumeration of 16 or 32 cases. Doing so probably won’t make the API any easier to use.

We can already illustrate this problem with our own example. Suppose the failure case also comes with an optional Data? :

enum CallbackInfo2 {
    case success(Data, URLResponse)
    casefailure(Data? , Error) }Copy the code

Since both cases can contain data, it makes more sense to provide the data as an optional attribute rather than hiding it in the associated value of the enumeration, which makes access more difficult.

Enumerations are not always better than a set of options and vice versa, but it depends on the possible states we are trying to model.

The user’s session

The second example comes from Apple’s book on Swift, the Swift programming language:

struct Session {
    var user: User?
    var expired: Bool
}
Copy the code

Here we have a user session. The User attribute is optional because there may be no registered users. This model of a user session allows for four possible states: whether the user can exist or not, and the expired Boolean attribute can be true or false.

The Swift book then goes on to say that we can choose to model sessions as enumerations to eliminate states that did not occur, we have no users and expired sessions:

enum Session1 {
    case loggedIn(User)
    case expired(User)
    case notRegistered
}
Copy the code

The enumerated version simulates the domain more precisely than the structure because it can only represent the possible state of the user session.

But as in the previous example, we now have two cases where related values are shared, and we have to switch the enumeration so that the User extracts a from the session. A third approach is to model the session so that it is easier to access the user without becoming less precise:

struct Session {
    var user: User
    var expired: Bool
}

var session: Session?
Copy the code

Here we use a structure again, but this time the user attribute is not optional. Instead, the session itself is stored in optional variables. One possible state is that session is nil, meaning Session1 enumeration is the same as notRegistered. In the other two states, a session exists and is therefore also a user, and the session is expired or not expired.

We have encountered many cases where multiple cases of an enumeration share the same associated value, we can usually wrap the enum in a struct and pull the associated value out of the struct’s properties.

Map file names to data

Let’s look at another example. Suppose we have an array of filenames as a string, and we’re writing a function that maps the array and returns data from the file. What should be the result type of the function?

This function simply returns an array of Data:

func readFiles(_ fileNames: [String]) -> [Data] {
    // ... }
Copy the code

That works, but what happens if one of the files doesn’t exist or can’t be read? This function can omit the file’s data and return the rest, but as users, we have no way of knowing which files failed.

The result type can also be an array of options:

func readFiles(_ fileNames: [String]) -> [Data?] {/ /... }Copy the code

Then we can try to find out which files loaded successfully, but we can’t be completely sure because we can’t guarantee that the result array is sorted in the same way as the input array.

We might want to report errors about missing files, so the function should probably return the filename along with optional data values, combined in a tuple:

func readFiles(_ fileNames: [String]) -> [(String, Data?)] {/ /... }Copy the code

Another option is to make the entire array optional. This makes the result all-or-nothing: we either get data from all requested files, or we fail to use one of them and we get nothing at all:

func readFiles(_ fileNames: [String]) -> [(String, Data)]? {/ /... }Copy the code

Even with simple features like the one above, we can easily think of seven variations. For example, we might decide to return a Result instead of an optional one, or we might want to include a custom enumeration that describes different types of failures. Choosing between types depends entirely on what makes the most sense to your application.

Accuracy and ease of use

We can go one step further and try to force the input and output arrays of the readFiles function to have the same length. There are programming languages that let you express this, but in Swift we also have a few tricks that can help.

We can try to mark an array of length in some way. We can then define a map function readFiles that is reserved length and implemented using this mapping. But as we push how much information we can put into a type, we should ask ourselves if the added complexity is worth it.

Less strict typing means we need to trust a piece of code implementation more. We can always write tests to check how our code behaves when we provide lots of sample input.

The library has many examples, and for simplicity, the types used are not the most accurate descriptions of what they do. First, an Array is indexed by integers (Int), not by unsigned integers (UInt), even though the index is never negative.

The same is true for count arrays or strings. The amount can never be negative, so use UInt instead of the more precise Int. But this makes the count attribute harder to use, because in most cases we have to cast the type to pass its Int to the other API.

Choosing the right type means a trade-off between accuracy and ease of use. The best approach is to explore the different types and determine the one that is most accurate and best describes whatever type we describe, but does not contain any garbage values that might represent impossible states.

Use phantom type

In our episode on phantom types using Brandon Kase, we discussed the concept of token types to make them more descriptive, and more restrictive, with the intention of using the type system to help prevent misuse of our API.

We can find practical examples of the phantom types used in automatic layout. There, the view’s anchor points are marked with a phantom type to distinguish between horizontal and vertical anchors. This makes it impossible, for example, to fix the lead anchor to the top anchor point, which makes no sense.

The problem of accurately modeling domain-specific data arises automatically in the strongly typed language community. Richard Feldman, one of the leading elm developers, spoke of doing the impossible. There are similar discussions about F#, OCaml and Haskell, all of which discuss how to find data representations and only allow you to define functions that make sense.

Original address:Talk. Objc. IO/episodes/S0…