Swiftpack.co -  sloik/OptionalAPI as Swift Package
Swiftpack.co is a collection of thousands of indexed Swift packages. Search packages.
sloik/OptionalAPI
Optional extensions for Swift Optional Monad... use it or not... it's optional.
.package(url: "https://github.com/sloik/OptionalAPI.git", from: "2.4.1")

OptionalAPI

Optional extensions for Swift Optional Monad... use it or not... it's optional.

Why

Some common idioms popup when working with Optionals in Swift. Here is a bunch of useful extensions for some types.

Besides if let is harmful 🤓

Installation

Just copy and paste files to your project 🍝

Or use SPM 😎

Examples:

Running some code if none or some

Old:

someOptional == nil ? True branch : False branch

New:

someOptional.isSome ? True branch : False branch
someOptional.isNone ? True branch : False branch

someOptional.isNotSome ? True branch : False branch
someOptional.isNotNone ? True branch : False branch

Sequencing of operations that might also return optional

Operation that returns optional:

func maybeIncrement(_ i: Int) -> Int? { i + 1 }

Old the terrible way:

if let trueInt = someIntOptional {
    let incrementedOnce = maybeIncrement(trueInt) {
        // you get the idea ;)
    }
}

andThen

someOptional
    .andThen(maybeIncrement)
    .andThen(maybeIncrement)
    // ... you get the idea :)

In this case result of this chaining is a instance of Int?. If the someOptional was nil then whole computation results with nil. If it had some value (42) ten it would be incremented so many times.

Recovering from none case

Let's say you have a chain of operations and there is a chance that the result might return none.

func returningNone(_ i: Int) -> Int? { Bool.random() ? .none : i }

someOptional
    .andThen(maybeIncrement)
    .andThen(returningNone)  // <-- returns nil
    .andThen(maybeIncrement)  

Final result is nil. And you can't use a ??. Use mapNone it's like normal map on Optional but for the nil case.

func returningNone(_ i: Int) -> Int? { .none }

someOptional
    .andThen(maybeIncrement)
    .andThen(returningNone)  
    .mapNone(42)
    .andThen(maybeIncrement)  

If someOptional started with 10 and we had luck (returningNone did not returned nil) then the final result is 12. But if were not so lucky then the mapNone would take over and the final result would be 43.

You can also use more than one mapNone to handle any failures along the way. Oh and you can use an more friendly name defaultSome like so:

someOptional
    // if someOptional is nil then start computation with default value
    .defaultSome(5)     
    // increment whatever is there         
    .andThen(maybeIncrement)
    // are you feeling lucky?
    .andThen(returningNone)  
    // cover your ass if you had bad luck
    .defaultSome(42)
    // do some work with what's there
    .andThen(maybeIncrement) 
    // what... again
    .andThen(returningNone)  
    // saved
    .defaultSome(10)

I hope you can see that this gives you a very flexible API to handle Optionals in your code.

andThenTry

This operator expects an transformation that may throw an error. When this happens it returns .none which alows to recover with other operators.

let jsonData: Data? = ...

jsonData
    .andThenTry{ data in 
        try JSONDecoder().decode(CodableStruct.self, from: data) 
    }
    // this can also explode!
    .andThenTry( functionTakingCodbaleStructAndThrowing ) 
    // if any did thow an error then just recover with this one
    .defaultSome( CodableStruct.validInstance ) 

You can revocer differently after different tries. Or you can totaly ignore it. Either way you have a nice API.

But wait there's more!

Sometimes you are working with a Optional collection. Most common case is a String and and Optional Array of something. This Optional API has you covered to!

In the examples below I will be using those Optionals:

let noneString     : String? = .none
let emptySomeString: String? = ""
let someSomeString : String? = "some string"

let noneIntArray : [Int]? = .none
let emptyIntArray: [Int]? = []
let someIntArray : [Int]? = [11, 22, 33]

I think this should cover all the cases

Optional collection has values is nil or empty

A lot of ifology is made when working whit a collection inside a Optional context. Those properties should help.

hasElements

noneString.hasElements      // false
emptySomeString.hasElements // false
someSomeString.hasElements  // true

noneIntArray.hasElements  // false
emptyIntArray.hasElements // false
someIntArray.hasElements  // true

isNoneOrEmpty

noneString.isNoneOrEmpty      // true
emptySomeString.isNoneOrEmpty // true
someSomeString.isNoneOrEmpty  // false

noneIntArray.isNoneOrEmpty  // true
emptyIntArray.isNoneOrEmpty // true
someIntArray.isNoneOrEmpty  // false

recoverFromEmpty

This is called only if the underlying collection is empty. That is if your optional is nil or has some value this will not be called. As String is a collection I will only show examples for [Int]? :)

noneIntArray.recoverFromEmpty([42])  // nil
emptyIntArray.recoverFromEmpty([42]) // [42]
someIntArray.recoverFromEmpty([42])  // [11, 22, 33]

If you need a default value for the none case then defaultSome is the thing you want.

noneIntArray.defaultSome([42])  // [42]
emptyIntArray.defaultSome([42]) // []
someIntArray.defaultSome([42])  // [11, 22, 33]

or

There are cases when you need an actual result from an Optional or a default non optional value. This is exactly the case for or

let noneInt: Int? = .none
let someInt: Int? = .some(42)

var result: Int = someInt.or(69) // 42

In this case result variable stores value 42. It's an honest Int not an optional. But what happens when it's none:

