قالب وردپرس درنا توس
Home / IOS Development / An introduction to functional programming in Swift

An introduction to functional programming in Swift



Update Note : Warren Burton updated this tutorial to Swift 4.2. Joe Howard wrote the original tutorial and Niv Yahel did a previous update.

Swift's major entry into the world of programming at WWDC in 2014 was much more than just an introduction to a new language. It facilitates new approaches to software development for the iOS and macOS platforms.

This tutorial focuses on one of these approaches: Functional Programming or FP for Short. You get an introduction to a wide range of ideas and techniques used in FP.

When you go through this training, you will be working on a playground. You can find the finished playground using Download the materials button at the top or bottom of this page.

Getting Started

Create a new empty playground in Xcode so you can follow the tutorial by selecting File ytt New lass Playground … .

 create new playground

Set up your playground so you can see the performance panel and console by dragging the slots.

 configure playground

Delete everything from the playground and add this line:

  Import Foundation

You begin by reviewing some basic theory to warm up your brain.

Imperative Programming Style

When you first learned to code, you probably learned imperative style. How does imperative style work?

Add the following code to your playground:

  was thing = 3
// some stuff
things = 4

That code is normal and reasonable. First, create a variable called thing that corresponds to 3, and you decide things to be 4 later.

It is imperative in a nutshell. You create a variable with some data and you tell that the variable is something else.

Function Programming Concepts

In this section you get an introduction to some important terms in FP. Many papers discussing FP solely immutable state and lack side effects as the most important aspects of FP, so you start there.

Impermeability and Side Effects

] No matter what programming language you learned first, one of the first concepts you probably learned was that a variable represents data or state. If you go a little to think about the idea, variables may seem quite strange.

The term "variable" means a quantity that varies when the program is running. Think of the quantity thing from a mathematical perspective, you have introduced time as an important parameter in how the software behaves. By changing the variable, you create mutable state .

For a demonstration, add this code to your playground:

  func superHero () {
print ("I'm batman")
things = 5
}

print ("original state =")
superhero ()
print ("mutated state = (thing)")

Holy Mysterious Changes! Why are things now 5? That change is called a side effect . The function superHero () changes a variable that it did not even define itself.

For itself or in a single system, mutant state is not necessarily a problem. Problems arise when connecting many objects together, for example, in a large object-oriented system. Mutable condition can produce headaches by making it difficult to understand the value of a variable and how that value changes over time.

For example, when you write code for a multi-threaded system, if two or more threads access the same variable simultaneously they can modify or access it properly. This leads to unexpected behavior. The unexpected entry includes running conditions dead locks and many other issues.

Imagine if you could write code where the state never mutated. A whole host of problems that arise in simultaneous systems will disappear. Systems that work like this have an unchangeable state which means that the state is not allowed to change during a program.

The main advantage of using undeveloped data is that the device's devices that use it are free of side effects. The features of your code do not change the elements beyond themselves, and no spooky effects appear when feature calls occur. Your program works predictably because without any side effects you can easily reproduce the intended effects.

This training covers high-level FP, so it is useful to consider the concepts in a real world situation. In this case, you can imagine building an app for an amusement park, and that the park's back-end server provides driving data via a REST API.

Creating a model amusement park

Set up the data structure by adding this code to your playground:

  enum RideCategory: String, CustomStringConvertible {
scissors family
case of children
case thrill
so scary
case relaxing
seawater

was description: String {
return rawValue
}
}

Typical Minutes = Double
struct Ride: CustomStringConvertible {
let the name: String
la categories: sets 
leave waiting time: minutes

was description: String {
return "Ride -" (name) ", wait: (wait) minutes," +
"categories: (categories) n"
}
}

