Decided to start a series of (regular?) blog posts on interesting features in the Swift language. First up, callAsFunction.
Introduced in Swift 5.2, callAsFunction provides a way to call values as functions. An instance of a class or struct can be called as a function if it contains a callAsFunction method.
Let’s dig into this by first starting off with a simple class, JsonUtility, which can be initialized with a raw JSON String. It contains a lookup(key:) function which can recursively check for the value for a given key in the JSON:
enum JsonUtilityError: Error {
case initError(String)
}
class JsonUtility {
private let generatedDict: [String: Any]
init(withString string: String) throws {
guard let data = string.data(using: .utf8) else {
throw JsonUtilityError.initError("Can't extract data from string")
}
let obj = try JSONSerialization.jsonObject(with: data, options: [])
guard let dict = obj as? [String: Any] else {
throw JsonUtilityError.initError("JSON invalid")
}
generatedDict = dict
}
func lookup(key: String) -> Any? {
lookup(key: key, inDict: generatedDict)
}
private func lookup(key: String, inDict dictionary: [String: Any]) -> Any? {
var output: Any?
for dictKey in dictionary.keys {
if dictKey == key {
output = dictionary[dictKey]
} else if let childDict = dictionary[dictKey] as? [String: Any] {
output = lookup(key: key, inDict: childDict)
}
}
return output
}
As you can see, when initialized with a string, this class converts the provided string into a [String: Any] dictionary and assigns it to generatedDict. A lookup(key:) function searches the value for this key in this generatedDict recursively. This is what it would look like in action:
var str = """
{
"id": 234896773,
"name": "Beaver",
"owner": {
"login": "ravitripathi",
"id": 13906959,
"url": "https://api.github.com/users/ravitripathi",
"html_url": "https://github.com/ravitripathi",
"followers_url": "https://api.github.com/users/ravitripathi/followers",
"starred_url": "https://api.github.com/users/ravitripathi/starred{/owner}{/repo}",
"repos_url": "https://api.github.com/users/ravitripathi/repos",
"events_url": "https://api.github.com/users/ravitripathi/events{/privacy}",
"received_events_url": "https://api.github.com/users/ravitripathi/received_events",
"type": "User",
"site_admin": false
}
}
"""
if let jsonUtil = try? JsonUtility(withString: str) {
let htmlUrlString = jsonUtil.lookup(key: "html_url") // "https://github.com/ravitripathi"
}
But what if we could remove the .lookup call to obtain the urlString? callAsFunction allows you to use the jsonUtil instance as a function call, allowing you to make your call syntax more concise.
class JsonUtility {
private let generatedDict: [String: Any]
init(withString string: String) throws { ... }
private func lookup(key: String, inDict dictionary: [String: Any]) -> Any? { ... }
func callAsFunction(findValueForKey key: String) -> Any? {
return lookup(key: key, inDict: generatedDict)
}
}
//Usage
if let jsonUtil = try? JsonUtility(withString: str) {
let htmlUrlString = jsonUtil(findValueForKey: "html_url") // "https://github.com/ravitripathi"
}
Just like with normal functions, it is also possible to overload a callAsFunction method:
func callAsFunction(findValueForKey key: String) -> Any? { .. }
func callAsFunction(findKeyForValue value: String) -> String? { .. }
jsonUtil(findValueForKey: "html_url") // "https://github.com/ravitripathi"
jsonUtil(findKeyForValue: "https://api.github.com/users/ravitripathi/repos") // "repo_url"
Where should I use this?
Often, you might have many nominal types, that have a “primary method” that performs their main use. For example, a calculator class which mostly calls a specific function.
calculator.calculating(query)
Since a primary function like this one would be called frequently, callAsFunction simplifies the expression used for it.