Improving WunderTDD with a higher order function, functional programming, and an extension

WunderTDD is my contribution to OpenSource with a MIT license, a project demonstration of getting Weather information from Weather Underground by location name, including Test Driven Design elements.

I realized that I could improve the code in the WunderTDD project, so I am back at the WunderTDD project to make an improvement;

GitHub project;

git clone https://github.com/eSpecialized/WunderTDD.git
git checkout vPreFPChange

Tag: The git tag before changes is vPreFPChange to follow along.

In the WunderAPI.swift file, there is a member function findIconUrlString that could be done differently.  It already works, but it is a bit long for such a simple task. In fact it is almost a Pure Function (Zero side effects, Always produces the same output given the same input) but has some variables inside. This makes a good candidate for Functional Programming.

func findIconUrlString(iconName: String) -> String?
    {
        var iconStringResult : String?
        
        let iconName = iconName + ".gif"
        for thisString in weatherIconUrls {
            let urlComps = URLComponents(string: thisString)
            let pathComps = urlComps?.path.components(separatedBy: CharacterSet(charactersIn: "/"))
            if let theLastComp = pathComps?.last {
                if theLastComp == iconName {
                    iconStringResult = thisString
                    break
                }
            }
        }
        return iconStringResult
    }

In English;
We go through each item in the String Array, compare it to what we are looking for, and return that result once found. Nil is returned if the result isn’t found.

Using a playground we can quickly build a working function that does exactly the same thing.

Step 1; Lets start with this code that works in a new playground called URLComponentsFPPlayground.playground

import Foundation

let weatherIconUrlStrings = ["https://icons.wxug.com/i/c/d/chanceflurries.gif",
        "https://icons.wxug.com/i/c/d/chancerain.gif",
        "https://icons.wxug.com/i/c/d/chancesleet.gif",
        "https://icons.wxug.com/i/c/d/chancesnow.gif",
        "https://icons.wxug.com/i/c/d/chancetstorms.gif",
        "https://icons.wxug.com/i/c/d/clear.gif",
        "https://icons.wxug.com/i/c/d/cloudy.gif",
        "https://icons.wxug.com/i/c/d/flurries.gif",
        "https://icons.wxug.com/i/c/d/fog.gif",
        "https://icons.wxug.com/i/c/d/hazy.gif",
        "https://icons.wxug.com/i/c/d/mostlycloudy.gif",
        "https://icons.wxug.com/i/c/d/mostlysunny.gif",
        "https://icons.wxug.com/i/c/d/partlycloudy.gif",
        "https://icons.wxug.com/i/c/d/partlysunny.gif",
        "https://icons.wxug.com/i/c/d/sleet.gif",
        "https://icons.wxug.com/i/c/d/rain.gif",
        "https://icons.wxug.com/i/c/d/snow.gif",
        "https://icons.wxug.com/i/c/d/sunny.gif",
        "https://icons.wxug.com/i/c/d/tstorms.gif",
        "https://icons.wxug.com/i/c/d/nt_chanceflurries.gif",
        "https://icons.wxug.com/i/c/d/nt_chancerain.gif",
        "https://icons.wxug.com/i/c/d/nt_chancesleet.gif",
        "https://icons.wxug.com/i/c/d/nt_chancesnow.gif",
        "https://icons.wxug.com/i/c/d/nt_chancetstorms.gif",
        "https://icons.wxug.com/i/c/d/nt_clear.gif",
        "https://icons.wxug.com/i/c/d/nt_cloudy.gif",
        "https://icons.wxug.com/i/c/d/nt_flurries.gif",
        "https://icons.wxug.com/i/c/d/nt_fog.gif",
        "https://icons.wxug.com/i/c/d/nt_hazy.gif",
        "https://icons.wxug.com/i/c/d/nt_mostlycloudy.gif",
        "https://icons.wxug.com/i/c/d/nt_mostlysunny.gif",
        "https://icons.wxug.com/i/c/d/nt_partlycloudy.gif",
        "https://icons.wxug.com/i/c/d/nt_partlysunny.gif",
        "https://icons.wxug.com/i/c/d/nt_sleet.gif",
        "https://icons.wxug.com/i/c/d/nt_rain.gif",
        "https://icons.wxug.com/i/c/d/nt_snow.gif",
        "https://icons.wxug.com/i/c/d/nt_sunny.gif",
        "https://icons.wxug.com/i/c/d/nt_tstorms.gif",
        "https://icons.wxug.com/i/c/d/nt_cloudy.gif",
        "https://icons.wxug.com/i/c/d/nt_partlycloudy.gif"
    ]
    
    func findIconUrlString(iconName: String) -> String?
    {
        var iconStringResult : String?
        
        let iconName = iconName + ".gif"
        for thisString in weatherIconUrlStrings {
            let urlComps = URLComponents(string: thisString)
            let pathComps = urlComps?.path.components(separatedBy: CharacterSet(charactersIn: "/"))
            if let theLastComp = pathComps?.last {
                if theLastComp == iconName {
                    iconStringResult = thisString
                    break
                }
            }
        }
        return iconStringResult
    }