Then make some data using this model:

  let parkRides = [
  Ride(name: "Raging Rapids",
       categories: [.family, .thrill, .water],
waiting time: 45.0),
Ride (name: "Crazy Funhouse", categories: [.family] waiting time: 10.0),
Ride (name: "Spinning Tea Cups", categories: [.kids] waiting time: 15.0),
Ride (name: "Spooky Hollow", categories: [.scary] waiting time: 30.0),
Ride (name: "Thunder Coaster",
categories: [.family, .thrill],
waiting time: 60.0),
Ride (name: "Grand Carousel", categories: [.family, .kids] waiting time: 15.0),
Ride (name: "Bumper boats", categories: [.family, .water] waiting time: 25.0),
Ride (name: "Mountain Railroad",
categories: [.family, .relaxing],
waiting time: 0.0)
]

Since you declare parkRides with la instead of was both the matrix and the content are immutable.

Try changing one of the elements of the group, via the following:

  parkRides [0] = Ride (name: "Functional programming",
categories: [.thrill] waiting time: 5.0)

It produces a compiler error, which is good. You want the Swift compiler to prevent you from changing the data.

Now remove these lines so you can continue with the tutorial.

Modularity

Modularity work is like playing with children's bricks. You have a box of simple bricks that you can use to build a large and complex system by joining them. Each wall has a single job. You want your code to have the same effect.

Suppose you need an alphabetical list of all the rides names. Start by making this necessary, meaning you are using mutable state. Add the following function to the bottom of the playground:

  func sortedNamesImp (of rides: [Ride]) -> [String] {

// 1
were sortedRides = rides
was key: Ride

// 2
for i in (0 .. <sortedRides.count) {
key = sortedRides [i]

// 3
for j in stride (from: i to: -1, by: -1) {
if key.name.localizedCompare (sortedRides [j] .name) == .orderedAscending {
sortedRides.remove (by: j + 1)
sortedRides.insert (key, by: j)
}
}
}

// 4
var sortedNames: [String] = []
for riding in sortedRides {
sortedNames.append (ride.name)
}

return sortedNames
}

la sortedNames1 = sortedNamesImp (by: parkRides)

