R.O.P. Railway Oriented Programming is a technique to manage complex chains of transformations and checks focusing only on the “happy path” (the good = green rail) and pretending that the errors (the bad = red rail) does not happens.
Using it is very simple: you need only to “wrap” your return types into a Result and returning good or bad things.
IE: a simple function
func check(_ x: Int) -> Result<Int> {
if x>10 {
return good(x)
} else {
return bad("X should be greater than 10")
}
}
IE: a sequence of functions with a mix of normal and ROP functions…
let complexComputation = good(22)
.then(check)
.then{$0/11} // Note that this is a closure
.then{x in "The result is \(x)!"}
print(complexComputation)
Ok(“The result is 2!”)
If something goes bad computation does not go wrong and we’ve a meaningful error…
let miserableMistake = good(7)
.then(check) // check function act as a railway exchange
.then{$0/11} // Note that this is a closure
.then{x in "The result is \(x)!"}
print(miserableMistake)
Error(__lldb_expr_21.SimpleError(msg: “X should be greater than 10”))
WARNING: I’ve found some problem with embedded images: should fix them soon.
Really great explanation! This approach is also very useful for asynchronous tasks, where you need to wait for some things to finish before you can run other tasks. Tasks can of course be modelled as closures, and you can chain your desired operations in a similar fashion. Futures / Promises frameworks extend this concept to deal with results that may or may not be available at a given point.
this is so cool, wonder what’s the different to Rx approach?
AFAIK, Rx is an implementation of Functional Reactive Programming paradigm: that focus on stream processing and asynchronous events.
(Functional Reactive Animation - Functional reactive programming - Wikipedia)
TL;DR: ROP is an optional with the “reason” when the result is nil.
The ROP is a revisitation of classical “Either” (Monad) proposed by Scott Wlaschin (Railway Oriented Programming | F# for fun and profit) and focused on the error handling.
Instead of classical if-the-else pyramid, you can reason on the “happy path” only, pretending that everything works, and if any of your “functions” breaks your computation train change the rail and you know why (looking at the error).
As @clattner said, you can have same behaviour with the optional chaining operator a()?.foo().
Q: what happens when you hit bad ? does it stop? then what happens if you want to do something special in response to bad ?
When the bad happens, you can take a look at your final result, but the current “then” operator is made to interrupt the computation.
This complete example shows how to use ROP to create a “shellCommand” utility function that is aware of bot errors (misspelled commands and exceptions) and command result (standard unix convention where:‘0’ means OK).
func countNumberOfFilesInFolder(_ folder: String) -> Result<Int> {
return shellCommandROP("/bin/ls", [folder])
.then(myLinesCounter)
}
printResult("1) Run on existing folder:", countNumberOfFilesInFolder("/notebooks/swift-rop/jupyter"))
printResult("2) Run on wrong folder: ", countNumberOfFilesInFolder("/wtf"))
Run on existing folder: 3
Run on wrong folder: ERROR: [2]: /bin/ls: cannot access ‘/wtf’: No such file or directory
In this way you can chain multiple commands
Longer chain
func countNumberOfFilesInCurrentFolder() -> Result<Int> {
return shellCommandROP("/bin/pwd")
.then{pwd in pwd.replacingOccurrences(of: "\n", with: "")} // Use previous result
.then{pwd in shellCommandROP("/bin/ls", [pwd])} // Use the clean string to search files
.then(myLinesCounter)
}
let complexResult = countNumberOfFilesInCurrentFolder();
printResult("The count of number of files in ls is:", complexResult)
The count of number of files in ls is: 3
Use cases
let theFolder = "/notebooks/swift-rop/jupyter/tmp"
let theFile = theFolder + "/test.txt"
print("1) create the folder \(theFolder)",shellCommandROP("/bin/mkdir",[theFolder]))
print("2) list files in the folder",shellCommandROP("/bin/ls", [theFolder]))
print("3) create the file \(theFile)",shellCommandROP("/bin/touch", [theFile]))
print("4) run non existent command",shellCommandROP("/bin/wtf", [theFile])) // wtf is not a command
printResult("5) list files again. Folder content:",shellCommandROP("/bin/ls", ["-lh",theFolder])) // list files in new folder
print("6) remove the file",shellCommandROP("/bin/rm", ["-rf", theFile]))
printResult("7) Folder content:",shellCommandROP("/bin/ls", ["-lh",theFolder])) // list files in new folder
print("8) remove the folder",shellCommandROP("/bin/rmdir",[theFolder]))
print("9) try to list files in deleted folder",shellCommandROP("/bin/ls", [theFolder]))
1) create the folder /notebooks/swift-rop/jupyter/tmp Ok("")
2) list files in the folder Ok("")
3) create the file /notebooks/swift-rop/jupyter/tmp/test.txt Ok("")
4) run non existent command Error(__lldb_expr_15.SimpleError(msg: "ERROR: The operation could not be completed."))
5) list files again. Folder content: total 0
-rw-r--r-- 1 root root 0 Apr 26 15:41 test.txt
6) remove the file Ok("")
7) Folder content: total 0
8) remove the folder Ok("")
9) try to list files in deleted folder Error(__lldb_expr_21.ShellCommandError(code: 2, msg: "/bin/ls: cannot access \'/notebooks/swift-rop/jupyter/tmp\': No such file or directory\n"))
NOTE: on command (4) we have a misspell error, while on command (9) we call a correct command, but on an illegal folder (The error holds error code 2 and descriptive text).
import Rop
func unixPwd() -> String {
return shell("/bin/pwd")
.then{pwd in pwd.replacingOccurrences(of: "\n", with: "")} // Clean pwd output form extra "\n"
.value! // Should be safe: usually pwd is not nil ;-)
}
Test it…
print("'\(unixPwd())'")
‘/notebooks/swift-rop/jupyter’
if you make a mistake…
print("'\(unixPwd())'")
‘/notebooks/swift-rop/jupyter’
The whole ack command wrapper
import Foundation
typealias AckResults = Result<[AckResult]>
func printAckResults(ackrRes: AckResults, printFullPath: Bool) {
if let ackr=ackrRes.value {
for row in ackr {
let theFileName = (printFullPath) ? row.fileName : (row.fileName as NSString).lastPathComponent
print(theFileName, "R:\(row.row)", row.sample)
}
} else { print(">> No results found <<") }
}
func ack(word: String, path: String? = nil, caseInsensitive: Bool = true, show: Bool = true, printFullPath: Bool = false) -> AckResults {
let thePath = path ?? unixPwd()
let args = good([word, thePath])
.then{args in caseInsensitive ? args + ["-i"] : args}
let ret = shell("/usr/bin/ack", args.value!)
.then{str in str.components(separatedBy: "\n")}
//.use{lines in print(lines)} // Debug example
.then{lines in lines.map(processLine)}
.then{rets in rets.filter{r in r.fileName != "?"}} // Filtering out errors
if show && ret.isOk() { printAckResults(ackrRes: ret, printFullPath: printFullPath) }
return ret
}
#Test it...
ack(word: "float", path: "/notebooks/fastai_docs/dev_swift/FastaiNotebook_08_data_block")