📜 ⬆️ ⬇️

Functional Swift is just

image


In articles about functional programming, they write a lot about how the FP approach improves development: it becomes easy to write, read, break into threads and test, it is impossible to build a bad architecture. , and hair becomes soft and silky .


Lack of one - a high threshold of entry. Trying to understand the FP, I ran into a huge amount of theory, functors, monads, category theory and algebraic data types. How to use the FP in practice was not clear. In addition, examples were given in languages ​​I am not familiar with - Haskel and rock.


Then I decided to understand the OP from the very beginning. I figured out and told in codefest that the OP is actually just that we already use it in Swift and can use it even more efficiently.


Functional programming: pure functions and lack of states


Determining what it means to write in a particular paradigm is not an easy task. For decades, paradigms have been formed by people with different visions, embodied in languages ​​with different approaches, and overgrown with tools. These tools and approaches are considered an integral part of paradigms, but in fact they are not.


For example, object-oriented programming is considered to be on three pillars — inheritance, encapsulation, and polymorphism. But encapsulation and polymorphism are implemented on functions with the same ease as on objects. Or closures - they were born in pure functional languages, but so long ago they migrated into industrial languages ​​that they no longer associated with FP. Monads also make their way into industrial languages, but so far they have not lost their belonging to the conditional haskel in people's minds.


As a result, it turns out that it is impossible to clearly define what this particular paradigm is. I once again ran into this at codefest 2019, where all the experts of the OP, speaking of the functional paradigm, called different things.


I personally liked the wiki definition:


“Functional programming is a section of discrete mathematics and a programming paradigm in which the calculation process is interpreted as calculating the values ​​of functions in the mathematical understanding of the latter (as opposed to functions as subroutines in procedural programming).”


What is a mathematical function? This is a function whose result depends only on the data to which it is applied.


An example of a mathematical function in four lines of code looks like this:


func summ(a: Int, b: Int) -> Int { return a + b } let x = summ(a: 2, b: 3) 

Calling summ with input arguments 2 and 3, we get 5. This result is unchanged. Change the program, flow, place of performance - the result will remain the same.


And a non-mathematical function is when a global variable is declared somewhere.


 var z = 5 

The sum function now adds the input arguments and the value of z.


 func summ(a: Int, b: Int) -> Int { return a + b + z } let x = summ(a: 2, b: 3) 

Added dependence on the global state. Now you can not uniquely predict the value of x. It will constantly change depending on when the function was called. Call the function 10 times in a row, and each time we can get a different result.


Another variant of the non-mathematical function:


 func summ(a: Int, b: Int) -> Int { z = b - a return a + b } 

In addition to returning the sum of the input arguments, the function changes the global variable z. This feature has a side effect.


In functional programming, there is a special term for mathematical functions — pure functions. A pure function is a function that for the same set of input values ​​returns the same result and does not have side effects.


Pure functions are the cornerstone of the OP; everything else is secondary. It is assumed that, following this paradigm, we use only them. And if there is no way to work with global or changeable states, then they will not be in the application.


Classes and structures in the functional paradigm


Initially, I thought that the OP is only about functions, and classes and structures are used only in OOP. But it turned out that classes also fit into the concept of OP. Only they must be, let's say, "clean."


A “pure” class is a class, all methods of which are pure functions, and properties are immutable. (This is an informal term, coined during the preparation of the report).


Take a look at this class:


 class User { let name: String let surname: String let email: String func getFullname() -> String { return name + " " + surname } } 

It can be considered as data encapsulation ...


 class User { let name: String let surname: String let email: String } 

and functions to work with them.


 func getFullname() -> String { return name + " " + surname } 

From the point of view of the FP, using the User class is no different from working with primitives and functions.


Let's announce the value - user Vanya.


 let ivan = User( name: "", surname: "", email: "ivanov@example.com" ) 

Apply the getFullname function to it.


 let fullName = ivan.getFullname() 

As a result, we get a new value - the full name of the user. Since you cannot change the parameters of the ivan property, the result of the getFullname call is unchanged.


Of course, an attentive reader can say: “Wait, the getFullname method returns a result based on its global values ​​— the properties of the class, not the arguments.” But in reality, a method is simply a function to which an object is passed as an argument.


Swift even supports this entry explicitly:


 let fullName = User.getFullname(ivan) 

If we need to change some value of the object, for example email, we will have to create a new object. This can be done by an appropriate method.


 class User { let name: String let surname: String let email: String func change(email: String) -> User { return User(name: name, surname: surname, email: email) } } let newIvan = ivan.change(email: "god@example.com") 

