Movatterモバイル変換


[0]ホーム

URL:


Static and Dynamic Callable Types in Swift

Written byMattt

Last week, Apple released thefirst beta of Xcode 11.4, and it’s proving to be one of the most substantial updates in recent memory.XCTest gota huge boost, with numerous quality of life improvements, andSimulator, likewise, got a solid dose ofTLC. But it’s the changes to Swift that are getting the lion’s share of attention.

In Xcode 11.4, Swift compile times are down across the board, with many developers reporting improvements of 10 – 20% in their projects. And thanks to anew diagnostics architecture, error messages from the compiler are consistently more helpful. This is also the first version of Xcode to ship with the newsourcekit-lsp server, which serves to empower editors likeVSCode to work with Swift in a more meaningful way.

Yet, despite all of these improvements (which are truly an incredible achievement by Apple’s Developer Tools team), much of the early feedback has focused on the most visible additions to Swift 5.2. And the response from the peanut galleries of Twitter, Hacker News, and Reddit has been — to put it charitably —“mixed”.


If like most of us, you aren’t tuned into the comings-and-goings ofSwift Evolution, Xcode 11.4 was your first exposure to two new additions to the language:key path expressions as functions andcallable values of user-defined nominal types.

The first of these allows key paths to replace one-off closures used by functions likemap:

// Swift >= 5.2"🧁🍭🍦".unicodeScalars.map(\.properties.name)// ["CUPCAKE", "LOLLIPOP", "SOFT ICE CREAM"]// Swift <5.2 equivalent"🧁🍭🍦".unicodeScalars.map{$0.properties.name}

The second allows instances of types with a method namedcallAsFunction to be called as if they were a function:

structSweetener{letadditives:Set<Character>init<S>(_sequence:S)whereS:Sequence,S.Element==Character{self.additives=Set(sequence)}funccallAsFunction(_message:String)->String{message.split(separator:" ").flatMap{[$0,"\(additives.randomElement()!)"]}.joined(separator:" ")+"😋"}}letdessertify=Sweetener("🧁🍭🍦")dessertify("Hello, world!")// "Hello, 🍭 world! 🍦😋"

Granted, both of those examples are terrible. And that’s kinda the problem.


Too often, coverage of“What’s New In Swift” amounts to little more than a regurgitation of Swift Evolution proposals, interspersed with poorly motivated (and often emoji-laden) examples. Such treatments provide a poor characterization of Swift language features, and — in the case of Swift 5.2 — serves to feed into the popular critique that these are frivolous additions — meresyntactic sugar.

To the extent that we’ve been guilty of that… our bad🙇‍♂️.

This week, we hope to reach the ooey gooey center of the issue by providing some historical and theoretical context for understanding these new features.

Syntactic Sugar in Swift

If you’re salty about “key path as function” being too sugary, recall that thestatus quo isn’t without a sweet tooth. Consider our saccharine example from before:

"🧁🍭🍦".unicodeScalars.map{$0.properties.name}

That expression relies on at least four different syntactic concessions:

  1. Trailing closure syntax, which allows a final closure argument label of a function to be omitted
  2. Anonymous closure arguments, which allow arguments in closures to be used positionally ($0,$1, …) without binding to a named variable.
  3. Inferred parameter and return value types
  4. Implicit return from single-expression closures

If you wanted to cut sugar out of your diet completely, you’d best getMavis Beacon on the line, because you’ll be doing a lot moretyping.

"🧁🍭🍦".unicodeScalars.map(transform:{(unicodeScalar:Unicode.Scalar)->StringinreturnunicodeScalar.properties.name})

Also, who knew that the argument label inmap was “transform”?

In fact, as we’ll see in the examples to come, Swift is a marshmallow world in the winter,syntactically speaking. From initializers and method calls to optionals and method chaining, nearly everything about Swift could be described as a cotton candy melody — it really just depends on where you draw the line between “language feature” and “syntactic sugar”.


To understand why, you have to understand how we got here in the first place, which requires a bit of history, math, and computer science. Get ready to eat your vegetables 🥦.