result = noneInt.or(69) // 69

Here the final result is 69 as everything evaluates to none. Once again after or you have a honest value or some default.

default value with or

If the wrapped type has a empty initializer (init that takes no arguments) you can call it to get an instance:

someOptional
    .or(.init()) // creates an instance

To put it in a context if you have some optionals you can use this to get zero value like so:

let noneInt: Int? = nil
noneInt.or( .init() ) // 0
noneInt.or( .zero   ) // 0

let noneDouble: Double? = nil
noneDouble.or( .init() ) // 0

let defaults: UserDefaults? = nil
defaults.or( .standard ) // custom or "standard"

let view: UIView? = nil
view.or( .init() ) 

// or any other init ;)
view.or( .init(frame: .zero) )

// Collections
let noneIntArray : [Int]? = .none
noneIntArray.or( .init() ) // []

let emptySomeString: String? = ""
noneString.or( .init() ) // ""

// Enums
enum Either {
    case left, right
}
let noneEither: Either? = nil
noneEither.or(.right)

Anything that you can call on this type (static methods) can be used here.

zip

Zip function is defined on sequences in Swift. This is a nice extension to have it on Optional.

Let say you have some computations or values that are optional. It might be tedious to if let them. Using zip you just flip the container inside out (check out how type is transormed in this documentation on zip in haskell) and map on the result.

let userName: String? 
let userLast: String?
let userAge: Int? 

zip(userName, userLast, userAge)
    .map{ (name: String, last: String, age: Int) in 
        // Working with not optional values
     }

And map can be replaced with andThen:

zip(userName, userLast, userAge)
    .andThen{ (name: String, last: String, age: Int) in 
        // Working with not optional values
     }

Under the hood map is used by it reads better.

cast

Have you ever wrote code similar to this one:

if let customVC = mysteryVC as? CustomVC {
    // do stuff
}

With cast you can streamline your code to this:

 let someViewController: UIViewController? = ...
 someViewController
     .cast( CustomVC.self )
     .andThen({ (vc: CustomVC) in
        // work with a non optional instance of CustomVC
     })

If the type can be inferred from the context then you do not have to type it in.

let anyString: Any? = ...

let result: String? = anyString.cast()

As you can see compiler is able to inferred the correct type. But be aware that in more complex cases this can slow down your compilation.

If you want to have faster compilation then always be explicit about your types. In all of your code not only using this package.

encode & decode

One of the common places when you want to encode or decode something is when you have some data from the network. Flow might look something like this:

  • make a API call for a resource
  • get JSON data

To keep is simple let's say our Data Transfer Model (DTO) looks like this:

struct CodableStruct: Codable, Equatable {
    let number: Int
    let message: String
}

What happens is that a JSON string is send thru the network as data. To simulate this in code one could write this:

let codableStructAsData: Data? =
    """
    {
        "number": 55,
        "message": "data message"
    }
    """.data(using: .utf8)

Stage is set:

decode

Networking code will hand us an instance of Data? that we want to decode.

let result: CodableStruct? = codableStructAsData.decode()

It's that simple. Compiler can infer the type so there's no need to add it explicitly. Buy you can do it in some longer pipelines eg.:

codableStructAsData
    .decode( CodableStruct.self )
    .andThen({ instance in
        // work with not optional instance
    })

encode

Encode goes other way. You have a instance that you want to encode to send it as a json.

let codableStruct: CodableStruct? = 
    CodableStruct(
        number: 69, 
        message: "codable message"
    )

To get the desired encoded vale just use the method:

codableStruct
    .encode() // <- encoding part if you missed it ;)
    .andThen({ instance in
        // work with not optional instance
    })

whenSome and whenNone

When working with optionals it happens that you want to run some code but not change the optional. This is where whenSome and whenNone can be used.

    let life: Int? = 42
    
    life
        .whenSome { value in
            print("Value of life is:", value)
        }

This code prints to the console: Value of life is: 42.

whenSome also comes in a favor that does not need the argument.

    let life: Int? = 42
    
    life
        .whenSome { 
            print("Life is a mistery. But I know it's there!")
        }

This is a very nice way of triggering some logic without having to write if statements. But what about when the optional is none (or how it's known nil)?

whenNone is here for the rescue.

    let life: Int? = .none
    
    life
        .whenNone { 
            print("No life here!")
        }

No life here! will be printed in the console.

But what's eaven more cool is that you can chain them!

    let life: Int? = 42
    
    life
        .whenSome { value in
            print("Value of life is:", value)
        }
        .whenSome { 
            print("Life is a mistery. But I know it's there!")
        }
        .whenNone { 
            print("No life here!")
        }

Depending on the operator and the value of optional different blocks will be called. And efcourse other operators can be thrown in to the mix.

That's it

Hope it will help you :)

If you want to find out more about me and some more Swift stuff then I invite you to my site: http://idoit.tech/en/

Cheers! :D

GitHub

link
Stars: 7
Last commit: 4 weeks ago

Ad: Job Offers

iOS Software Engineer @ Perry Street Software
Perry Street Software is Jack’d and SCRUFF. We are two of the world’s largest gay, bi, trans and queer social dating apps on iOS and Android. Our brands reach more than 20 million members worldwide so members can connect, meet and express themselves on a platform that prioritizes privacy and security. We invest heavily into SwiftUI and using Swift Packages to modularize the codebase.

Swiftpack is being maintained by Petr Pavlik | @ptrpavlik | @swiftpackco | API