Swift Literal Convertibles

来源:互联网 发布:如何上淘宝网买东西 编辑:程序博客网 时间:2024/06/05 08:25

Written by Mattt Thompson — August 18th, 2014

Last week, we wrote about overloading and creating custom operators in Swift, a language feature that is as powerful as it is controversial.

By all accounts, this week’s issue threatens to be equally polarizing, as it covers a feature of Swift that is pervasive, yet invisible: literal convertibles.


In code, a literal is notation representing a fixed value. Most languages define literals for logical values, numbers, strings, and often arrays and dictionaries.

let int = 57let float = 6.02let string = "Hello"

Literals are so ingrained in a developer’s mental model of programming that most of us don’t actively consider what the compiler is actually doing (thereby remaining blissfully unaware of neat tricks like string interning).

Having a shorthand for these essential building blocks makes code easier to both read and write.

In Swift, developers are provided a hook into how values are constructed from literals, called literal convertible protocols.

The standard library defines 10 such protocols:

  • ArrayLiteralConvertible
  • BooleanLiteralConvertible
  • DictionaryLiteralConvertible
  • ExtendedGraphemeClusterLiteralConvertible
  • FloatLiteralConvertible
  • NilLiteralConvertible
  • IntegerLiteralConvertible
  • StringLiteralConvertible
  • StringInterpolationConvertible
  • UnicodeScalarLiteralConvertible

Any class or struct conforming to one of these protocols will be eligible to have an instance of itself statically initialized from the corresponding literal.

It’s what allows literal values to “just work” across the language.

Take optionals, for example.

NilLiteralConvertible and Optionals

One of the best parts of optionals in Swift is that the underlying mechanism is actually defined in the language itself:

enum Optional<T> : Reflectable, NilLiteralConvertible {    case None    case Some(T)    init()    init(_ some: T)    init(nilLiteral: ())    func map<U>(f: (T) -> U) -> U?    func getMirror() -> MirrorType}

Notice that Optional conforms to the NilLiteralConvertible protocol:

protocol NilLiteralConvertible {    init(nilLiteral: ())}

Now consider the two statements:

var a: AnyObject = nil // !var b: AnyObject? = nil

The declaration of var a generates the compiler warning Type 'AnyObject' does not conform to the protocol 'NilLiteralConvertible, while the declaration var b works as expected.

Under the hood, when a literal value is assigned, the Swift compiler consults the corresponding protocol (in this case NilLiteralConvertible), and calls the associated initializer (init(nilLiteral: ())).

Although the implementation of init(nilLiteral: ()) is private, the end result is that an Optionalset to nil becomes .None.

StringLiteralConvertible and Regular Expressions

Swift literal convertibles can be used to provide convenient shorthand initializers for custom objects.

Recall our Regex example from last week:

struct Regex {    let pattern: String    let options: NSRegularExpressionOptions!    private var matcher: NSRegularExpression {        return NSRegularExpression(pattern: self.pattern, options: self.options, error: nil)    }    init(pattern: String, options: NSRegularExpressionOptions = nil) {        self.pattern = pattern        self.options = options    }    func match(string: String, options: NSMatchingOptions = nil) -> Bool {        return self.matcher.numberOfMatchesInString(string, options: options, range: NSMakeRange(0, string.utf16Count)) != 0    }}

Developers coming from a Ruby or Perl background may be disappointed by Swift’s lack of support for regular expression literals, but this can be retcon’d in using the StringLiteralConvertibleprotocol:

extension Regex: StringLiteralConvertible {    typealias ExtendedGraphemeClusterLiteralType = StringLiteralType    init(unicodeScalarLiteral value: UnicodeScalarLiteralType) {        self.pattern = "\(value)"    }    init(extendedGraphemeClusterLiteral value: ExtendedGraphemeClusterLiteralType) {        self.pattern = value    }    init(stringLiteral value: StringLiteralType) {        self.pattern = value    }}

StringLiteralConvertible itself inherits from the ExtendedGraphemeClusterLiteralConvertible protocol, which in turn inherits from UnicodeScalarLiteralConvertibleExtendedGraphemeClusterLiteralType is an internal type representing a String of length 1, while UnicodeScalarLiteralType is an internal type representing a Character. In order to implement the required inits, ExtendedGraphemeClusterLiteralType and UnicodeScalarLiteralType can be typealias’d to StringLiteralType and Character, respectively.

