简体   繁体   中英

Generic protocol for basic types

I've been pondering the possible implementations of string formatting for swift, most of which boil down to "use NSString(format:...)" That's all well and good, but I wanted a concise and readable format, so I decided to implement something like python's % formatting operator:

@infix func % (value:Double, format:String) -> String {
    return NSString(format:format, value)
}

This works great for Double's as I can use:

println("PI = " + M_PI % "%.3f")

which results in:

PI = 3.142

While I can create 5 of these trivially, I'd like to turn it into a generic function:

@infix func %<T> (value:T, format:String) -> String {
    return NSString(format:format, value)
}

But that results in the message:

Could not find an overload for 'init' that accepts the supplied arguments

Reasonable enough, I could be passing in a tuple, or something equally non-objective-C. (Note that to really do this Python-style, I want to pass in a tuple, but that's another matter and beyond the scope of this question)

I tried declaring my own empty protocol and implementing that on Double, but it didn't help at all.

protocol NSStringFormattable {}
extension Double : NSStringFormattable {}

@infix func % <T:NSStringFormattable> (value:T, format:String) -> String {
    return NSString(format:format, value)
}

I could obviously do something like add a format function to each class and then just define the operator in terms of the format function, but in many ways that's not any better than just defining 5 different operator overloads.

protocol NSStringFormattable {
    func format(format:String) -> String
}

extension Double : NSStringFormattable {
    func format(format:String) -> String {
        return NSString(format:format, self)
    }
}

@infix func % <T:NSStringFormattable> (value:T, format:String) -> String {
    return value.format(format)
}

How can I restrict T to only those types that can be passed to NSString(format:...) ?

Found it!

@infix func % (value:CVarArg, format:String) -> String {
    return NSString(format:format, value)
}

This single function allows for:

5 % "%04x"

3.4 % "%.3f"

M_PI % "%.3f"

Int64(32) % "%04X"

Unfortunately, it also allows for:

"String" % "%3.3s"

and produces garbage, but welcome to printf without argument type checking

Even further, by defining a set of functions:

@infix func % (values:(CVarArg, CVarArg), format:String) -> String {
    return NSString(format:format, values.0, values.1)
}

@infix func % (values:(CVarArg, CVarArg, CVarArg), format:String) -> String {
    return NSString(format:format, values.0, values.1, values.2)
}

We can achieve python-like affects:

(M_PI, 5) % "%.3f->%d"

Kind of ugly to have to define one per tuple-length, but I'll keep hacking on it :)

You're very close, but you don't need a fixed-length tuple. That's what's causing your headaches. Just use an array instead.

@infix func % (values:CVarArg[], format:String) -> String {
  return NSString(format:format, arguments:getVaList(values))
}

[M_PI, 6] % "%.3f->%d"
==> "3.142->6"

[M_PI, M_PI_2] % "%.3f %.3f"
==> "3.142 1.571"

Of course this is highly type-unsafe because it's an unchecked printf as you say.

BTW, this even works with mixed-type stuff, and with non-literals:

let x = 1
let y = 1.5
let z = "yes"

[x, y, z] % "%d, %.2f, %@"
==> "1, 1.50, yes"

I don't know if that part is going to be fragile, however. Mixed-type literals are promoted to NSArray , which seems a dangerous thing to do automatically, so they may change it. But NSArray is acceptable as a CVarArg[] .

Note that not all types can be converted this way. Characters currently cannot for instance. You can overcome this by extending them to do so:

extension Character : CVarArg {
  func encode() -> Word[] {
    var result = Word[]()
    let s = String(self)
    for c in s.unicodeScalars {
      result.append(Word(c.value))
    }
    return result
  }
}

let c:Character = "c"

["I", c, 2*3] % "%@, %lc, %d"
==> "I, c, 6"

I'm wondering if there's an easier way to write encode() , but I'm not sure yet. Hopefully a Character encoding will be provided by Swift in the future. But the lesson here is that arbitrary types could be given an encode and be formatted.

class Car {
  let make = "Ford"
}

extension Car : CVarArg {
  func encode() -> Word[] {
    return NSString(string:self.make).encode()
  }
}

let car = Car()

[car] % "%@"

The lesson here is that you can turn arbitrary things into a CVarArg or into any protocol, via extensions.

As far as I have discovered (having gone through the same journey as you), generics are not the solution here, rather multiple overloading (which isn't pretty) -

operator infix % { }
@infix func % (format: String, value: Double) -> String {
    return NSString(format:format, value)
}
@infix func % (format: String, value: Float) -> String {
    return NSString(format:format, value)
}
@infix func % (format: String, value: Int) -> String {
    return NSString(format:format, value)
}

sorry, reverse parameter order from your example - allows

println("PI = %.3f" % M_PI)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM