Functional programming – Functor, Monad, Applicative in Swift

preface

During the winter vacation, I made a Haskell book, which was soon put on hold due to my sudden project tasks. However, I could not understand all kinds of concepts in the book, which were slightly abstract. In addition, I did not properly practice the Demo at that time, so my mind was completely empty soon. Fortunately, Swift is a language that is highly compatible with functional programming paradigm, and I am a program Dog that likes to tap Swift. When I use Swift coding later, I consciously or unconsciously apply some concepts of functional programming paradigm, and gradually deepen my understanding of functional programming. This article is a small summary of what I know about functional programming, focusing on the concepts of Functor, Applicative, and Monad and how they are represented in Swift. Due to my limited ability, I hope you can forgive me for some imprecise concepts and incomplete coding. Welcome to leave your valuable comments or questions.

This article is about pure concept, later may have functional programming actual combat article launch (I have time to write again 😊)

concept

Context

When we code, we have all kinds of data types, and the basic data type is called a value, but not the basic data type in a programming language, such as integer 1, which is called a value, a struct Person {let name: String; An instance of let age: Int} can also be a value, so what is Context? We can think of it as a wrapper around the value, and through this wrapper we can know what state we are in at that time. In Haskell, this wrapper is typeclass. In Swift, the magic enum can play this role. For example, the Optional type in Swift is defined as follows (the related inheritance or protocol relationships are not marked here):

Optional<Wrapped> {
    case none
    case some(Wrapped)Copy the code

Optional has two states: none, which is equivalent to the nil we passed in, and Wrapped, which is the type of value that’s Wrapped in that context. From this example, we can understand the Context intuitively: describes the state of the value at a certain stage. Of course, in normal development, we see various contexts, such as Either:

enum Either<L.R> {
    case left(L)
    case right(R)}Copy the code

It means that at some stage the value may exist in either left or right. In some functional responsive programming frameworks such as ReactiveCocoa and RxSwift, Context is everywhere: RACSignal, Observable, and even Swift’s basic Array can be considered a Context itself. So as you can see, whenever you touch functional programming, Context touches.

Here, I specifically mention the Context: Result, because I’m going to use it as a basis for other concepts and actual presentations:

enum Result<T> {
    case success(T)
    case failure(MyError)}Copy the code

The Result context has two states: a success state, in which Result holds a value of a specific type, and a failure state, in which you can get an instance of an error (which you can write yourself). So what’s the use of this Context? Imagine that you are working on a network operation, access to the data is not sure, you may be able to as you wish, to derive it from the server to your expected value, but may also be some unknown error occurred when the server, or network latency, or some of the force majeure influence, so, at this point you will get an error, Such as HTTP Code 500… Result can be introduced in this case to indicate whether you get the final Result of the network operation, success or failure. In addition to network requests, such as database operations, data parsing, and so on, Result can be introduced for more explicit identification.

What is Functor, Applicative, Monad?

Think of Functor, Applicative, and Monad as protocols in Swift. They can be abstractions of data structures that interface to the Context I mentioned earlier. To implement a Context as Functor, Applicative, or Monad, you must implement specific functions within it, so to understand what Functor, Applicative, or Monad are, you need to know which protocol functions they define. I’m going to go through them all.

Functor

For example, if I want to double the value of an integer, we can define a function:

func double(_ value: Int) -> Int {
    return 2 * value
}Copy the code

We can then use this function to operate on specific values:

let a = 2
let b = double(a)Copy the code

Ok, so the question is, what if this value is now wrapped in a Context? A function can only operate on the specific type of value it declares. Functions that operate on an integer cannot be used to operate on a non-integer Context, so we introduce Functor. All it does is make a value-only function work on a Context containing the value type, and finally return a Context containing the result. To do this, we implement map (fmap in Haskell), and its pseudocode looks like this: Context(result value) = map(Context(initial value), operation function)

Now we implement this with Result:

extension Result {
    func map<O>(_ mapper: (T) -> O) - >Result<O> {
        switch self {
        case .failure(let error):
            return .failure(error)
        case .success(let value):
            return .success(mapper(value))
        }
    }
}Copy the code

As we can see, first we match the pattern of Result. When the status is failure, we return failure directly and pass the instance of the error. If the status is success, we calculate the initial value and return the success status of the package with the Result value. For the sake of simplicity, I define the map operator <^> here:

precedencegroup ChainingPrecedence {
    associativity: left
    higherThan: TernaryPrecedence
}

// Functor
infix operator< ^ > :ChainingPrecedence

// For Result
func <^><T, O>(lhs: (T) -> O, rhs: Result<T- > >)Result<O> {
    return rhs.map(lhs)
}Copy the code

We can test it now:

let a: Result<Int> = .success(2)
let b = double <^> aCopy the code

I mentioned above that the Swift array can also be thought of as a Context, which exists as a package with multiple values. The map function in the Swift array is often used in daily development:

let arrA = [1.2.3.4.5]
let arrB = arrA.map(double)Copy the code

We also use map a lot in RxSwift:

let ob = Observable.just(1).map(double)Copy the code

Applicative

Applicative is an advanced Functor, and we can call up the map pseudocode for Functor: Context(resulting value) = map(initial value) In functional programming, a function can also be treated as a value, if the function is also wrapped in a Context, a map alone cannot accept a Context wrapped around the function, So we introduce Applicative: Context(result value) = apply(Context(initial value), Context(operation function))

We implement Result Applicative:

extension Result {
    func apply<O>(_ mapper: Result<(T) -> O- > >)Result<O> {
        switch mapper {
        case .failure(let error):
            return .failure(error)
        case .success(let function):
            return self.map(function)
        }
    }
}

// Applicative
infix operator< * > :ChainingPrecedence

// For Result
func <*><T, O>(lhs: Result<(T) -> O>, rhs: Result<T- > >)Result<O> {
    return rhs.apply(lhs)
}Copy the code

Use:

let function: Result< (Int) - >Int> = .success(double)
let a: Result<Int> = .success(2)
let b = function <*> aCopy the code

Applicative is not used much in everyday development. Most of the time we don’t want to stuff a function into a Context, but it can be powerful when you use higher-order functions. Here’s a slightly more obscure example that you can spend some time understanding: The idea of this example is from the source of the Swift functional JSON parsing library Argo basic usage, if you are interested in reading Argo source code: Suppose I now define a function that takes an Any JSON Object and a Key that corresponds to the value in JSON, and returns a result parsed from the JSON data. Since the result is inconclusive, There may not be a value for this key in JSON, so we’ll wrap it in Result. This function is signed as:

func parse<T>(jsonObject: Any, key: String) -> Result<T>Copy the code

When the parse succeeds, the Result returned is in the successful state; when the parse fails, the Result returned is in the failed state and carries the wrong entity. We can learn the reason for the parsing failure from the wrong entity.

Now we have a structure with multiple members that implements the default constructor:

struct Person {
    let name: String
    let age: Int
    let from: String
}Copy the code

For example, we have a function whose basic signature is: func haha(a: Int, b: Int, c:) Int) -> Int, we can convert it to (Int) -> (Int) -> (Int) -> (Int) -> Int. Curry (person.init), and we get a value of type (String) -> (Int) -> (String) -> Person. Now for the magic, I define a function that parses JSON into Person:

func parseJSONToPerson(json: Any) -> Result<Person> {
    return curry(Person.init)
        <^> parse(jsonObject: json, key: "name")
        <*> parse(jsonObject: json, key: "age")
        <*> parse(jsonObject: json, key: "from")}Copy the code

With this function, I can parse a JSON data into an instance of Person and return it as a Result wrapper, and if parsing fails, the Result processing failure status will carry an instance of the error.

So why is this function written this way? Let’s break it down: We get (String) -> (Int) -> (String) -> Person, which is also a function, and then <^>map. It is of type Result

, map applies the function (String) -> (Int) -> (String) -> Person to Result

, Return (Int) -> (String) -> Person Result wrapper: Result<(Int) -> (String) -> Person>(because a parameter has been consumed), this function is wrapped in a Context, and we can’t use map to apply this function to the data that we parse. The parse function returns a Result of type Result

that parses the JSON. Result<(Int) -> (String) -> Person> Result<(String) -> Person> Result<(String) -> Person> And so on, we get the Result parsed by JSON. The power of Applicative to make code so elegant is part of the beauty of functional programming.


Monad

Monad is called Monad in Chinese. Many people are shocked by the concept of Monad on the Internet. In fact, it is based on the concept described above. For those of you who have used functional responsive programming frameworks (Rx series [RxSwift, RxJava], ReactiveCocoa), you may not know what Monad is, but you must have used it in real life. The required functions are simply flatMap:

let ob = Observable.just(1).flatMap { num in
    Observable.just("The number is \(num)")}Copy the code