Functional Attributes in Swift


I already wrote that many of the tools, implementations and approaches that are considered part of a particular paradigm can in fact be used in other paradigms. For example, monads, algebraic data types, automatic type inference, strong typification, dependent types, program validity at compile time are considered to be part of the OP. But many of these tools we can find in Swift.


Strong typing and type inference is part of Swift. They do not need to be understood or entered into the project, we just have them.


There are no dependent types, although I would not refuse to check the string by the compiler that it has an email, an array, that it is not empty, it has a dictionary, that it contains the key “apple”. By the way, Haskell has no dependent types either.


Algebraic data types are available, and this is a cool, but difficult to understand mathematical thing. The beauty is that it does not need to be understood mathematically to use. For example, Int, enum, Optional, Hashable are algebraic types. And if Int is in many languages, and Protocol is in Objective-C, then enum with related values, protocols with default implementation and associative types are far from being everywhere.


Validation at compile time is often mentioned when talking about languages ​​like rust or haskell. It is understood that the language is so expressive that it allows you to describe all boundary cases so that they are checked by the compiler. So, if the program is compiled, then it will definitely work. Nobody argues that it may contain errors in logic, because you have incorrectly filtered the data to show to the user. But it will not fall, because you did not receive data from the database, the server returned to you the wrong answer, or you entered the date of your birth in a string, not a number.


I can not say that compiling swift code can catch all the bugs: for example, a memory leak is easy to prevent. But strict typing and Optional protect well against a lot of stupid mistakes. The main thing - to limit the forced extraction.


Monads: not part of the OP paradigm, but a tool (optional)


Quite often, OP and monads are used in the same application. At one time I even thought that monads are functional programming. When I understood them (but this is not accurate), I made several conclusions:



Swift already has two standard monads, Optional and Result. Both are needed to deal with side effects. Optional protects against possible nil. Result - from various exceptional situations.


Consider the example, brought to the point of absurdity. Suppose we have functions that return an integer from the database and from the server. The second may return nil, but we use implicit extraction to get the behavior of the Objective-C tenses.


 func getIntFromDB() -> Int func getIntFromServer() -> Int! 

We continue to ignore Optional and implement a function to sum these numbers.


 func summInts() -> Int! { let intFromDB = getIntFromDB() let intFromServer = getIntFromServer()! let summ = intFromDB + intFromServer return summ } 

Call the final function and use the result.


 let result = summInts() print(result) 

Does this example work? Well, it will definitely compile, but if we get a crash at runtime or not, nobody knows. This code is good, it perfectly shows our intentions (we need the sum of some two numbers) and it does not contain anything superfluous. But he is dangerous. Therefore, only juniors and self-confident people write like this.


Let's change the example by making it safe.


 func getIntFromDB() -> Int func getIntFromServer() -> Int? func summInts() -> Int? { let intFromDB = getIntFromDB() let intFromServer = getIntFromServer() if let intFromServer = intFromServer { let summ = intFromDB + intFromServer return summ } else { return nil } } if let result = summInts() { print(result) } 

This code is good, it is safe. Using explicit extraction, we defended ourselves against possible nil. But it became cumbersome, and among the safe checks it is already difficult to discern our intention. We still need the sum of some two numbers, not a security check.


In this case, Optional has a map method, inherited from the Maybe type from Haskell. Apply it, and the example will change.


 func getIntFromDB() -> Int func getIntFromServer() -> Int? func summInts() -> Int? { let intFromDB = getIntFromDB() let intFromServer = getIntFromServer() return intFromServer.map { x in x + intFromDB } } if let result = summInts() { print(result) } 

Or even more compact.


 func getIntFromDB() -> Int func getintFromServer() -> Int? func summInts() -> Int? { return getintFromServer().map { $0 + getIntFromDB() } } if let result = summInts() { print(result) } 

We used map to convert intFromServer to the desired result without extraction.


We got rid of the check inside summInts, but left it at the top level. This is intentional, since at the end of the chain of calculations we must choose a way to handle the absence of a result.


Take out


 if let result = summInts() { print(result) } 

Use default


 print(result ?? 0) 

Or display a warning if the data is not received.


 if let result = summInts() { print(result) } else { print("") } 

Now the code in the example does not contain too much, as in the first example, and is safe, as in the second.


But the map does not always work as it should.


 let a: String? = "7" let b = a.map { Int($0) } type(of: b)//Optional<Optional<Int>> 

