Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/ijoshsmith/function-composition-in-swift
An interactive introduction to function composition in Swift 3.
https://github.com/ijoshsmith/function-composition-in-swift
Last synced: 25 days ago
JSON representation
An interactive introduction to function composition in Swift 3.
- Host: GitHub
- URL: https://github.com/ijoshsmith/function-composition-in-swift
- Owner: ijoshsmith
- Created: 2017-01-21T08:02:26.000Z (almost 8 years ago)
- Default Branch: master
- Last Pushed: 2017-01-21T17:45:15.000Z (almost 8 years ago)
- Last Synced: 2024-04-22T12:31:11.106Z (8 months ago)
- Language: Swift
- Size: 31.3 KB
- Stars: 59
- Watchers: 6
- Forks: 2
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
- Awesome-Swift-Playgrounds - Function Composition in Swift - Exploration of function composition in Swift. π (Theoretical Computer Science / Functional Reactive Programming)
README
# Function Composition in Swift
This exploration of function composition in Swift is also available in an Xcode playground, which allows you to experiment with the presented code and immediately see the result. The playground is included in this repository.
## Getting started
Suppose you need to process comma-separated values. You receive some CSV text and need to only keep the rows that contain exactly three values. Here is the CSV text to process.
```
Ace,Ale,Are
Bag,Beg,Bug
Cat,Cut
```
The last row (Cat,Cut) is invalid because it has two values.The following functions would do the trick.
```swift
import Foundationfunc splitLines(ofText text: String) -> [String] {
return text.components(separatedBy: .newlines)
}func createRows(fromLines lines: [String]) -> [[String]] {
return lines.map { line in
line.components(separatedBy: ",")
}
}func removeInvalidRows(fromRows rows: [[String]]) -> [[String]] {
return rows.filter { row in
row.count == 3
}
}
```
Weaving these functions together might look something like this.
```swift
let lines = splitLines(ofText: csv)
let rows = createRows(fromLines: lines)
let validRows = removeInvalidRows(fromRows: rows)
```
It can be tempting to remove temporary variables that store each return value, but that often makes the code harder to understand.
```swift
let huh = removeInvalidRows(fromRows: createRows(fromLines: splitLines(ofText: csv)))
```
The code above is somewhat confusing because the functions appear in the opposite order from which they run. It's like reading a story backwards. It'd be nice to avoid extra variables without adding extra weirdness.What's a Swift developer to do?
## Function composition
Let's take a page from the functional programmer's book and use function composition! Composing two functions yields a new function, which wraps them both up. It's simple. The following code creates a new operator that we can put between two functions and it composes a new function. Ignore the `AdditionPrecedence` bit for now.
```swift
infix operator --> :AdditionPrecedence## Allowing for side effects
This all seems fine and dandy, but does it make debugging tricky? How can you inspect a function's return value when using function composition? One approach is to add logging statements to the functions being composed, but that only works if you can edit those functions, which isn't always the case. There's a better way, check it out.
```swift
func --> (
aToB: @escaping (A) -> B,
sideEffect: @escaping (B) -> Void)
-> (A) -> B
{
return { a in
let b = aToB(a)
sideEffect(b)
return b
}
}
```
This overload of the `-->` operator function gives special treatment to a special case. When the function on the righthand side returns `Void`, it is assumed to exist only to produce a side effect, such as logging to the console or saving data to a file. In the world of functional programming this is considered an "impure" function, because it does not operate only on its inputs, and therefore can have unpredictable side effects.Here's how we can use this new version of the `-->` function to perform logging.
```swift
let processCSVWithLogging = splitLines(ofText:)
--> { print("lines: \($0)") }
--> createRows(fromLines:)
--> { print("rows: \($0)") }
--> removeInvalidRows(fromRows:)
--> { print("valid rows: \($0)") }let validRows = processCSVWithLogging(csv)
```
## Optional chaining
The function composition operator works fine with optional values, since `Optional` is a Swift enum and can be used in a generic function just like any other type. But it would be nice if there was extra support for working with optional values when using function composition, similar to the optional chaining feature in Swift. For example, `account.emergencyContact?.sendAlert()` will only send an alert to an emergency contact if one exists.Here is a new variant of the function composition operator that supports optional chaining. The function on the left returns an optional value, and the function on the right will only be called if that value is non-nil.
```swift
infix operator -->? :AdditionPrecedencefunc url(forCompany stockSymbol: String) -> URL? {
let companyMap = ["AAPL": "http://apple.com",
"GOOGL": "http://google.com",
"MSFT": "http://microsoft.com"]
guard let path = companyMap[stockSymbol] else { return nil }
return URL(string: path)
}func data(fromURL url: URL) -> Data? {
return try? Data(contentsOf: url)
}func attributedHTML(withData data: Data) -> NSAttributedString? {
return try? NSAttributedString(data: data,
options: [NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType],
documentAttributes: nil)
}let attributedHTMLForCompany = url(forCompany:) -->? data(fromURL:) -->? attributedHTML(withData:)
let html = attributedHTMLForCompany("AAPL")
```
If any of the functions return `nil`, none of the subsequent functions will be called. This is a nice Swifty addition to our function composition toolbox. Remember, the `-->` and `-->?` operators can both be used when forming larger statements.
## Passing multiple arguments
All of the examples so far have composed functions with just one parameter, but it's possible to
use functions with multiple parameters. One way this can be accomplished is to wrap the function
call in a closure, as seen in the following example.
```swift
func getHour(fromDate date: Date) -> Int {
return Calendar.current.component(.hour, from: date)
}func isHour(_ hour: Int, between startHour: Int, and endHour: Int) -> Bool {
return (startHour...endHour).contains(hour)
}let isWorkHour = getHour(fromDate:) --> { isHour($0, between: 9, and: 17) }
let now = Date()
let shouldBeAtWork = isWorkHour(now)
```
## Have fun
That's all for this quick excursion into the world of function composition. It's definitely not a silver bullet, but it can lead to code that is easier to read and understand. Perhaps this style of coding feels right to you. If so, give it a try!