A lot of people like to describe flatMap’s ability to reduce dimension, but it can do more than that. The function Monad needs to implement is called bind, which in Haskell uses the symbol >>=, and in Swift we define the operator >>- to represent bind, or flatMap. First, we define a function that wraps a value. This function’s signature is: function :: Bind (A, B, B); bind (B, B, B); bind (B, B, B);

extension Result {
    func flatMap<O>(_ mapper: (T) -> Result<O- > >)Result<O> {
        switch self {
        case .failure(let error):
            return .failure(error)
        case .success(let value):
            return mapper(value)
        }
    }
}

// Monad
infix operator> > - :ChainingPrecedence

// For Result
func >>-<T, O>(lhs: Result<T>, rhs: (T) -> Result<O- > >)Result<O> {
    return lhs.flatMap(rhs)
}Copy the code

The definition of Monad is simple, but what exactly does Monad help us solve? How does it work? Don’t worry, you can get a better understanding of Monad by using the following example: Suppose I now have a series of operations:

  1. Query the local database through specific conditions to find the relevant data
  2. Use the data from the database above as parameters to make a request to the server and get the response data
  3. Convert raw data from the network to JSON data
  4. Parses the JSON data and returns the final parsed entity of a specific type

From the analysis of the above operations, we can see that the final result of each operation is uncertain, which means that we can’t guarantee that the operation will complete 100% and return the data we want, so we can easily think of using the Context defined above: Reuslt wraps the obtained results. If the obtained results are successful, Result will carry the Result value in the successful state; if the obtained results fail, Result will carry the error information in the failed state. Now we define the function for each of these operations:

// A represents the type of condition to look up data from the database
// B represents the type of result expected from the database
func fetchFromDatabase(conditions: A) -> Result<B> {... }// Type B is used as the parameter type to initiate network requests
// The obtained data is of type C, which may be raw string or binary
func requestNetwork(parameters: B) -> Result<C> {... }// Convert the obtained raw data type to JSON data
func dataToJSON(data: C) -> Result<JSON> {... }// Parses JSON to output entities
func parse(json: JSON) -> Result<Entity> {... }Copy the code

Now let’s assume that all operations are performed in the same thread (not the UI thread). If we were to call these functions purely in the basic way, we might do this:

var entityResult: Entity?
if let .success(let b) = fetchFromDatabase(conditions: XXX) {
    if let .success(let c) = requestNetwork(parameters: b) {
        if let .success(let json) = dataToJSON(data: c) {
            if let .success(let entity) = parse(json: json) {
                entityResult = entity
            }
        }
    }
}Copy the code

The code looks like a lot of tears to write, and there is a drawback that we can’t get the wrong information from it. If we want to get the wrong information, we have to write a lot more code.

And here comes Monad:

let entityResult = fetchFromDatabase(conditions: XXX) >>- requestNetwork >>- dataToJSON >>- parseCopy the code

Just one line of code connects all the things that need to be done, and finally we get the data wrapped by Result. If there is an error during the operation, the error information is also recorded in the data. This is the power of Monad 😏

Of course, we can continue to optimize the above operation. For example, now I need to add one more parameter to the network request function, which represents the URL of the request. We can define the network request function like this:

// Type B is used as the parameter type to initiate network requests
// The obtained data is of type C, which may be raw string or binary
func requestNetwork(urlString: String)- > (B) - >Result<C> {
    return { parameters in
        return{... }}}Copy the code

To do this, we just need to call:

let entityResult = fetchFromDatabase(conditions: XXX) >>- requestNetwork(urlString: "XXX.com/XXX/XXX") >>- dataToJSON >>- parseCopy the code

This is mainly the use of higher-order functions.

My personal summary of Monad’s role has two parts:

  1. Chain together a series of operations on values and Context, and the code is extremely elegant and clean.
  2. To mask the conversion between the values and the Context, the operations that are going on inside the Context, as I did in the original way, we need to manually analyze the Context, manually perform operations on different Context states, whereas if we useMonadThe whole process we don’t need to do anything just sit back and get the final result.

conclusion

Swift is a language highly adapted to functional programming paradigm. You can find the idea of functional programming everywhere in Swift. Through the introduction of related concepts of Functor, Appliactive and Monad above, I hope it can help you to understand functional programming besides consolidating my knowledge of functional programming. If there are any loose concepts or mistakes in the article, PLEASE forgive me, and also hope to put forward to me. Thanks for reading.

Refer to the link

Ruan Yifeng’s weblog – Illustrated Monad