AtelierClockwork

Custom Operators

December 16, 2014

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.