Custom Operators
Dangerous Toys Are Fun, but You Might Get Hurt
I'd have to have a very good reason to define a custom operator in a production codebase. That being said, one of the fun parts of playing around in a very small side project code base is that I can play with concepts that are a bit silly.
Building the URL query involves a lot of optional values, which made chaining them together a bit cumbersome.
The first way I did this was by extending the array class and adding a very simple wrapped version of append:
extension Array{
mutating func appendOptional(optionalValue:T?){
if let safeValue = optionalValue{
self.append(safeValue)
}
}
}
private var queryBody:String {
get{
var queries = [String]()
queries.appendOptional(termKey.queryParameter)
queries.appendOptional(mediaKey.queryParameter)
queries.appendOptional(safeEntities.queryParameter)
queries.appendOptional(safeAttribute.queryParameter)
queries.appendOptional(limitKey.queryParameter)
queries.appendOptional(countryKey.queryParameter)
queries.appendOptional(explicitKey.queryParameter)
queries.appendOptional(languageKey.queryParameter)
return queries.count > 0 ? "?" + "&".join(queries) : ""
}
}
That's using mutable objects, has to instantiate an empty array first because I'm writing the code assuming any of the values can be optional, it's using a mutable variable, and it's just more verbose than I'd like.
I decided to give a custom infix operator a shot next, more because it's a fun tool to play with than thinking it was a great idea.
infix operator <<? {
associativity left
precedence 100
}
func <<?<T>(a:[T],i:T?) -> [T]{
if let ui = i{
return a + [ui]
}
return a
}
func <<?<T>(a:T?,b:T?) -> [T]{
if let ua = a{
if let ub = b{
return [ua, ub]
}else{
return [ua]
}
}else if let ub = b{
return [ub]
}
return []
}
private var queryBody:String {
get{
let queries:[String] = termKey.queryParameter <<?
mediaKey.queryParameter <<?
safeEntities.queryParameter <<?
safeAttribute.queryParameter <<?
limitKey.queryParameter <<?
countryKey.queryParameter <<?
explicitKey.queryParameter <<?
languageKey.queryParameter
return queries.count > 0 ? "?" + "&".join(queries) : ""
}
}
The code in the function is an improvement, but the infix implementation itself is harder to read, and it's a bit of a strange hack of an operator. I also still had to explicitly provide a type definition to the array to make sure the compiler knew to use create a String array rather than a String? array.
As silly as it sounds, having to be reminded that Swift can handle nil
values in an array, I've internalized some Objective-C quirks too well, made me think of the best solution to the problem so far:
func compact<T>(arr_in:[T?]) -> [T]{
return arr_in.reduce([T]()){
if let value = $1{
return $0 + [value]
}
return $0
}
}
private var queryBody:String {
get{
let queries = compact([termKey.queryParameter,
mediaKey.queryParameter,
safeEntities.queryParameter,
safeAttribute.queryParameter,
limitKey.queryParameter,
countryKey.queryParameter,
explicitKey.queryParameter,
languageKey.queryParameter])
return queries.count > 0 ? "?" + "&".join(queries) : ""
}
}
That one has it all, it has minimal mutable state, the compiler can correctly guess all of the types via inference without hinting, it's built on a reusable generic helper function, and long term readability. I thought about implementing it as a extension to Array
like the first variant, but I like the compact
method explicitly demanding an optional array as input.
I spent a foolishly long amount of time getting the implementation just so, but it's a great example of forcing myself to unlearn my Objective-C habits like avoiding putting nil
in an array and learning how to create code that takes advantage of all of the new bits in Swift.