If in map to transfer function which result optional, we will receive double Optional. But we do not need double protection against nil. One is enough. The flatMap method allows to solve the problem, it is an analogue of map with one difference, it unfolds the dolls.


 let a: String? = "7" let b = a.flatMap { Int($0) } type(of: b)//Optional<Int>. 

Another example where map and flatMap is not very convenient to use.


 let a: Int? = 3 let b: Int? = 7 let c = a.map { $0 + b! } 

What if the function takes two arguments and they are both optional? Of course, the FP has a solution — it is an applicative functor and currying. But these tools look rather clumsy without the use of special operators that are not in our language, and writing custom operators is considered bad form. Therefore, consider a more intuitive way: write a special function.


 @discardableResult func perform<Result, U, Z>( _ transform: (U, Z) throws -> Result, _ optional1: U?, _ optional2: Z?) rethrows -> Result? { guard let optional1 = optional1, let optional2 = optional2 else { return nil } return try transform(optional1, optional2) } 

It takes as arguments two optional values ​​and a function with two arguments. If both options have values, the function is applied to them.
Now we can work with several optionals without expanding them.


 let a: Int? = 3 let b: Int? = 7 let result = perform(+, a, b) 

The second monad, Result, also has methods map and flatMap. So, you can work with it in the same way.


 func getIntFromDB() -> Int func getIntFromServer() -> Result<Int, ServerError> func summInts() -> Result<Int, ServerError> { let intFromDB = getIntFromDB() let intFromServer = getIntFromServer() return intFromServer.map { x in x + intFromDB } } if case .success(let result) = summInts() { print(result) } 

Actually, this makes the monads related to each other - the ability to work with the value inside the container without removing it. In my opinion, this makes the code more concise. But if you don’t like it, just use explicit extracts, this is not contrary to the OP paradigm.


Example: reducing the number of "dirty" functions


Unfortunately, in real programs everywhere there are global states and side effects - network queries, data sources, UI. And only pure functions can not do. But this does not mean that the OP is completely unavailable for us: we can try to reduce the number of dirty functions, of which there are usually a lot.


Consider a small example that is close to production development. Construct a UI, specifically an input form. The form has some limitations:


1) Login not shorter than 3 characters
2) Password not shorter than 6 characters
3) The "Login" button is active if both fields are valid
4) The color of the field frame reflects its condition, black is valid, red is not valid


The code describing these restrictions might look like this:


Handling any user input


 @IBAction func textFieldTextDidChange() { // 1.     // 2.   guard let login = loginView.text, let password = passwordView.text else { // 3. - loginButton.isEnabled = false return } let loginIsValid = login.count > constants.loginMinLenght if loginIsValid { // 4. - loginView.layer.borderColor = constants.normalColor } let passwordIsValid = password.count > constants.passwordMinLenght if passwordIsValid { // 5. - passwordView.layer.borderColor = constants.normalColor } // 6. - loginButton.isEnabled = loginIsValid && passwordIsValid } 

Processing login completion:


 @IBAction func loginDidEndEdit() { let color: CGColor // 1.     // 2.   if let login = loginView.text, login.count > 3 { color = constants.normalColor } else { color = constants.errorColor } // 3.   loginView.layer.borderColor = color } 

Processing password entry completion:


 @IBAction func passwordDidEndEdit() { let color: CGColor // 1.     // 2.   if let password = passwordView.text, password.count > 6 { color = constants.normalColor } else { color = constants.errorColor } // 3. - passwordView.layer.borderColor = color } 

Pressing the login button:


 @IBAction private func loginPressed() { // 1.     // 2.   guard let login = loginView.text, let password = passwordView.text else { return } auth(login: login, password: password) { [weak self] user, error in if let user = user { /*  */ } else if error is AuthError { guard let `self` = self else { return } // 3. - self.passwordView.layer.borderColor = self.constants.errorColor // 4. - self.loginView.layer.borderColor = self.constants.errorColor } else { /*   */ } } } 

Perhaps this code is not the best, but in general it is not bad and works. True, he has a number of problems:



The main problem is that you can not just take and say what is happening with our screen. Looking at one method, we see what it does with the global state, but we do not know who, where and when still touches the state. As a result, in order to understand what is happening, it is necessary to find all points of work with the views and understand in what order what effects occur. Keeping it all in your head is very difficult.


If the process of state change is linear, you can study it step by step, which will reduce the cognitive load on the programmer.


Let's try to change the example, making it more functional.


First, let's define a model that describes the current state of the screen. This will let you know exactly what information is needed for work.


 struct LoginOutputModel { let login: String let password: String var loginIsValid: Bool { return login.count > 3 } var passwordIsValid: Bool { return password.count > 6 } var isValid: Bool { return loginIsValid && passwordIsValid } } 

Model describing changes applied to the screen. It is necessary to know exactly what we will change.


 struct LoginInputModel { let loginBorderColor: CGColor? let passwordBorderColor: CGColor? let loginButtonEnable: Bool? let popupErrorMessage: String? } 

Events that may lead to a new screen state. So we will know exactly what actions change the screen.


 enum Event { case textFieldTextDidChange case loginDidEndEdit case passwordDidEndEdit case loginPressed case authFailure(Error) } 

We now describe the main change method. This pure function based on the current state event collects a new screen state.


 func makeInputModel( event: Event, outputModel: LoginOutputModel?) -> LoginInputModel { switch event { case .textFieldTextDidChange: let mapValidToColor: (Bool) -> CGColor? = { $0 ? normalColor : nil } return LoginInputModel( loginBorderColor: outputModel .map { $0.loginIsValid } .flatMap(mapValidToColor), passwordBorderColor: outputModel .map { $0.passwordIsValid } .flatMap(mapValidToColor), loginButtonEnable: outputModel?.passwordIsValid ) case .loginDidEndEdit: return LoginInputModel(/**/) case .passwordDidEndEdit: return LoginInputModel(/**/) case .loginPressed: return LoginInputModel(/**/) case .authFailure(let error) where error is AuthError: return LoginInputModel(/**/) case .authFailure: return LoginInputModel(/**/) } } 

The most important thing is that this method is the only one who is allowed to engage in the construction of a new state - and it is clean. It can be studied step by step. See how events transform the screen from point A to point B. If something breaks, then the problem is exactly here. And it is easy to test.


Add an auxiliary property to get the current state, this is the only method that depends on the global state.


 var outputModel: LoginOutputModel? { return perform(LoginOutputModel.init, loginView.text, passwordView.text) } 

Add another "dirty" method to create side effects of changing the screen.


 func updateView(_ event: Event) { let inputModel = makeInputModel(event: event, outputModel: outputModel) if let color = inputModel.loginBorderColor { loginView.layer.borderColor = color } if let color = inputModel.passwordBorderColor { passwordView.layer.borderColor = color } if let isEnable = inputModel.loginButtonEnable { loginButton.isEnabled = isEnable } if let error = inputModel.popupErrorMessage { showPopup(error) } } 

Although the updateView method is not clean, this is the only place where screen properties change. The first and last item in the chain of calculations. And if something went wrong, this is where the breakpoint will stand.


It remains only to start the conversion in the right places.


 @IBAction func textFieldTextDidChange() { updateView(.textFieldTextDidChange) } @IBAction func loginDidEndEdit() { updateView(.loginDidEndEdit) } @IBAction func passwordDidEndEdit() { updateView(.passwordDidEndEdit) } 

The loginPressed method came out a bit unique.


 @IBAction private func loginPressed() { updateView(.loginPressed) let completion: (Result<User, Error>) -> Void = { [weak self] result in switch result { case .success(let user): /*  */ case .failure(let error): self?.updateView(.authFailure(error)) } } outputModel.map { auth(login: $0.login, password: $0.password, completion: completion) } } 

The fact is that clicking on the "Login" button launches two chains of calculations, which is not prohibited.


Conclusion


Before I started learning FP, I placed a strong emphasis on programming paradigms. For me it was important that the code follow the OOP, I did not like static functions or stateless objects, did not write global functions.


Now it seems to me that all those things that I considered part of one or another paradigm are rather conditional. The main thing is a clean, clear code. To achieve this goal, you can use everything that is possible: pure functions, classes, monads, inheritance, composition, type inference. They all get along well together and make the code better - just apply them to the site.


What else to read on the topic


Definition of functional programming from wikipedia
Haskell Beginner Book
Explanation of functors, monads, and applicative functors on fingers
A book about Has (i) Practices for Using Maybe (Optional)
The book on the functional nature of Swift
Determining Algebraic Data Types from a Wiki
Article about algebraic data types
Another article about algebraic data types
Report on functional Swift programming in Yandex
Implementing the Prelude (Haskell) Standard Library on Swift
Library with functional tools on Swift
Another library
And another one


')

Source: https://habr.com/ru/post/455359/


All Articles