Your code performs the following tasks:

  1. Create a variable to keep the sorted rides.
  2. Loop over all the rides that went into the function.
  3. Sort the rides using a Stake Sort
  4. Place the following test on the playground to verify that this function works as calculated:

      func testSortedNames (_ names: [String]) {
    let expected = ["Bumper Boats",
                      "Crazy Funhouse",
                      "Grand Carousel",
                      "Mountain Railroad",
                      "Raging Rapids",
                      "Spinning Tea Cups",
                      "Spooky Hollow",
                      "Thunder Coaster"]
    claim (name == expected)
    print ("erte test sorted name = PASS n-")
    }
    
    print (sortedNames1)
    testSortedNames (sortedNames1)
    

    You now know that if you change your sorting routine in the future (for example, doing it functional :]), you may discover which error occurs.

    From the perspective of a caller to sortedNamesImp (off :) it provides a list of rides, and then publishes the list of sorted names. Nothing outside sortedNamesImp (off :) is changing.

    You can prove this with another test. Add the following code to the end of the playground:

      was originalNames: [String] = []
    for walk in parkRides {
    originalNames.append (ride.name)
    }
    
    func testOriginalNameOrder (_ name: [String]) {
    let expected = ["Raging Rapids",
                      "Crazy Funhouse",
                      "Spinning Tea Cups",
                      "Spooky Hollow",
                      "Thunder Coaster",
                      "Grand Carousel",
                      "Bumper Boats",
                      "Mountain Railroad"]
    claim (name == expected)
    print ("original test original order order = PASS n-")
    }
    
    print (originalNames)
    testOriginalNameOrder (originalNames)
    

    In this test, you collect the names of the list of trips that you have passed as a parameter, and test what you order against the expected order.

    In the result area and console you see sorting tours inside sortedNamesImp (off :) did not affect the entry list. The modular feature you have created is semi-functional. The logic of sorting trips by name is a single, testable, modular and reusable function.

    The absolute code in sortedNamesImp (of :) is designed for a long and unmanageable function. The feature is hard to read and you can't easily find out what it does. In the next section you will learn techniques to simplify the code in a function such as sortedNamesImp (of :) even longer.

    Premium and higher order features

    In FP languages, features are first class citizens. You treat functions like other objects you can assign variables.

    Because of this, features can also accept other features such as parameters or return other functions. Functions that accept or return other functions are called higher order functions.

    In this section, you will work with three of the most common high-order functions in FP languages: filter map and reduce .

    Filter

    In Swift, filters (_ :) is a method of Collection types, for example Swift arrays. It accepts another function as a parameter. This second function accepts a single value from the array as input, checks if the value is and returns a Bool . filter (_ :) uses the input function of each element of the call array and returns another matrix. The output arm contains only those array elements whose parameter function returns true .

    Try this simple example:

      la apples = ["🍎", "🍏", "🍎", "🍏", "🍏"]
    la greenapples = apples.filter {$ 0 == "🍏"}
    print (greenapples)
    

    There are three green apples in the input list, so you see three green apples in the output.

    Think back to the list of actions performed by sortedNamesImp (of :) :

    1. Loops over all rides passed to the function.
    2. Sorts the rides by name.
    3. Gather the names of the sorted rides.

    Instead of thinking about this imperatively, think about it declaratively ie. Just by thinking what you will happen instead of how . Begin by creating a function that has a Ride object as an entry parameter to the function:

      func waitTimeIsShort (_ ride: Ride) -> Bool {
    return trip.waitTime <15.0
    }
    

    waitTimeIsShort (_ 🙂 accepts a Ride and returns true if the trip's waiting time is less than 15 minutes; otherwise it returns fake .

    Call filter (_ :) on your park rides and submit the new feature you just created:

      la shortWaitTimeRides = parkRides.filter (waitTimeIsShort)
    print ("rides with short waiting time: n (shortWaitTimeRides)")
    

    At the end of the playground you only see Crazy Funhouse and Mountain Railroad in the conversation of filter (_ :) s output, which is correct.

    Since Swift functions are also known as closures you can produce the same result by sending a subsequent closure to filter and using the closing syntax:

      la shortWaitTimeRides2 = parkRides.filter {$ 0.waitTime <15.0}
    print (shortWaitTimeRides2)
     Here,  filter (_ :)  takes each turn in  parkRides  - represented by  $ 0  - looks at  waitTime  the property and tests if it is less than 15 minutes. You become declarative and tell the program what you want it to do. 

    The Map Collection method map (_ :) accepts a single function as a parameter. This can look pretty cryptic the first time you work with it. It publishes a matrix of the same length after using that function for each element of the collection. The return type of the mapped function not must be the same type as the collection elements.

    Try this:

      la oranges = apples.map {_ in "🍊"}
    print (oranges)
    

    You map each and every apple to an orange that produces an orange party:].

    You can use maps (_ :) to the elements of your parkRides array to get a list of all the tour names as strings:

      la rideNames = parkRides.map { $ 0.name}
    print (rideNames)
    testOriginalNameOrder (rideNames)
    

    You have shown that using the map (_ :) to get the tour names, you do the same as iterating across the collection, just as you did earlier.

    You can also sort the trip name as shown below when using sorted (by :) method on the Collection type to perform the sorting:

      print (rideNames.sorted (by: <))
     collection  method  sorted (by :)  takes a function that compares two elements and returns a  Bool  as a parameter. Because the operator  < is a fashionable fashion feature, you can use the Swift shorthand for the closure  {$ 0 <$ 1} . Swift provides the left and right sides as standard. 

    You can now reduce the code to extract and sort the trip names to only two lines, thanks to map (_ :) and .

    Sort by NameImp (_ 🙂 as Sorted nameFP (_ :) with the following code:

      Func sortedNamesFP _ rides: [Ride]) -> [String] {
    la rideNames = parkRides.map {$ 0.name}
    return riNames.sorted (by: <)
    }
    
    la sortedNames2 = sortedNamesFP (parkRides)
    testSortedNames (sortedNames2)
    

    Your declarative code is easier to read and you can find out how it works without too much trouble. The test shows that sortedNamesFP (_ :) does the same as sortedNamesImp (_ :) .

    Reduce method ] reduce (_: _ :) takes two parameters: The first is a starting value of any type T T and [theotherisafunctionthatcombinesavalueofthesametype T with an element of the collection to produce another value of the type T .

    The entry function applies to each element of the call collection one by one, until it reaches the end of the collection and produces a final accumulated value.

    For example, you can reduce these oranges to some juice:

      la juice = oranges.reduce ("") {juice, orange in juice + "🍹"}
    print ("fresh juice is served -" juice) ")
    

    Here you start with an empty string. Then add one 🍹 to the string for each orange. This code can juice any matrix so be careful what you put into it:].

    To be more practical, add the following method that lets you know the total waiting time for all the rides in the park.

      la totalWaitTime = parkRides.reduce (0.0) {(total, ride) in
    total + ride.waitTime
    }
    print ("total waiting time for all rides = (totalWaitTime) minutes")
    

    This function works by transmitting the start value of 0.0 to reducing and using descending synchronization to add to how much time each turn contributes to total latency. The code again uses Swift shorthand to omit the keyword return . By default, you return the result of total + ride.waitTime .

    In this example, the iterations look like this:

      Iteration's initial ride.waitTime resulting total
    1 0 45 0 + 45 = 45
    2 45 10 45 + 10 = 55
    ...
    8 200 0 200 + 0 = 200
    

    As you can see, the resulting total transmits as the starting value for the following iteration. This continues until reduce iterates through each Ride in parkRides . This allows you to get the sum with a line of code!

    Advanced Techniques

    You've learned about some of the common FP methods. Now it's time to take things a little further with a bit more function theory.

    Parts Features

    Partial function allows you to encapsulate a function in another. To see how this works, add the following method to the playground:

      func filter (for category: RideCategory) -> ([Ride]) -> [Ride] {
    return {rides in
    rides.filter {$ 0.categories.contains (category)}
    }
    }
    

    Here, filter (for :) accepts a RideCategory as its parameter and returns a function of the type ([Ride]) -> [Ride]. The output function takes a number of Ride objects and returns a number of Ride objects filtered by the specified category.

    Check the filter here by looking for trips suitable for young children:

      la kidRideFilter = filter (for: .kids)
    print ("some good trips for children are: n (kidRideFilter (parkRides))")
    

    You should see Spinning Tea Cups and Grand Carousel in the console output.

    Pure Functions

    A primary concept in FP that gives you reason for program structure, as well as test program results, is the idea of ​​a clean feature .

    A function is ] if it meets two criteria:

    • The function always gives the same output when given the same input, such as output only depends on the entrance. 19659048] The function creates zero side effects outside it.

    Add the following clean feature to your playground:

      func ridesWithWaitTimeUnder (_ waitTime: Minutes,
    from rides: [Ride]) -> [Ride] {
    return rides.filter {$ 0.waitTime <waitTime}
    }
    

    ridesWithWaitTimeUnder (_: from :) is a pure feature because its output is always the same when it is given the same waiting time and same list of rides.

    With a pure function, it is easy to write a good device test against the function. Add the following test to your playground:

      la shortWaitRides = ridesWithWaitTimeUnder (15, from: parkRides)
    
    func testShortWaitRides (_ test Filter: (Minutes, [Ride]) -> [Ride]) {
    la limit = minutes (15)
    leave the result = test file (limit, parkRides)
    print ("rides with wait less than 15 minutes: a (result)")
    name = result.map {$ 0.name} .sorted (by: <)
    let expected = ["Crazy Funhouse",
                      "Mountain Railroad"]
    claim (name == expected)
    print ("er test trips with waiting time below 15 = PASS n-")
    }
    
    testShortWaitRides (ridesWithWaitTimeUnder (_: from :))
    

    Notice how to pass the function ridesWithWaitTimeUnder (_: from :) to the test. Remember that features are first-class citizens and you can transfer them as any other data. This will benefit the next part.

    Also, again use map (_ :) and sorted (by :) to extract the names to try your test. You use FP to test your FP skills.

    Reference Transparency

    Pure functions are related to the concept reference transparency . An element in a program is reference glass transparent if you can replace it with its definition and always produce the same result. It provides predictable code and allows the compiler to perform optimizations.

    : 

    : : :  is reference glass transparent by leading the body to  testShortWaitRides (_ :) : 

     ] testShortWaitRides ({waitTime, rides in
    return rides.filter {$ 0.waitTime <waitTime}
    })
    

    In this code, you took the body of ridesWithWaitTimeUnder (_: from :) and passed it directly to the test testShortWaitRides (_ :) wrapped in the closing syntax. There is evidence that ridesWithWaitTimeUnder (_: from :) is the reference glass transparent.

    Reference authenticity comes in handy when you refactoring any code and you want to make sure you don't break anything. Reference transparent code is not only easy to test but also lets you move the code around without verifying implementations.

    Recursion

    The final concept for discussing is recursion . Recursion occurs when a function calls itself as part of its functional part. In functional languages, recursion replaces many of the looping constructions you use in imperative languages.

    When the function's entry leads to the function that calls itself, you have a recursive case . To avoid an infinite stack of function calls, recursive functions need a base case to terminate them.

    You must add a recursive sorting function for your rides. First layer Ride according to Comparable using the following extension:

      extension Ride: Comparative {
    public static func <(lhs: Ride, rhs: Ride) -> Bool {
    return lhs.waitTime < rhs.waitTime
      }
    
      public static func ==(lhs: Ride, rhs: Ride) -> Bool {
    return lhs.name == rhs.name
    }
    }
    

    In this extension, use operator overload to create functions that allow you to compare two trips. You can also see the full function declaration for the < operator that you used earlier in sorted (by :) . Less than is another ride if the waiting time is less, and the rides are like if the rides have the same name.

    Now, expand Array to include a quickSorted method:

      extension Array where Element: Comparative {
    func quickSorted () -> [Element] {
    if self.count> 1 {
    la (pivot, remaining) = (even [0] dropFirst ())
    la lhs = remaining.filter {$ 0 <= pivot }
          let rhs = remaining.filter { $0 > pivot}
    return lhs.quickSorted () + [pivot] + rhs.quickSorted ()
    }
    return yourself
    }
    }
    

    This extension lets you sort an array as long as the elements are Comparable .

    The Algorithm Quick Sort first selects a pivot element. Then you divide the collection into two parts. Some contain all the elements that are less than or equal to the pivot, the other holds the remaining elements larger than the pivot. Recursion is then used to sort the two parts. Note that by using recursion, you do not need a rendering mode.

    Confirm your function by entering the following code:

      let quickSortedRides = parkRides.quickSorted ()
    print ("(quickSortedRides)")
    
    
    func testSortedByWaitRides (_ rides: [Ride]) {
    let expected = rides.sorted (by: {$ 0.waitTime <$ 1.waitTime})
    assert (rides == expected, "unexpected order")
    print ("✅ test sorted by waiting time = PASS n-")
    }
    
    testSortedByWaitRides (quickSortedRides)
    

    Here, make sure that your solution matches the expected value from the reliable Swift standard library feature.

    Remember that recursive features have extra memory usage and runtime overhead. You don't have to worry about these issues until your datasets get bigger.

    Imperative vs Declarative Code Style

    In this section, you combine what you learned about FP to get a clear demonstration of the benefits of functional programming.

    Consider the following situation:

    A family with young children wants to go as many trips as possible between frequent bathing holidays. They need to find which kid-friendly rides have the shortest lines. Help them out by finding all family trips with waiting times less than 20 minutes and sorting them by shortest to longest wait.

    Solving the Problem of the Decisive Approach

    Think about how to solve this problem with an absolute algorithm. Try to implement your own solution to the problem.

    Your solution is likely to look like:

      var ridesOffInterest: [Ride] = []
    for driving in parkRider where ri.waitTime <20 {
    for category in ride.categories where category == .family {
    ridesOfInterest.append (ride)
    break
    }
    }
    
    la sortedRidesOfInterest1 = ridesOfInterest.quickSorted ()
    print (sortedRidesOfInterest1)
    

    Add this to your playground and perform it. You should see that the Mountain Railroad, Crazy Funhouse and Grand Carousel are the best tour choices and that the list is in order to increase the waiting time.

    As written, the obligatory code is fine, but a quick glance gives no clarity, immediate idea of ​​what it does. You need to pause to look at the algorithm in detail to understand it. Will the code be easy to understand when you return to maintenance six months later or if you deliver it to a new developer?

    Add this test to compare an FP approach to your important solution:

      func testSortedRidesOfInterest (_ rides: [Ride]) {
    leave name = rides.map {$ 0.name} .sorted (of: <)
    let expected = ["Crazy Funhouse",
                      "Grand Carousel",
                      "Mountain Railroad"]
    claim (name == expected)
    print ("test rides of interest = PASS
    }
    
    testSortedRidesOfInterest (sortedRidesOfInterest1)
    

    Solve the problem with a functional approach

    You can make your code much more self-explanatory with a FP solution. Add the following code to your playground:

      la sortedRidesOfInterest2 = parkRides
    .filter {$ 0.categories.contains (.family) && $ 0.waitTime <20}
    .sorted (by: <)
    

    Verify that this line of code gives the same output as the absolute code by adding:

      testSortedRidesOfInterest (sortedRidesOfInterest2)
    

    In a line of code you have told Swift what to calculate. You will filter parkRides to .family rides with waiting times less than 20 minutes and then sort them. It solves the problem above.

    The resulting code is declarative which means that it is self-explanatory and reads as the problem it solves.

    This is different from important code, which comes with the steps the computer must take to solve the problem.

    When and why of functional programming

    Swift is not purely a functional language, but it does combine several programming paradigms to give you flexibility for app development.

    A great place to start working with FP techniques, is in your model team and anywhere where the app's business logic is displayed. You've seen how easy it is to make discrete tests for that logic.

    For user interfaces, it is less clear to see where you want to use FP techniques. Reactive programming is an example of a FP-like approach to UI development. For example, RxSwift is a reactive library for iOS and macOS programming.

    By taking a functional, declarative approach, the code becomes more consistent and clear. Plus, your code will be easier to test when isolated in modular features that are free of side effects.

    When you want to maximize the full potential of your multi-core CPU, it is important to minimize side effects and problems from concurrency. FP is a great tool for having your skills set for those kinds of problems.

    Where to go from here?

    You can download the complete playground with all the code in this tutorial from Download the materials button at the top or bottom of the tutorial.

    While I have reviewed many of the important concepts in FP in this tutorial, you have just ridden on the surface of what FP is and what it can do. When you have some experience with these basic FP concepts, take the chance in the heart of FP by exploring:

    • Monads, Endofunctors and Category Theory.
    • Programming languages ​​where FP is the focus, such as Haskell and Clojure. 19659125] You get great insight into different aspects of Swift by studying these. You may be able to do things you never dreamed of with your newborn knowledge.

      You can also check out:

      If you have questions or comments about this tutorial, come and join the discussion below! [19659187]

      Source link