Swift enum advance topic

Love for Enum, continuing…



On our previous blog post we had some talk, which covers the surface of enum. Here we will dive a bit deeper for familiarizing ourself with some more advance concepts of enum. And Finally on the next case study blog post, tic tac toe through enum, we will wrap it up by using our gained enum knowledge to implement a real life problem. So let’s get started on our swift enum advance talk.

For a more in-depth knowledge on enum we can visit the enum hub page, where we listed all the blog post related with Swift enum.

Background

Generic enum

Generic itself is a big topic. We will cover generic on some future blog series. But here we can have a glimp of Generic usage on enum. So what is the generic concepts? Generic basically broader the boundary of types, ie String Int etc, So that a larger number of types can be target of some task. In other words we can use multiple types all together when we put those types under Generic. We can also shorter the boundary by applying some constraints. On the following code T itself is generic, but we are setting a constraint CustomStringConvertible.

enum Status<T: CustomStringConvertible>{
    case success(T)
    case failed
}

extension Status{
    var description: String{
        switch self {
        case .success(let text):
            return text.description
        case .failed:
            return "Failed...."
        }
    }
}

Status.success("Good to go").description /"Good to go"
Status.success(5).description // "5"

Error definition through enum

On the Swift ecosystem enum are the best candidate for defining Error. The Error itself does not have any requirements. And this Error instance can be thrown by Swift error handling system.

Say we have an enum Mismatched

enum Mismatched : String{
    case notFound
    case undefined
    case invalid
}

We can confirm the Error protocol to make Mismatched throwable.

extension Mismatched: Error{}

So our throwable function:

func find(text: String?, on sentence: String?) throws {
    guard let text = text, text.count > 0 else { throw Mismatched.undefined }
    guard let sentence = sentence, sentence.count > 0 else { throw Mismatched.invalid }
    
    if !sentence.contains(text) { throw Mismatched.notFound }
}

As we can see, if the target text is not set, then we will throw an undefined error. Similarly for the invalid sentence we will throw an invalid error. And finally notFound if not found.

do {
    try find(text: nil, on: "Some input")
} catch Mismatched.undefined { Mismatched.undefined.rawValue } //undefined

do {
    try find(text: "any", on: nil)
} catch Mismatched.invalid { Mismatched.invalid.rawValue } //invalid

Now for those two input we will get the corresponding error.

enum makes it much easier. 😊

For a far more details talk on Swift’s Error handling, we can always visit another swift enum advance talk, Error handling through try variance.

Enum mutation, code smelling!!!

The concept is very clear. enum is a value-based instance and it is been chosen by us for its immutability property. Thats mean always there will be only a single entity of that enum. If we need another instance of different value for that enum it has to be created. The previous was will not be modified. Thats it, full-stop.

If we saw something other that, an enum is being modified, then we can be sure there is a code smelling.

value types, such as enum, struct or tuple, are not used for mutation. If we need mutation then class is the way to go.

Now, how will we find an enum is being modified/mutated. Simple the instance method which mutate the enum will be prefixed with mutate keyword. Example coming through:

enum State{
    case notConnected
    case connecting
    case disconnected
    case connected(String)
    
    indirect case currentState(State)
}

extension State{
    mutating func updateCurrentState(state: State) {
        self = .currentState(state)
    }
}

var state = State.connecting
state.updateCurrentState(state: .connected("Oh ya"))

We will talk about the indirect just on the next section. As we can see the updateCurrentState(state: State) modified the self or the enum instance.

We already talked about mutating of value types, have a look.

Recursive enum

The name suggest it all. Yes this type of enum is recursive, so they can call themselves. We express the recursive enum with the indirect keyword, placed either in front of case declaration or in front of enum declaration.

indirect enum Buffer{
    case data(String)
    case append(Buffer, Buffer, Buffer)
}

On the above we can see the append case will take three of its sibling Buffer case. Now we can write:

let mobi = Buffer.data("Mobile")
let dev = Buffer.data("development")
let talk = Buffer.data("talk")

let mobiDevTalk = Buffer.append(mobi, dev, talk)

The mobiDevTak takes three Buffer instance, mobi, dev and talk. Hmm ok no deal. But what to do with that. Lets convert the Buffer to String .

extension Buffer{
    func convert() -> String {
        switch self {
        case .data(let text):
            return text
        case .append(let appendedBuffer):
            return appendedBuffer.0.convert() + " " + appendedBuffer.1.convert() + " " + appendedBuffer.2.convert()
        }
    }
}

As we can see for .data(let text) case we are just returning the String. But on the .append(let appendedBuffer)we are recursively calling convert()to get the full string. Just a note: Swift is smart enough to define appendedBuffer as Tuple 😉.

Now we can get the full string for append(Buffer, Buffer, Buffer).

mobiDevTalk.convert() //"Mobile development talk"

On the above we saw how to use recursive enum. Now the most important question should we use recursive enum? Well it may add some complexity to our code, but gives us the ability encapsulate the similar concerns. So it depends on the situation.

allCases of CaseIterable

We will found this feature of enum very useful when we need to do some common operation over all the case of that enum and definitely on the TDD, Test Driven Development, time. This feature was added on Swift 4.2 with Xcode 10.0+. Prior to allCases there was some wired ways to list all the cases of an enum.

As we can already understand, this feature will list all the cases of an enum. So an example please:

enum Direction{
    case north
    case south
    case east
    case west
}
extension Direction: CaseIterable{}
Direction.allCases //[north, south, east, west]

Pretty simple. Now why do we write the CaseIterable on the extension of Direction. It is kind of a good practice to have separated implementation for different protocol. So what will happen if there is another protocol? Probably another extension. “Separation of concerns” or “Single responsibility” fits well with this type of extension based implementation of different concern aka protocol.

At the end

We talked about swift enum advance topic. But still a lot of things to talk about, they are not cover on this blog post, something like patterns matching needs separated blog series. And there are more. But we have to move on. On the next blog post we will go with the case study of Tic-tac-toe using enum. See you around.

The code used for this blog post can be found on Github

2 thoughts on “Love for Enum, continuing…

Leave a Reply

Notifications for mobidevtalk! Cool ;) :( not cool