A. Why is this code a good candidate for functional programming?

  1. There is no side effects outside of the function, ie its not mutating.
  2. We know that either a value must be returned, or nil if the string that is sent isn’t in the array.

B. Why is this code a good candidate for a higher-order function?

  1. Filter could have been used instead of a for-loop.

C. How can we make those goals line up?
We can make an extension, it simplifies our code to something very concise. Simple is better allowing the code to be much more readable now and in the future.

Step 2; Insert a simple test line to ensure the current code is working properly.

  print(findIconUrlString(iconName: "nt_rain")!)
  print(findIconUrlString(iconName: "none")) //should result in a nil

The results of which is the following two lines of output;

https://icons.wxug.com/i/c/d/nt_rain.gif
nil

Step 3; Start with the first refactor to our method, start using urlComponents and filter on a single line.

func findIconUrlString(iconName: String) -> String?
{
    let iconName = iconName + ".gif"
    
    let results: [String] = weatherIconUrlStrings.filter { weatherStringUrl in
        let lastComp = URLComponents(string: weatherStringUrl)?.path.components(separatedBy: CharacterSet(charactersIn: "/")).last
        return lastComp == iconName
    }
    return results.first
}

For a first refactor this looks great.

What is going on here?
1. The Function, the parameters and return value never changed
2. We refactored the internals of the Function to be more concise, and clear.

Going a little further;

Step 4; Adding an extension urlComponentsFromString to String, using $0 as the block input argument as opposed to specifying ‘weatherStringUrl in’. The swift language knows to infer the return value is a array of Strings, so we remove the type info :[String] on the let results:[String] line.

extension String {
    func urlComponentsFromString() -> URLComponents? {
        return URLComponents(string: self)
    }
}

func findIconUrlString(iconName: String) -> String?
{
    let iconName = iconName + ".gif"

    let results = weatherIconUrlStrings.filter {
        let lastComp = $0.urlComponentsFromString()?.path.components(separatedBy: CharacterSet(charactersIn: "/")).last
        return lastComp == iconName
    }
    return results.first
}

What do we gain/loose from what could be considered oversimplification?

  1. The line length is no longer a run on sentence. But it is only slightly more readable and short.
  2. We would now have to read the code more carefully to understand that an array of [String] is the return from the filter

Step 5; Lets go for a far more concise version of the function and extension.
We could actually do slightly better to be more concise here. The extension is plain, and you can see we want path components back from the extension call.

extension String {
    func urlPathComponentsFromString(separator: String) -> [String]? {
        return URLComponents(string: self)?.path.components(separatedBy: CharacterSet(charactersIn: separator))
    }
}

func findIconUrlString(iconName: String) -> String?
{
    let results = weatherIconUrlStrings.filter {
        let lastComp = $0.urlPathComponentsFromString(separator: "/")?.last
        return lastComp == iconName + ".gif"
    }
    return results.first
}

A little simpler;

func findIconUrlString(iconName: String) -> String?
{
    let results = weatherIconUrlStrings.filter {
        let lastComp = $0.urlPathComponentsFromString(separator: "/")?.last
        return lastComp == iconName + ".gif"
    }
    return results.first
}

Now that looks like a great logical conclusion to be a concise, functional programming.

Every step of the way those two print lines will print the exact same thing, never changing.

Another functional programming item is Referential Transparency, which is to say that the same code works outside of the function call.

Final project changes;

Create an empty swift file named String+URLComponents.swift, place the extension code into the file along with a single import.

import Foundation

extension String {
    func urlPathComponentsFromString(separator: String) -> [String]? {
        return URLComponents(string: self)?.path.components(separatedBy: CharacterSet(charactersIn: separator))
    }
}

Edit the WWunderAPI.swift file,

rename

let weatherIconUrls

let weatherIconUrlStrings

for clarity.

Here is the final edit to our function. No more let statements inside or outside, just returning the filter results.

Also remove the original implementation of the findIconUrlString, and replace it with this code.

func findIconUrlString(iconName: String) -> String?
{
    return weatherIconUrlStrings.filter {
        $0.urlPathComponentsFromString(separator: "/")?.last == iconName + ".gif"
    }.first
}

Review questions for you;

  • Does the code it produce Zero side effects, and consistently provides the same return values based on input?
  • Does it meet the definition for Functional Programming?
  • Is this a Pure Function?
  • Is the code Referentially Transparent?
  • Is the resulting changes and output easier to read and understand?

Final tag for these changes which includes the playground;

git checkout vPostFPChange

Tested ; Passed 1/26/2018
platform tested on iOS 11.2 using Xcode 9.2 and Playgrounds with Swift 4.0.3

Links more on Functional Programming;

https://en.wikipedia.org/wiki/Functional_programming

https://www.raywenderlich.com/114456/introduction-functional-programming-swift

 

Facebook Comments

Bill Thompson

I am a mobile smartphone and embedded Linux engineer. I love to tinker with electronics and fly drones in spare time when not geeking out in a developers world!

Leave a Reply

Your email address will not be published. Required fields are marked *