preface
Recently Haskell has fallen into a deep hole that will be hard to climb out of. As I learn more about Haskell concepts and use them to solve real-world problems, I think about porting those concepts to Swift. Many of the concepts of functional programming paradigms in object-oriented languages such as Swift, like design patterns, elegantly help us build our projects and make our code more beautiful, elegant, secure and reliable. In this article, the second in a series called “Functional Programming,” I’ll cover some of Monad’s small concepts and try to incorporate Monad into Swift to make it useful for our actual engineering projects.
Some insights on Monad and implementing Monad in Swift
Monad review
As mentioned in our last article functional Programming – An Overview of Functor, Monad, and Applicative, we can wrap a value around a Context that not only represents itself but also contains some additional information, Bind (>>=) is one of the most important functions in Monad. Bind (>>=) allows you to focus only on the values in the calculation. You don’t have to spend extra effort to deal with Context changes and transformations during the computation. In plain English, we’ll just do the value manipulation and leave the Context to Bind’s internal implementation. Here’s an Optional monad from Swift:
Func bind<O>(_ f: func bind<O>(_ f: (Wrapped) -> Optional<O>) -> Optional<O> { switch self { case .none: return .none case .some(let v): Return f(v)}}} // define bind operator '>>-' PRECEDencegroup bind {associativity: left higherThan: DefaultPrecedence } infix operator >>- : Bind func >>- <L, R>(lhs: L? , rhs: (L) -> R?) -> R? Bind (RHS)} // A B C // (Double) -> (Double) -> Double? Func divide(_ num: Double) -> Double? { return { guard num ! = 0 else {return nil} return $0 / num}} let ret = divide(2)(16) >>- divide(3) >>- divide(2) // 1.33333333... // let ret = option.some (16) >>- divide(2) >>- divide(3) >>- divide(2) let ret = option.some (16) >>- divide(2) let ret = option.some (16) >>- divide(2) >>- divide(0) >>- divide(2) // nilCopy the code
As above, I implemented the Optional type in Swift as Monad, so for an Optional data type, its context is whether the data is null. Divide Divides two numbers and returns nil if the divisor is 0 to secure the operation. In the end, I performed two consecutive operations, ret and ret2. It can be seen that if all the divisor in the operation is not 0, the result after the continuous division operation is finally returned. If a certain divisor in the operation is 0, the returned result will be nil. You can see that we’re just focusing on the method and the data that’s involved in the operation, and we’re not spending any extra time checking if the divisor is zero, and if it’s zero, we’re going to stop the operation and return nil, because bind has taken care of that context for us.
Swift implementation of Monad
Haskell’s powerful type system, coupled with its high support for Monad (such as the do syntax sugar), makes it easy to create and use Monad. However, for Swift, due to its generic system and syntax limitations, we are not able to implement Monad as gracefully as Haskell. Personally, THERE are two reasons:
The protocol in Swift cannot define Monad
In Haskell, Monad is defined as:
class Applicative m => Monad (m :: * -> *) where
(>>=) :: m a -> (a -> m b) -> m b
(>>) :: m a -> m b -> m b
return :: a -> m a
fail :: String -> m a
Copy the code
Haskell’s type class is similar to the protocol in Swift. We can see that the first line declares Monad, and m can be seen as the type that needs to implement Monad. Here are some functions that need to be implemented. In fact, m above is actually a type constructor, which is of type (* -> *). We can directly think of it as a Swift generic with one generic parameter. Accordingly, if it is a Haskell type constructor of type (* -> * -> *), it is a Swift generic with two generic parameters. The type constructor for the (*) type is actually a concrete type. Now, the problem is that with Haskell, we can have a nonconcrete type (a type constructor with one or more type parameters) implement some type classes, but with Swift, to implement a protocol, we must provide a concrete type. So Monad cannot be implemented by protocol in Swift.
protocol Monad {
associatedtype MT
func bind(_ f: (MT) -> Self) -> Self
}
Copy the code
Like the Monad protocol defined above, the generic parameter is MT. The bind function of Monad is problematic because it accepts a function that returns Self and returns Self. Self refers to the type of the protocol, and its generic parameter remains unchanged, which does not satisfy Monad’s requirements. To achieve Monad in Swift, only by ourselves to ensure that each Monad implementation class in the implementation of the specified Monad function.
Lambda nesting in Monad cannot be solved gracefully in Swift
Haskell’s DO syntax avoids multiple lambdas nesting, which makes Monad’s syntax more elegant:
main = do
first <- getLine
second <- getLine
putStr $ first ++ second
Copy the code
For Swift, it might be a bit sad to write about lambda nesting when using Monad, as in the Optional Monad above:
let one: Int? = 4
let two: Int? = nil
let three: Int? = 7
let result1 = one >>- { o in two >>- { t in o + t } }
let result2 = one >>- { o in two >>- { t in three >>- { th in o * t * th } } }
Copy the code
If Swift supports the DO syntax (not the do syntax for exception handling), this would be much simpler:
let result1 = do {
o <- one
t <- two
th <- three
return o * t * th
}
Copy the code
The above syntax is purely imaginary.
So in general you wouldn’t want to use Swift to implement monads that require multiple nested lambdas.
Either Monad
In the previous article on functional programming, Result Monad was mentioned, which indicates that there may be success or failure of an operation. If the operation succeeds, the Result value can be obtained, and if the operation fails, the cause of failure (error message) can be obtained. You can also do this using Either Monad.
enum Either<L, R> {
case left(L)
case right(R)
}
extension Either {
static func ret(_ data: R) -> Either<L, R> {
return .right(data)
}
func bind<O>(_ f: (R) -> Either<L, O>) -> Either<L, O> {
switch self {
case .left(let l):
return .left(l)
case .right(let r):
return f(r)
}
}
}
func >>- <L, R, O> (lhs: Either<L, R>, f: (R) -> Either<L, O>) -> Either<L, O> {
return lhs.bind(f)
}
Copy the code
Either is an enumerated type and takes two generic parameters that indicate that the data is Either in left or right at some state. Since Monad requires that the implemented type have a generic parameter, and since the data types contained in the context may be converted when bind is performed, the data types contained in the context do not change, we use the generic parameter L for the data types contained in the context, and R for the type of the value.
What are the data types contained in the context, and what are the types of values? Result Monad has a data generic that represents the data types in it. This type of data is returned if the operation succeeds, or an Error type is returned if the operation fails. We can think of the Error type as the data type contained in the context. It is immutable over a series of operations because Result is required to record failures. If an operation suddenly becomes an Int, the entire context loses its original meaning. So, if Either monad works as Result monad, we must fix a context-contained type that does not change over a series of operations, while the value type can change. The signature of the >>- operator clearly shows this type constraint: the left side of Either received and returned Either is L, and the right side of Either can be changed as the function is received (R -> O).
Use Either monad as a Result monad to refine the type of error message. In Result Monad, Error messages are carried by instances of Error type, but we use Either Monad to draw up different Error types according to our needs. If we have two modules, with module one indicating the error type as ErrorOne and module two as ErrorTwo, we can define two Either monads for each module:
typealias EitherOne<T> = Either<ErrorOne, T>
typealias EitherTwo<T> = Either<ErrorTwo, T>
Copy the code
As you can see from the code above, Swift can also perform the same currification operation on type constructors (generic classes) as Haskell does. This means that instead of filling out all of the generic parameters it needs to implement a generic, we can just fill in a few of them.
Writer monad
To introduce Writer Monad, I first throw out a requirement:
- Do a series of tasks in a row
- After completing each task, record relevant records (such as logging)
- When all tasks are finally completed, the final data and overall record files are obtained
For this requirement, the traditional approach might be to keep a global archive record that we modify in response to each task completion until all tasks are complete.
Writer Monad provides a more elegant solution to this situation. Its Context holds archive records. Every time we perform an operation on the data, we do not need to separate our efforts from the organization and modification of the file, we only need to focus on the operation of the data.
Monoid
Before going further into Writer Monad, we first mention a concept: Monoid (unit semigroup). As a mathematical concept, Monoid has some characteristics, but we only use it to complete some logic in engineering projects, so we do not discuss its mathematical concept in depth. Here is a brief mention of the features it needs to satisfy:
For a set, there is a binary operation:
- You take two elements of the set, you still get the elements of the set (closure)
- This operation is associative
- There is an element (the identity element), and when you apply it to another element using a binary operation, the result is still that other element.
For example, for an integer type, it has an addition operation that takes two integers and adds them together to get an integer, and we all know that addition is associative. For the integer 0, any number added to it is equal to the original number, so 0 is the identity element of the identity semigroup.
We can define the protocol for Monoid in Swift:
// Unit semigroup protocol Monoid {typeAlias T = Self static var mEmpty: T {get} func mAppend(_ next: T) -> T}Copy the code
Where, mEmpty represents the identity element of the unit semigroup, and mAppend represents the corresponding binary operation.
The above example can be implemented in Swift like this:
struct Sum {
let num: Int
}
extension Sum: Monoid {
static var mEmpty: Sum {
return Sum(num: 0)
}
func mAppend(_ next: Sum) -> Sum {
return Sum(num: num + next.num)
}
}
Copy the code
We use Sum to represent the unit semigroup in the example above. Why not just use Int to implement Monoid instead of wrapping it one more layer? Because Int can also implement other unit semigroups, such as:
struct Product {
let num: Int
}
extension Product: Monoid {
static var mEmpty: Product {
return Product(num: 1)
}
func mAppend(_ next: Product) -> Product {
return Product(num: num * next.num)
}
}
Copy the code
The binary operation of the unit semigroup above is the multiplication operation, so the identity element is 1,1 multiplied by anything is the original number.
Like Boolean types, two monoids can be derived:
struct All {
let bool: Bool
}
extension All: Monoid {
static var mEmpty: All {
return All(bool: true)
}
func mAppend(_ next: All) -> All {
return All(bool: bool && next.bool)
}
}
struct `Any` {
let bool: Bool
}
extension `Any`: Monoid {
static var mEmpty: `Any` {
return `Any`(bool: true)
}
func mAppend(_ next: `Any`) -> `Any` {
return `Any`(bool: bool || next.bool)
}
}
Copy the code
When we want to determine whether a set of Booleans are All true or whether true exists, we can use the All or Any monoid property:
let values = [true, false, true, false]
let result1 = values.map(`Any`.init)
.reduce(`Any`.mEmpty) { $0.mAppend($1) }.bool // true
let result2 = values.map(All.init)
.reduce(All.mEmpty) { $0.mAppend($1) }.bool // false
Copy the code
Realize the Writer monad
Let’s take a closer look at Writer Monad and first give its implementation in Swift:
// Writer
struct Writer<W, T> where W: Monoid {
let data: T
let record: W
}
extension Writer{
static func ret(_ data: T) -> Writer<W, T> {
return Writer(data: data, record: W.mEmpty)
}
func bind<O>(_ f: (T) -> Writer<W, O>) -> Writer<W, O> {
let newM = f(data)
let newData = newM.data
let newW = newM.record
return Writer<W, O>(data: newData, record: record.mAppend(newW))
}
}
func >>- <L, R, W>(lhs: Writer<W, L>, rhs: (L) -> Writer<W, R>) -> Writer<W, R> where W: Monoid {
return lhs.bind(rhs)
}
Copy the code
Analysis of the implementation of the source code:
- The generic parameter
M
The requirement is oneMonoid
, which represents the type of files recorded for a series of operations; The generic parameterT
Means to be wrapped inWriter monad
The type of data in the context. -
ret
Method action traceHaskell
In thereturn
Function to wrap a value around a MonadMinimum context
. forWriter monad
, we are inret
The function returns oneWriter
, where the data is the parameter passed in, and the record file is the unit of the specified Monoid, so that a data can be wrapped inWriter monad
The minimum context of. -
bind
In the implementation, we can see that the inside will automatically put twoWriter monad
To recordmAppend
Action to return a file containing new data and new recordsWriter monad
. In front ofMonad
According to the concept:Monad
thebind
The manipulation lets us focus on the manipulation of the data. We don’t have to worry about the context processing, which is automatic. So forWriter monad
.bind
The operation automatically helps us to recordmAppend
In addition, we do not need to spend other energy on the operation of the record. - To make the code more elegant, I defined the operators
>>-
And it’s on theHaskell
Looks like> > =
.
Demo
Let’s do a little Demo with Writer Monad. As introduced in front of the demand, here I’m going to do about a Double a series of simple operations, including addition, subtraction, multiplication, division, after each operation, we need to use the string to record the process of operation, such as x * 3 will record into 3 times, and the record before and the new operation created record to merge, finally, after the completion of a series of operations We’ll get the result of the operation and a record of the operation.
First, we’ll let String implement Monoid:
extension String: Monoid {
static var mEmpty: String {
return ""
}
func mAppend(_ next: String) -> String {
return self + next
}
}
Copy the code
The identity semigroup for String has a binary operation of +, which means that two strings are concatenated, so its identity is an empty String.
Writer monad alias for Double Writer monad alias for Double Writer monad alias for Double Writer monad
typealias MWriter = Writer<String, Double>
Copy the code
Then define the addition, subtraction, multiplication, and division operations:
func add(_ num: Double) -> (Double) -> MWriter { return { MWriter(data: $0 + num, record: Subtract (_ num: Double) -> (Double) -> MWriter {return {MWriter(data: $0 -num, record: Multiply (_ num: Double) -> (Double) -> MWriter {return {MWriter(data: $0 * num, record: "Multiply \(num) ")}} func divide(_ num: Double) -> (Double) -> MWriter {return {MWriter(data: $0 / num, record: "Divide by \(num) ")}}Copy the code
Note that these functions are higher-order functions. If their parameters and return values are (a) -> (b) -> c, they perform the operation b X a (X is addition, subtraction, multiplication, and division) and return c. After each operation, information about the operation is recorded, such as adding X and dividing by X.
Now let’s test it out:
Let resultW = mwriter.ret (1) >> -add (3) >> -multiply (5) >> -subtract (6) >> -divide (7) Let resultD = resultw.data // 2.0 Let resultRecord = resultw. record // "Add 3.0 times 5.0 minus 6.0 divided by 7.0"Copy the code
As you can see, we have the result 2.0 after many consecutive operations, and the auto-concatenated record “plus 3.0 times 5.0 minus 6.0 divided by 7.0”.
If the score is greater than or equal to 60, the student can pass the exam. Now we need to count the scores of the students in a class and determine whether the whole class has passed or whether at least one student has passed the exam. We can create score Writer monad using All monoid and Any monoid as described above:
typealias ScoreWriter = Writer<All, Int> func append(_ score: Int) -> (Int) -> ScoreWriter { return { ScoreWriter(data: $0 + score, record: All(bool: score >= 60)) } } let allScores = [45, 60, 98, 77, 65, 59, 60, 86, 93] let result = allScores.reduce(ScoreWriter.ret(0)) { $0 >>- append($1) } let resultBool = result.record.bool // false let resultScore = result.data // 643Copy the code
Append is a higher-order function, and we can think of it as a currified form of a function that takes two arguments. We determine whether the first argument passed meets the eligibility requirements and add the two arguments to create a ScoreWriter. In the ScoreWriter monad, I set the record type to All, so the Boolean type returned indicates whether the entire class passed. There is obviously less than 60 in the incoming data, so the final Boolean result is false.
If you change All to Any, the final Boolean is true, indicating that at least one student in the class passed:
Typealias ScoreWriter = Writer< 'Any', Int> func append(_ score: Int) -> (Int) -> ScoreWriter { return { ScoreWriter(data: $0 + score, record: `Any`(bool: score >= 60)) } } let allScores = [45, 60, 98, 77, 65, 59, 60, 86, 93] let result = allScores.reduce(ScoreWriter.ret(0)) { $0 >>- append($1) } let resultBool = result.record.bool // trueCopy the code
State Monad
For Swift, since it is not a purely functional programming language, there is no immutable data, and we can always create variables using VAR. Haskell, by virtue of its immutable nature, requires a different approach for some state-related operations. State MonAD can be used to address this requirement. But in Swift, if you don’t like to define variables all the time, or if you have variables mixed up, you can use this method.
State Monad can play a powerful role in Haskell’s DO syntax, but to do so in Swift we need to write multiple lambda nesting (closure nesting), which is cumbersome, observable, and unappealing, against the brevity of functional programming. So, we’ll just explore the case for a >>- (bind) chaining call to State monad. State Monad is a bit of a challenge, and it’s probably rarely needed in everyday engineering projects, but it’s a great way to improve your familiarity with functional programming. The following is a cursory overview of Stata Monad, but more information about State Monad is available if you are interested.
First let’s implement State Monad:
struct State<S, T> {
let f: (S) -> (T, S)
}
extension State {
static func ret(_ data: T) -> State<S, T> {
return State { s in (data, s) }
}
func bind<O>(_ function: @escaping (T) -> State<S, O>) -> State<S, O> {
let funct = f
return State<S, O> { s in
let (oldData, oldState) = funct(s)
return function(oldData).f(oldState)
}
}
}
func >>- <S, T, O>(lhs: State<S, T>, f: @escaping (T) -> State<S, O>) -> State<S, O> {
return lhs.bind(f)
}
Copy the code
If an operation requires state, we don’t want to create a new variable in scope to record some temporary state that changes as the operation progresses. We can return the new state after each operation so that we can use the new state for the next operation, and so on. State has a member that is of type a function, which can be thought of as an operation that takes a State as an argument and returns the resulting data and a new tuple of states. State Monad’s RET function accepts a value of any type and returns State itself. Since the RET function wraps the data in the minimal context of Monad, the member functions in State do nothing with the data and State at this point. Bind automatically passes the new state from the previous operation to the next operation, so we don’t have to worry about passing the state when we call bind.
Here is a small example of using State Monad. This example may be a bit far-fetched, but I may revise this part later if I think of something better.
Now suppose that the server provides an API to get the user’s name from the user’s ID, and we want to get the names of n users with consecutive ids wrapped in an array. Let’s start by simulating the server database data and API functions:
struct Person { let id: Int let name: String } let data = ["Hello", "My", "Name", "Is", "Tangent", "Haha"].enumerated().map(Person.init) func fetchNameWith(id: Int) -> String? { return data.filter { $0.id == id }.first? .name }Copy the code
The server provides the fetchNameWith method to get the name of the specified user by ID, or return nil if no user with this ID exists.
We define a State Monad type to solve this problem and create a request function:
typealias MState = State<Int, [String]>
func fetch(names: [String]) -> MState {
return MState { id in
guard let name = fetchNameWith(id: id) else { return (names, id) }
return (names + [name], id + 1)
}
}
Copy the code
The fetch function is of type ([String]) -> MState, which takes an array of all the names of the previously requested users. The returned MState does two things:
- Call the server API, get the specified user name, and add the user name to the array
- Increment the original user ID by one so that you can get the name of the next user in later operations
There’s a bound case here, where the server returns nil when it can’t find the user, and our operand doesn’t do anything, returns the original data, indicating that no matter how much we continue to call the requestor, the result won’t change.
Here’s a test:
let fetchFunc = MState.ret([]) >>- fetch >>- fetch >>- fetch >>- fetch
let namesAndNextID = fetchFunc.f(1)
let names = namesAndNextID.0 // ["My", "Name", "Is", "Tangent"]
let nextID = namesAndNextID.1 // 5
Copy the code
We started by wrapping an empty array into the State Monad’s minimum context. After four requests, bind automatically did all the State operations and returned the result State Monad. The operation function in the State Monad has merged all the previous operations. So we can call this operation function directly to get the data we want.
conclusion
This paper summarizes the concept of Monad, discusses some defects of implementing Monad in Swift, and introduces Either Monad, Writer Monad and State Monad to try to implement them in Swift. While we typically use the object-oriented programming paradigm in normal development, having the flexibility to incorporate functional programming concepts and ideas into your code can work wonders. But the pit is a little deep 😐