The λ-Calculus and Speculative Computer Science Fiction

All programming languages can be seen as various attempts to representtheλ-calculus. Everything you need to write code — variables, binding, application — it’s all in there, buried under a mass of Greek letters and mathematical notation.

Setting aside syntactic differences, each programming language can be understood by its combination of affordances for making programs easier to write and easier to read. Language features like objects, classes, modules, optionals, literals, and generics are all just abstractions built on top of the λ-calculus.

Any other deviation from pure mathematical formalism can be ascribed to real-world constraints, such asa typewriter from the 1870s,a punch card from the 1920s,a computer architecture from the 1940s, ora character encoding from the 1960s.

Among the earliest programming languages were Lisp, ALGOL*, and COBOL, from which nearly every other language derives.

We’re using FORTRAN as a stand-in here, for lack of an easily-accessible ALGOL environment.

(defunsquare(x)(*xx))(print(square4));; 16

Here you get a glimpse into three very different timelines; ours is the reality in which ALGOL’s syntax (option #2) “won out” over the alternatives. From ALGOL 60, you can draw a straight line fromCPL in 1963, toBCPL in 1967 andC in 1972, followed byObjective-C in 1984 and Swift in 2014. That’s the lineage that informs what types are callable and how we call them.


Now, back to Swift…

Function Types in Swift

Functions are first-class objects in Swift, meaning that they can be assigned to variables, stored in properties, and passed as arguments or returned as values from other functions.

What distinguishes function types from other values is that they’recallable, meaning that you can invoke them to produce new values.

Closures

Swift’s fundamental function type is theclosure, a self-contained unit of functionality.

letsquare:(Int)->Int={xinx*x}

As a function type, you can call a closure by passing the requisite number of arguments between opening and closing parentheses()a la ALGOL.

square(4)// 16

The number of arguments taken by a function type is known as itsarity.

Closures are so called because theyclose over and capture references to any variables from the context in which they’re defined. However, capturing semantics aren’t always desirable, which is why Swift provides dedicated syntax to a special kind of closure known as afunction.

Functions

Functions defined at a top-level / global scope are named closures that don’t capture any values. In Swift, you declare them with thefunc keyword:

funcsquare(_x:Int)->Int{x*x}square(4)// 16

Compared to closures, functions have greater flexibility in how arguments are passed.

Function arguments can have named labels instead of a closure’s unlabeled, positional arguments — which goes a long way to clarify the effect of code at its call site:

funcdeposit(amount:Decimal,fromsource:Account,todestination:Account)throws{}trydeposit(amount:1000.00,from:checking,to:savings)

Functions can begeneric, allowing them to be used for multiple types of arguments:

funcsquare<T:Numeric>(_x:T)->T{x*x}funcincrement<T:Numeric>(_x:T)->T{x+1}funccompose<T>(_f:@escaping(T)->T,_g:@escaping(T)->T)->(T)->T{{xing(f(x))}}compose(increment,square)(4asInt)// 25 ((4 + 1)²)compose(increment,square)(4.2asDouble)// 27.04 ((4.2 + 1)²)

Functions can also take variadic arguments, implicit closures, and default argument values (allowing for magic expression literals like#file and#line):

funcprint(items:Any...){}funcassert(_condition:@autoclosure()->Bool,_message:@autoclosure()->String=String(),file:StaticString=#file,line:UInt=#line){}

And yet, despite all of this flexibility for accepting arguments, most functions you’ll encounter operate on animplicitself argument. These functions are called methods.

Methods

Amethod is a function contained by a type. Methods automatically provide access toself, allowing them to effectively capture the instance on which they’re called as an implicit argument.

structQueue<Element>{privatevarelements:[Element]=[]mutatingfuncpush(_newElement:Element){self.elements.append(newElement)}mutatingfuncpop()->Element?{guard!self.elements.isEmptyelse{returnnil}returnself.elements.removeFirst()}}

Swift goes one step further by allowingself. to be omitted for member access — making the already implicitself all the more implicit.


Putting everything together, these syntactic affordances allow Swift code to be expressive, clear, and concise:

varqueue=Queue<Int>()queue.push(1)queue.push(2)queue.pop()// 1

Compared to more verbose languages like Objective-C, the experience of writing Swift is, well, prettysweet. It’s hard to imagine any Swift developers objecting to what we have here as being “sugar-coated”.

But like a 16oz can ofSurge, the sugar content of something is often surprising. Turns out, that example from before is far from innocent:

varqueue=Queue<Int>()// desugars to `Queue<Int>.init()`queue.push(1)// desugars to `Queue.push(&queue)(1)`

All this time, our so-called “direct” calls to methods and initializers were actually shorthand forfunction curryingpartially-applied functions.

Partial application and currying are often conflated. In fact,they’re distinct but related concepts.

Early versions of Swift had a dedicated syntax for currying functions, but it proved less useful than originally anticipated and was removed by thesecond-ever Swift Evolution proposal.

// Swift <3:funccurried(x:Int)(y:String)->Float{returnFloat(x)+Float(y)!}// Swift >=3funccurried(x:Int)->(String)->Float{return{(y:String)->FloatinreturnFloat(x)+Float(y)!}}

With this in mind, let’s now take another look at callable types in Swift more generally.

{Type, Instance, Member} ⨯ {Static, Dynamic}

Since their introduction in Swift 4.2 and Swift 5, respectively, many developers have had a hard time keeping@dynamicMemberLookup and@dynamicCallable straight in their minds — made even more difficult by the introduction ofcallAsFunction in Swift 5.2.

If you’re also confused, we think the following table can help clear things up:

 StaticDynamic
TypeinitN/A
InstancecallAsFunction@dynamicCallable
Memberfunc@dynamicMemberLookup

Swift has always had static callable types and type members. What’s changed in new versions of Swift is that instances are now callable, and both instances and members can now be called dynamically.

You might have noticed the blank spot in our table. Indeed, there’s no way to dynamically call types. In fact, there’s no way to statically call types other than to invoke initializers — and that’s probably for the best.

Let’s see what that means in practice, starting with static callables.

Static Callable

structStatic{init(){}funccallAsFunction(){}staticfuncfunction(){}funcfunction(){}}

This type can be called statically in the following ways:

letinstance=Static()// desugars to `Static.init()`Static.function()// (no syntactic sugar!)instance.function()// desugars to Static.function(instance)()instance()// desugars to `Static.callAsFunction(instance)()`
Calling theStatic type invokes an initializer
Callingfunction on theStatic type invokes the corresponding static function member, passingStatic as an implicitself argument.
Callingfunction on an instance ofStatic invokes the corresponding function member, passing the instance as an implicitself argument.
Calling an instance ofStatic invokes thecallAsFunction() function member,passing the instance as an implicitself argument.

A few points for completeness’ sake:

  • You can also statically call subscripts and variable members (properties).
  • Operators provide an alternative way to invoke static member functions.
  • Enumeration cases are, well… something else entirely.

Dynamic Callable

@dynamicCallable@dynamicMemberLookupstructDynamic{funcdynamicallyCall(withArgumentsargs:[Int])->Void{()}funcdynamicallyCall(withKeywordArgumentsargs:KeyValuePairs<String,Int>)->Void{()}staticsubscript(dynamicMembermember:String)->(Int)->Void{{_in}}subscript(dynamicMembermember:String)->(Int)->Void{{_in}}}

This type can be called dynamically in a few different ways:

letinstance=Dynamic()// desugars to `Dynamic.init()`instance(1)// desugars to `Dynamic.dynamicallyCall(instance)(withArguments: [1])`instance(a:1)// desugars to `Dynamic.dynamicallyCall(instance)(withKeywordArguments: ["a": 1])`Dynamic.function(1)// desugars to `Dynamic[dynamicMember: "function"](1)`instance.function(1)// desugars to `instance[dynamicMember: "function"](1)`
Calling an instance ofDynamic invokes thedynamicallyCall(withArguments:) method,passing an array of argumentsandDynamic as an implicitself argument.
Calling an instance ofDynamic with at least one labeled argument invokes thedynamicallyCall(withKeywordArguments:) method,passing the arguments in aKeyValuePairs objectandDynamic as an implicitself argument.
Callingfunction on theDynamic type invokes the staticdynamicMember subscript,passing"function" as the key;here, we call the returned anonymous closure.
Callingfunction on an instance ofDynamic invokes thedynamicMember subscript,passing"function" as the key;here, we call the returned anonymous closure.

Dynamism by Declaration Attributes

@dynamicCallable and@dynamicMemberLookupare declaration attributes,which means that they can’t be applied to existing declarationsthrough an extension.

So you can’t, for example,spice upInt withRuby-ish natural language accessors:

@dynamicMemberLookup// ⚠︎ Error: '@dynamicMemberLookup' attribute cannot be applied to this declarationextensionInt{staticsubscript(dynamicMembermember:String)->Int?{letstring=member.replacingOccurrences(of:"_",with:"-")letformatter=NumberFormatter()formatter.numberStyle=.spellOutreturnformatter.number(from:string)?.intValue}}// ⚠︎ Error: Just to be super clear, this doesn't workInt.forty_two// 42 (hypothetically, if we could apply `@dynamicMemberLookup` in an extension)

Contrast this withcallAsFunction,which can be added to any type in an extension.

For more information about these new language features, check out the original Swift Evolution proposals:


There’s much more to talk about with@dynamicMemberLookup,@dynamicCallable, andcallAsFunction,and we look forward to covering them all in more detailin future articles.


But speaking ofRubyPython

Swift ⨯_______

Adding toour list of“What code is like”:

Code is like fan fiction.

Sometimes to ship software, you need to pair up and “ship” different technologies.

In a way, the story of Swift is one of the great, tragic romances in modern computing; how else might we describe the way Objective-C sacrificed itself to make Swift possible?

In building these features, the “powers that be” have ordained thatSwift replace Python for Machine Learning. Taking for granted that an incremental approach is best, the way to make that happen is to allow Swift to interoperate with Python as seamlessly as it does with Objective-C. And since Swift 4.2, we’ve beengetting pretty close.

importPythonletnumpy=Python.import("numpy")letzeros=numpy.ones([2,4])/* [[1, 1, 1, 1]    [1, 1, 1, 1]] */

The Externalities of Dynamism

The promise of additive changes is that they don’t change anything if you don’t want them to. You can continue to write Swift code remaining totally ignorant of the features described in this article (most of us have so far). But let’s be clear: there are no cost-free abstractions.

Economics uses the termnegative externalities to describe indirect costs incurred by a decision. Although you don’t pay for these features unless you use them, we all shoulder the burden of a more complex language that’s more difficult to teach, learn, document, and reason about.


A lot of us who have been with Swift from the beginning have grown weary of Swift Evolution. And for those on the outside looking in, it’s unfathomable that we’re wasting time on inconsequential “sugar” like this instead of features that willreally move the needle, likeasync /await.

In isolation, each of these proposals is thoughtful and useful —genuinely. We’ve alreadyhad occasion to use a few of them. But it can be really hard to judge things on their own technical merits when they’re steeped in emotional baggage.

Everyone has their own sugar tolerance, and it’s often informed by what they’re accustomed to. Being cognizant of thedrawbridge effect, I honestly can’t tell if I’m out of touch, or if it’sthe children who are wrong

NSMutableHipster

Questions? Corrections?Issues andpull requests are always welcome.

This article uses Swift version 5.2. Find status information for all articles on thestatus page.

Written byMattt
Mattt

Mattt (@mattt) is a writer and developer in Portland, Oregon.

Next Article

Software development best practices prescribe strict separation of configuration from code. Learn how you can usexcconfig files to make your Xcode projects more compact, comprehensible, and powerful.


[8]ページ先頭

©2009-2025 Movatter.jp