Now, we can do this:

let string: String = "foo bar baz"let regex: Regex = "foo"regex.match(string) // true

…or more simply:

"foo".match(string) // true

Combined with the custom operator =~, this can be made even more idiomatic:

"foo bar baz" =~ "foo" // true

Some might bemoan this as the end of comprehensibility, while others will see this merely as filling in one of the missing parts of this new language.

It’s all just a matter of what you’re used to, and whether you think a developer is entitled to add features to a language in order for it to better suit their purposes.

Either way, I hope we can all agree that this language feature is interesting, and worthy of further investigation. So in that spirit, let’s venture forth and illustrate a few more use cases.


ArrayLiteralConvertible and Sets

For a language with such a deep regard for immutability and safety, it’s somewhat odd that there is no built-in support for sets in the standard library.

Arrays are nice and all, but the O(1) lookup and idempotence of sets… *whistful sigh*

So here’s a simple example of how Set might be implemented in Swift, using the built-in Dictionarytype:

struct Set<T: Hashable> {    typealias Index = T    private var dictionary: [T: Bool] = [:]    var count: Int {        return self.dictionary.count    }    var isEmpty: Bool {        return self.dictionary.isEmpty    }    func contains(element: T) -> Bool {        return self.dictionary[element] ?? false    }    mutating func put(element: T) {        self.dictionary[element] = true    }    mutating func remove(element: T) -> Bool {        if self.contains(element) {            self.dictionary.removeValueForKey(element)            return true        } else {            return false        }    }}

A real, standard library-calibre implementation of Set would involve a lot more Swift-isms, like generators, sequences, and all manner of miscellaneous protocols. It’s enough to write an entirely separate article about.

Of course, a standard collection class is only as useful as it is convenient to use. NSSet wasn’t so lucky to receive the first-class treatment when array and dictionary literal syntax was introduced with the Apple LLVM Compiler 4.0, but we can right the wrongs of the past with the ArrayLiteralConvertible protocol:

protocol ArrayLiteralConvertible {    typealias Element    init(arrayLiteral elements: Element...)}

Extending Set to adopt this protocol is relatively straightforward:

extension Set: ArrayLiteralConvertible {    public init(arrayLiteral elements: T...) {        for element in elements {            put(element)        }    }}

But that’s all it takes to achieve our desired results:

let set: Set = [1,2,3]set.contains(1) // trueset.count // 3

This example does, however, highlight a legitimate concern for literal convertibles: type inference ambiguity. Because of the significant API overlap between collection classes like Array and Set, one could ostensibly write code that would behave differently depending on how the type was resolved (e.g. set addition is idempotent, whereas arrays accumulate, so the count after adding two equivalent elements would differ)

StringLiteralConvertible and URLs

Alright, one last example creative use of literal convertibles: URL literals.

NSURL is the fiat currency of the URL Loading System, with the nice feature of introspection of its component parts according to RFC 2396. Unfortunately, it’s so inconvenient to instantiate, that third-party framework authors often decide to ditch them in favor of worse-but-more-convenient strings for method parameters.

With a simple extension on NSURL, one can get the best of both worlds:

extension NSURL: StringLiteralConvertible {    public class func convertFromExtendedGraphemeClusterLiteral(value: String) -> Self {        return self(string: value)    }    public class func convertFromStringLiteral(value: String) -> Self {        return self(string: value)    }}

One neat feature of literal convertibles is that the type inference works even without a variable declaration:

"http://nshipster.com/".host // nshipster.com

As a community, it’s up to us to decide what capabilities of Swift are features and what are bugs. We’ll be the ones to distinguish pattern from anti-pattern; convention from red flag.

So it’s unclear, at the present moment, how things like literal convertibles, custom operators, and all of the other capabilities of Swift will be reconciled. This publication has, at times, been more or less prescriptive on how things should be, but in this case, that’s not the case here.

All there is to be done is to experiment and learn.


原文转自:http://nshipster.com/swift-literal-convertible/

0 0
原创粉丝点击