原文链接:Swift中的静态和动态可调用类型
上周,苹果发布了 Xcode 11.4的第一个测试版,事实证明这是最近记忆中最重要的更新之一。XCT est获得了**巨大的提升,生活质量得到了许多提高,模拟器**也同样获得了大量的薄层色谱。但最受关注的是Swift的变化。
在Xcode 11.4中,Swift编译时间全面下降,许多开发人员报告他们的项目改进了10-20%。由于新的诊断体系结构[1],来自编译器的错误消息总是更有帮助。这也是Xcode的第一个版本,与新的source kit**-lsp**服务器一起发布,它可以让像VS **Code**这样的编辑以更有意义的方式与Swift合作。
然而,尽管有所有这些改进(这确实是苹果开发工具团队令人难以置信的成就),早期的大部分反馈都集中在Swift 5.2最明显的增加上。Twitter、Hacker News和Reddit等小媒体的反应——说得好听一点——是*“*喜忧参半”。
如果像我们大多数人一样,你不适应Swift Evolution[2的来来往往,Xcode 11.4是你第一次接触到该语言的两个新添加:键路径表达式作为函数[3]和用户定义标称类型[4]的可调用值。
其中第一个允许关键路径替换map等函数使用的一次性闭包:
// Swift >= 5.2"🧁🍭🍦".unicodeScalars.map(\.properties.name)// ["CUPCAKE", "LOLLIPOP", "SOFT ICE CREAM"]// Swift <5.2 equivalent"🧁🍭🍦".unicodeScalars.map { $0.properties.name }
第二种方法允许调用具有名为call As Function的方法的类型的实例,就好像它们是函数一样:
struct Sweetener { let additives: Set<Character> init<S>(_ sequence: S) where S: Sequence, S.Element == Character { self.additives = Set(sequence) } func callAsFunction(_ message: String) -> String { message.split(separator: " ") .flatMap { [$0, "\(additives.randomElement()!)"] } .joined(separator: " ") + "😋" }}let dessertify = Sweetener("🧁🍭🍦")dessertify("Hello, world!")// "Hello, 🍭 world! 🍦😋"
诚然,这两个例子都很糟糕。这就是问题所在。
《Swift的新动态》的报道往往只不过是对Swift Evolution提案的反复无常,其间穿插了动机不佳(而且往往充满表情符号)的例子。这种处理方法对Swift语言特征的描述很差,而且——以Swift 5.2为例——助长了流行的批评,即这些是轻率的添加——仅仅是***句法***上的糖。
本周,我们希望通过提供一些历史和理论背景来理解这些新特征,从而触及这个问题的核心。
Swift中的语法糖
如果你对“关键路径作为功能”过于甜蜜感到不满,请记住,现状并非没有甜食。考虑我们之前的糖精例子:
"🧁🍭🍦".unicodeScalars.map { $0.properties.name }
这个表达依赖于至少四种不同的句法让步:
-
尾随闭包语法,允许省略函数的最终闭包参数标签
-
匿名闭包参数,它允许闭包中的参数在不绑定到命名变量的情况下按位置(1, …) 使用。
-
推断参数和返回值类型
-
单表达式闭包的隐式返回
如果你想完全减少饮食中的糖,你最好让梅维斯·比肯·**[在线,因为你会做更多的打字**工作。
"🧁🍭🍦".unicodeScalars.map(transform: { (unicodeScalar: Unicode.Scalar) -> String in return unicodeScalar.properties.name})
事实上,正如我们将在接下来的例子中看到的,从语法上来说,斯威夫特是冬天的棉花糖世界。从初始化器和方法调用到选项和方法链接,几乎所有关于Swift的东西都可以被描述为棉花糖旋律——这实际上取决于你在“语言特征”和“句法糖”之间划出的界限。
要理解为什么,你必须首先理解我们是如何来到这里的,这需要一点历史、数学和计算机科学。准备🥦吃蔬菜。
λ-微积分与思辨性计算机科幻
所有的编程语言都可以看作是表示**λ**演算的各种尝试。编写代码所需的一切——变量、绑定、应用程序——都在里面,埋在大量希腊字母和数学符号下面。
撇开句法差异不谈,每种编程语言都可以通过它的组合来理解,这些组合使程序更容易编写和阅读。语言特征,如对象、类、模块、选项、文字和泛型,都只是建立在λ演算之上的抽象。
任何其他对纯数学形式主义的偏离都可以归因于现实世界的约束,例如19世纪70年代的打字机[6],[20世纪20年代的穿孔卡片,20世纪40年代的计算机架构7],或者20世纪****60年代的字符编码。
最早的编程语言有Lisp、ALGOL*和COBOL,几乎所有其他语言都是从这些语言派生出来的。
(defun square (x) (* x x))(print (square 4)) ;; 16
在这里,你可以瞥见三个非常不同的时间表;我们的现实是ALGOL的语法(选项2)在替代方案中“胜出”。从ALGOL 60开始,你可以从1963年的**C****PL到1967年的BCPL和1972年的C画一条直线,然后是1984年的目标C**和2014年的斯威夫特。这就是告诉我们什么类型是可调用的,以及我们如何调用它们的沿袭。
现在,回到斯威夫特…
Swift中的函数类型
函数是Swift中的第一类对象,这意味着它们可以分配给变量,存储在属性中,作为参数传递或作为其他函数的值返回。
函数类型与其他值的区别在于它们是可调用的,这意味着您可以调用它们来产生新值。
关闭
Swift的基本功能类型是闭包,这是一个独立的功能单元。
let square: (Int) -> Int = { x in x * x }
square(4) // 16
之所以称为闭包,是因为它们关闭并捕获对定义它们的上下文中的任何变量的引用。然而,捕获语义学并不总是可取的,这就是为什么Swift为一种特殊的闭包提供专用语法,称为函数。
函数
在顶级/全局范围中定义的函数被命名为闭包,不捕获任何值。在Swift中,使用func关键字声明它们:
func square(_ x: Int) -> Int { x * x }square(4) // 16
与闭包相比,函数在传递参数方面具有更大的灵活性。
函数参数可以有命名标签,而不是闭包的未标记位置参数——这大大有助于澄清代码在其调用站点的效果:
func deposit(amount: Decimal, from source: Account, to destination: Account) throws { … }try deposit(amount: 1000.00, from: checking, to: savings)
函数可以是泛型**[8]**,允许它们用于多种类型的参数:
func square<T: Numeric>(_ x: T) -> T { x * x }func increment<T: Numeric>(_ x: T) -> T { x + 1 }func compose<T>(_ f: @escaping (T) -> T, _ g: @escaping (T) -> T) -> (T) -> T { { x in g(f(x)) }}compose(increment, square)(4 as Int) // 25 ((4 + 1)²)compose(increment, square)(4.2 as Double) // 27.04 ((4.2 + 1)²)
函数还可以接受变量参数、隐式闭包和默认参数值(允许像#file和#line这样的神奇表达式字面值):
func print(items: Any...) { … }func assert(_ condition: @autoclosure () -> Bool, _ message: @autoclosure () -> String = String(), file: StaticString = #file, line: UInt = #line) { … }
然而,尽管接受参数有这么多灵活性,但你会遇到的大多数函数都是基于隐含的自我参数运行的。这些函数称为方法。
方法学
方法是由类型包含的函数。方法自动提供对自我的访问,允许它们有效地捕获作为隐式参数调用它们的实例。
struct Queue<Element> { private var elements: [Element] = [] mutating func push(_ newElement: Element) { self.elements.append(newElement) } mutating func pop() -> Element? { guard !self.elements.isEmpty else { return nil } return self.elements.removeFirst() }}
把所有的东西放在一起,这些语法提供允许Swift代码具有表现力、清晰和简洁:
var queue = Queue<Int>()queue.push(1)queue.push(2)queue.pop() // 1
与更冗长的语言,如目标C相比,编写Swift的经验是非常甜蜜的。很难想象Swift的开发人员会反对我们这里的“糖衣”。
但是就像一罐16盎司的**浪涌[9]一样,**某样东西的含糖量往往令人惊讶。事实证明,以前的例子远非无辜:
var queue = Queue<Int>() // desugars to `Queue<Int>.init()`queue.push(1) // desugars to `Queue.push(&queue)(1)`
一直以来,我们对方法和初始化器的所谓“直接”调用实际上是**函数咖喱****部分应用函数**的简写。
考虑到这一点,现在让我们更普遍地再看看Swift中的可调用类型。
{类型,实例,成员} ⨯ {静态,动态}
自从它们分别在Swift 4.2和Swift 5中被引入以来,许多开发人员很难在头脑中保持“动态成员查找”和“动态可调用”——由于在Swift 5.2中引入了call AsFunction,这变得更加困难。
如果您也感到困惑,我们认为下表可以帮助澄清问题:
StaticDynamicTypeinitN/AInstancecallAsFunction@dynamicCallableMemberfunc@dynamicMemberLookup
Swift始终具有静态可调用类型和类型成员。新版本Swift的变化是实例现在是可调用的,实例和成员现在都可以动态调用。
让我们看看这在实践中意味着什么,从静态可调用项开始。
静态可调用
struct Static { init() {} func callAsFunction() {} static func function() {} func function() {}}
可以通过以下方式静态调用此类型:
let instance = Static() // ❶ desugars to `Static.init()`Static.function() // ❷ (no syntactic sugar!)instance.function() // ❸ desugars to Static.function(instance)()instance() // ❹ desugars to `Static.callAsFunction(instance)()`
❶
调用静态类型调用初始化器
❷
对静态类型调用函数调用相应的静态函数成员,将静态作为隐式自参数传递。
❸
对Static实例调用函数会调用相应的函数成员,将该实例作为隐式自参数传递。
❹
调用Static的实例调用call As Function()函数成员,将该实例作为隐式自参数传递。
动态可调用
@dynamicCallable@dynamicMemberLookupstruct Dynamic { func dynamicallyCall(withArguments args: [Int]) -> Void { () } func dynamicallyCall(withKeywordArguments args: KeyValuePairs<String, Int>) -> Void { () } static subscript(dynamicMember member: String) -> (Int) -> Void { { _ in } } subscript(dynamicMember member: String) -> (Int) -> Void { { _ in } }}
可以通过几种不同的方式动态调用此类型:
let instance = 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)`
❶
调用Dynamic的实例调用Dynamic Call(with Arguments:)方法,传递参数数组和Dynamic作为隐式自参数。
❷
调用具有至少一个标记参数的Dynamic实例将调用Dynamic Call(with Keyword Arguments:)方法,在**Key Value Pair对象**和Dynamic中传递参数作为隐式自参数。
❸
在Dynamic类型上调用函数调用静态动态成员下标,传递函数作为键;在这里,我们调用返回的匿名闭包。
❹
在Dynamic的实例上调用函数,调用动态成员下标,传递函数作为键;在这里,我们调用返回的匿名闭包。
声明属性的动态性
@动态调用和@动态成员查找是声明属性,这意味着它们不能通过扩展应用于现有声明。
例如,你不能用类似Ruby的自然**语言访问器给Int增添趣味:
@dynamicMemberLookup // ⚠︎ Error: '@dynamicMemberLookup' attribute cannot be applied to this declarationextension Int { static subscript(dynamicMember member: String) -> Int? { let string = member.replacingOccurrences(of: "_", with: "-") let formatter = NumberFormatter() formatter.numberStyle = .spellOut return formatter.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)
将此与call As Function进行对比,后者可以添加到扩展中的任何类型。
关于“动态成员查找”、“动态可调用”和“调用函数”还有很多要讨论的,我们期待在未来的文章中更详细地介绍它们。
但是说到RubyPython…
斯威夫特 ⨯ ****___
代码就像同人小说。
有时候,要发布软件,你需要配对并“发布”不同的技术。
在构建这些功能时,“未来的力量”已经注定**Swift将取代Python用于机器学习。想当然地认为增量方法是最好的,实现这一点的方法是允许Swift与Python无缝互操作,就像它与目标C一样。自从Swift 4.2以来,我们已经非常接近**了。
import Pythonlet numpy = Python.import("numpy")let zeros = numpy.ones([2, 4])/* [[1, 1, 1, 1] [1, 1, 1, 1]] */
动态的外部性
附加变化的承诺是,如果你不想让它们改变任何东西,它们不会改变任何东西。您可以继续编写Swift代码,完全不了解本文中描述的功能(到目前为止,我们大多数人都知道)。但让我们明确一点:没有免费的抽象。
经济学使用**负外部性**一词来描述决策产生的间接成本。尽管除非你使用这些功能,否则你不会为它们付费,但我们都肩负着一种更复杂的语言的负担,这种语言更难教授、学习、记录和推理。
我们很多从一开始就和斯威夫特在一起的人已经厌倦了斯威夫特进化。对于那些在外面观察的人来说,我们把时间浪费在像这样无关紧要的“糖”上,而不是像***异步/等待***的那样真正移动指针的功能,这是深不可测的。
孤立地说,这些提议都是深思熟虑和有用的*——真的。我们已经****有机会****使用其中的一些。*但是,当事物沉浸在情感包袱中时,很难根据它们自己的技术优点来判断它们。
每个人都有自己的糖耐量,这通常是由他们习惯的东西决定的。意识到**吊桥效应,我真的不知道我是失去了联系,还是孩子**们错了…
参考资料
[1]Swift.org-新诊断架构概述:swift.org/blog/new-di…
[2]Swift Evolution Proposals仪表板:apple.github.io/swift-evolu…
[3]SE-0249:关键路径表达式作为函数:github.com/apple/swift…
[4]SE-0253:用户定义标称类型的可调用值:github.com/apple/swift…
[5]Mavis Beacon教授打字:en.wikipedia.org/wiki/Mavis_…
QWERTY:en.wikipedia.org/wiki/QWERTY
[7]冯·诺依曼Architecture:en.wikipedia.org/wiki/Von_Ne…
Swift编程语言docs.swift.org/swift-book/…