Quantcast
Channel: NSHipster
Viewing all articles
Browse latest Browse all 382

Swift Literal Convertibles

$
0
0

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.

letint=57letfloat=6.02letstring="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
  • CharacterLiteralConvertible
  • DictionaryLiteralConvertible
  • ExtendedGraphemeClusterLiteralConvertible
  • FloatLiteralConvertible
  • NilLiteralConvertible
  • IntegerLiteralConvertible
  • StringLiteralConvertible
  • StringInterpolationConvertible

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:

enumOptional<T>:Reflectable,NilLiteralConvertible{caseNonecaseSome(T)init()init(_some:T)varhasValue:Bool{get}funcmap<U>(f:(T)->U)->U?funcgetMirror()->MirrorTypestaticfuncconvertFromNilLiteral()->T?}

Notice that Optional conforms to the NilLiteralConvertible protocol:

protocolNilLiteralConvertible{classfuncconvertFromNilLiteral()->Self}

Now consider the two statements:

vara:AnyObject=nil// !varb: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 assigns the return value of the conversion function (convertFromNilLiteral()).

Although the implementation of convertFromNilLiteral() is private, the end result is that an Optional set 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:

structRegex{letpattern:Stringletoptions:NSRegularExpressionOptions!privatevarmatcher:NSRegularExpression{returnNSRegularExpression(pattern:self.pattern,options:self.options,error:nil)}init(pattern:String,options:NSRegularExpressionOptions=nil){self.pattern=patternself.options=options}funcmatch(string:String,options:NSMatchingOptions=nil)->Bool{returnself.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 StringLiteralConvertible protocol:

extensionRegex:StringLiteralConvertible{typealiasExtendedGraphemeClusterLiteralType=StringLiteralTypestaticfuncconvertFromExtendedGraphemeClusterLiteral(value:ExtendedGraphemeClusterLiteralType)->Regex{returnself(pattern:value)}staticfuncconvertFromStringLiteral(value:StringLiteralType)->Regex{returnself(pattern:value)}}

StringLiteralConvertible itself inherits from the ExtendedGraphemeClusterLiteralConvertible protocol. ExtendedGraphemeClusterLiteralType is an internal type representing a String of length 1. In order to implement convertFromExtendedGraphemeClusterLiteral(), ExtendedGraphemeClusterLiteralType can be typealias'd to StringLiteralType.

Now, we can do this:

letstring:String="foo bar baz"letregex: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 Dictionary type:

structSet<T:Hashable>{typealiasIndex=Tprivatevardictionary:[T:Bool]init(){self.dictionary=[T:Bool]()}varcount:Int{returnself.dictionary.count}varisEmpty:Bool{returnself.dictionary.isEmpty}funccontains(element:T)->Bool{returnself.dictionary[element]??false}mutatingfuncput(element:T){self.dictionary[element]=true}mutatingfuncremove(element:T)->Bool{ifself.contains(element){self.dictionary.removeValueForKey(element)returntrue}else{returnfalse}}}

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
    class func convertFromArrayLiteral(elements: Element...) -> Self
}

Extending Set to adopt this protocol is relatively straightforward:

extensionSet:ArrayLiteralConvertible{staticfuncconvertFromArrayLiteral(elements:T...)->Set<T>{varset=Set<T>()forelementinelements{set.put(element)}returnset}}

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

letset: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:

extensionNSURL:StringLiteralConvertible{publicclassfuncconvertFromExtendedGraphemeClusterLiteral(value:String)->Self{returnself(string:value)}publicclassfuncconvertFromStringLiteral(value:String)->Self{returnself(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.


Viewing all articles
Browse latest Browse all 382

Trending Articles