由于 Core Data 的复杂性,以及不支持 Immutable 的特性,对 SwiftUI 和 TCA 不够友好,直接将 Core Data Model 用于上层的 State 不是一个很好的选择。
一种方式是为每一个 Core Data Model 创建一个自己的 struct 类型的 Domain Model,手动的进行映射(Mapping)。但这种方式的缺点显然易见,需要写很多冗余代码,并且容易出错。
Core Data 本身就是 Obj-C 语言的 ORM,但却不是很好的 Swift ORM。Prisma 通过对 Core Data 进一步的抽象,提供一套新的 ORM 模型(主要也是利用了 Core Data 的运行时特性),Immutable, Type-safe,同时不损失大部分 Core Data 的优点。对比起手动 Mapping 也更简单。
TCA + Core Data 这个组合由于历史原因(所以才有了 SwiftData)没法天然的很好配合使用,比如最重要的 Data Flow 状态管理问题。还有一些问题是由于不直接使用 Core Data 而引入的新问题,有一些是 SwiftUI + Core Data 本身就有的问题或者说 Core Data 本身的问题。
因为 Core Data 是支持 Relationship 的,所以理想的情况 Core Data 的数据结构 Entity 也是一棵树。这样是不是能通过监听树根,来知道一整颗树的所有的变化呢?然后把 Core Data 的数据结构和 TCA 的数据结构进行对应,这样就能实现 Core Data 的数据变化,直接映射到 TCA 的 State 上了。
但理想归理想,现实情况是,首先 Core Data 并不能监听到 Relationship 对象的变化,只能监听到自己的属性的变化,所以这个方案行不通[^1];其次,TCA 践行的是 Immutable 的 State,所有的 State 的变化,都应该是可以有迹可循的,不同 Reducer 之间的 State 的变化,是主动的通过 Action 来进行更新的,而不是通过监听的方式。举例来说就是 Child 页面是某一个时刻从 Parent 中通过 Scope 来拆分出来的,是一个 Immutable 的 State,所以 Parent 页面的 State 的变化,是不会影响到 Child 页面的 State 的,也就是在根 Reducer 监听 Core Data 树的变化,没有办法实时更新到所有的子节点的 State 上,也就没法实时刷新页面。
Swift 中的 Value Type 也支持 KeyPath,但是和 Core Data 的 KeyPath 不是同一个东西,并且没有办法直接转换,因为 Swift struct 的 KeyPath 是静态的类型安全的,而 Core Data 的 KeyPath 是动态的利用了 NSObject 的运行时特性的。所以在设计接口的时候没法直接使用 KeyPath 来作为 predicate 的参数,这样会缺失很多便捷性。
Performance
Core Data 有很多的性能优化,上述的方案会导致一部分的性能优化不起作用,比如 Fault 特性。懒加载在大量数据的时候对性能的提升还是很有帮助的。除此之外,包括使用 fetchLimit 来实现分页加载;通过 predicate 来过滤非必要的数据,不要一次把所有的对象查询出来。
为什么需要引进一个新的关键词 some,不能直接写 var body: View { ... }?原因是 View 这个协议是经典的 PAT,对就是那个万恶之源 PAT 。
1 2 3 4
publicprotocolView { associatedtypeBody : View var body: Self.Body { get } }
对于 PAT 不能直接的把 View 当作一个类型来使用,Protocol ‘View’ can only be used as generic constraint because it has Self or associated type requirements。View 这个 PAT 没法自动生成一个 Existential,所以不能直接写 var body: View { ... },必须明确指定 View 的具体类型。
1 2 3 4 5 6
structContentView: View { // typealias Body = Text var body: Text { Text("Hello World") } }
明确 Body 的具体类型,有助于揭示 ContentView 的部分实现,同时也使得声明变得脆弱。如果想改变 body 的返回类型,必须同时修改对应的类型。在这个场景中,具体的 body 返回类型其实并不重要,重要的是它符合 View 协议,它是一个 View。这时候如果想要抽象出声明的返回值类型,就必须要考虑 existential 或者 type erasure。
基于这一点,Swift 5.1 中引入了 SE-0244 opaque result types 这一特性。some Protocol 表示一个确定的实现了 Protocol 协议的类型。同时它还有一个要求,就是 some 修饰 return type 的时候,要求所有的 return 语句返回相同的具体类型。
1 2 3 4 5 6 7 8
protocolP { } extensionInt : P { } extensionString : P { }
funcfoo(flip: Bool) -> someP { if flip { return17 } return"a string"// error: different return types Int and String }
在一个支持范型的语言中,想使用 PAT 又不需要指定具体的类型对于编写简洁代码非常重要。考虑一下嵌套的范型,如 SwiftUI 中那些 View 的 body 的真实类型。
Swift 中想要把一个 Protocol 作为一个类型来用,要求这个 Protocol 必须不能是 PAT,否则的话就会报错 Protocol can only be used as generic constraint because it has Self or associated type requirements。为了缓解这个问题,Swift 引入了 SE-0309 unlock existential types for all protocols,compiler 帮忙自动的进行 type erasure。
1 2 3 4 5 6 7
protocolLogging: Hashtable { }
// before // -func add(_ logger: AnyLogger) { // after, 🙂️ OK funcadd(_logger: Logging) { }
到这里,PAT 的使用成本大大的降低,实用性大大提升。终于可以像使用范型那样的使用 PAT 了。
1 2 3 4 5
funcadd(_logger: Logging) { ... } // existential type funcadd<T: Logging>(_logger: T) { ... } // generic type
let logger: MemoryLogger add(logger)
但是 generic type 和 existential type 始终是不同的东西,前者是 type-level abstraction 而后者是 value-level abstraction,前者强调的是类型以及类型之间的关系,后者关心的是值,类型信息被 compiler 帮忙抹除掉。
funcbar<T: Foo>(_foo: T) {} // This requires a concrete T that conforms to Foo funcbaz(_foo: Foo) {} // This requires a variable of type Foo (pedantically: "a Foo existential")
let foo: Foo=...// existential of protocol `Foo` bar(foo) // 😢 Protocol type 'Foo' cannot conform to 'Foo' because only concrete types can conform to protocols baz(foo) // 😊
所以当看到 “a protocol doesn’t conform to itself” 的时候,它实际上是指 “the existential of a protocol doesn’t conform to that protocol”。
funcbar<T: Foo>(_foo: T) {} let foo: Foo=... bar(foo) // Protocol type 'Foo' cannot conform to 'Foo' because only concrete types can conform to protocols
funcdecode(_type: Decodable.Type) { let decodable =JSONDecoder().decode(type, from: data) // let decodable = JSONDecoder().decode(Decodable.self, from: data) // Protocol type 'Decodable' cannot conform to 'Decodable' because only concrete types can conform to protocols } // 最终还是那个 `Protocol type 'Decodable' cannot conform to 'Decodable' because only concrete types can conform to protocols`
最近关于 [Add Result to the Standard Library](swift-evolution/0235-add-result.md at master · apple/swift-evolution · GitHub) 的提案正在激烈的[讨论中](SE-0235 - Add Result to the Standard Library - Proposal Reviews - Swift Forums),讨论的内容从命名到异步错误处理,再到是否应该有一个 Either 类型等等。
Result
对于在项目中使用过 Swift 的人来说,Result 类型应该再熟悉不过了,在 community 中有着非常广泛的应用。最早看到对于 Result 的应用是在Alamofire 中,然后是有订阅的博客开始介绍 Result 是如何帮忙解决非必要的错误值/可选值检查,明确的区分成功和失败。再后来就是大家都开始在项目中使用 Result 类型来进行 异步错误处理。
为什么说 异步错误处理 呢,因为最早接触到 Result 这个类型的使用案例,就是用来处理异步错误的,并且它非常的适合,如果不从语言设计上考虑的话,它可以说是非常完美,因为它和 Optional 一样是个 Monad。虽然 Result 只是一个非常简单的数据结构,它和同步异步一点关系都没有,它只跟错误处理有关。
extensionAlligatorwhereBase: AVAsset { /// Merge the given video asset and audio asset /// /// - Parameters: /// - videoAsset: the given video asset /// - audioAsset: the given audio asset /// - Returns: the merged asset /// - Throws: throws error when the given asset is invalid. e.g. video asset without video tracks. publicstaticfuncmerge(videoAsset: AVAsset, audioAsset: AVAsset) throws -> AVAsset { let mixComposition =AVMutableComposition() try mixComposition.agt.add(.video, from: videoAsset)
let videoDuration = mixComposition.duration try mixComposition.agt.add(.audio, from: audioAsset, maxBounds: videoDuration)
return mixComposition }
/// Merge the given assets one by one /// /// - Parameters: /// - segments: given assets, it can't be empty /// - isMuted: if true, it will passthrough audio tracks /// - Returns: merged asset /// - Throws: throws error when segments is empry, or some segment is invalid. publicstaticfuncmerge(segments: [AVAsset], isMuted: Bool) throws -> AVAsset { guard!segments.isEmpty else { throwError.segmentsEmpty }
structFormatter { /// Format a string, replace invalid symbol with empty character. If it is empty or contains `@`, `#`, return nil. /// /// - Parameter string: string needs to be format /// - Returns: formatted string funcformat(_string: String) -> String? { guard string.isEmpty else { thrownil }
structFormatter { enumError: Swift.Error { case emptyString case containsHashtag case containsMention }
/// Format a string, replace invalid symbol with empty character /// /// - Parameter string: string needs to be format /// - Returns: formatted string funcformat(_string: String) throws -> String { guard string.isEmpty else { throwError.emptyString }
publicenumLevel: Int { case off =0 case error =1// Flag.error | Level.off case warning =3// Flag.warning | Level.error case info =7// Flag.info | Level.warning case debug =15// Flag.debug | Level.info }
publicstructFlag: OptionSet { publiclet rawValue: Int
Protocols don’t actually implement any functionality themselves. Nonetheless, any protocol you create will become a fully-fledged type for use in your code. Because it’s a type, you can use a protocol in many places where other types are allowed, including:
As a parameter type or return type in a function, method, or initializer
As the type of a constant, variable, or property
As the type of items in an array, dictionary, or other container
publicprotocolIteratorProtocol { /// The type of element traversed by the iterator. associatedtypeElement
/// - Returns: The next element in the underlying sequence, if a next element /// exists; otherwise, `nil`. mutatingfuncnext() -> Element? }
publicprotocolSequence { /// A type representing the sequence's elements. associatedtypeElement
/// A type that provides the sequence's iteration interface and /// encapsulates its iteration state. associatedtypeIterator : IteratorProtocolwhereIterator.Element==Element
/// Returns an iterator over the elements of this sequence. funcmakeIterator() -> Iterator }
/// The plist data model publicenumPLIST { /// <true/> or <false/> case bool(Bool) /// 2017-08-05T14:25:14Z case date(Date) /// <data>VGVzdFZhbHVl</data> (<54657374 56616c75 65> case data(Data) /// <integer>233</integer> or <real>2.33</real> case number(Int) /// <string>string</string> case string(String) /// <array><string>The String</string></array> indirectcase array([PLIST]) /// <dict><key>The Key</key><string>The String</string></dict> indirectcase dict([String: PLIST]) }
Parser
根据 Plist data model,想要解析一个 Plist 字符串 得到 PLIST 类型,只需要一个 parser。
没错,只需要一个 parser,这个 parser 大概长这样:
1 2
let parser: Parser<PLIST> let result = parser.parse("plist")
let _true = string("<true/>") <&> const(PLIST.bool(true)) let _false = string("<false/>") <&> const(PLIST.bool(false))
let _bool = _true <|> _false _bool.parse("<false/>")
Date Parser
Plist 中的 Date 类型存储的是 UTC 字符串,如 <date>2017-08-05T14:25:14Z</date>。字符串中的开始标签 <date> 和结束标签 </date> 对于解析的结果来说是没有用的,所以一个 Date 类型的 parser 是要将这个字符串解析成 PLIST.date(date), date 为 2017-08-05T14:25:14Z 通过 format 得到。
functmap<T>(x: T) -> F<T> funcfmap<A, B>(f: A -> B) -> (F<A> -> F<B>)
简化一下变成:
1 2 3 4 5
// Swift 中把 Int 映射到 Array<Int> 由 Array 的初始化方法提供, // 所以可以不写。 // 由于 fmap 实际上是 (F<A>, A -> B) -> F<B> 的 currying 版本, // 所以两者是等价的。 funcmap<A, B>(x: F<A>, f: A -> B) -> F<B>
再来看看 Swift 中的 Array 和 Optional。如果把 Swift 中所有的类型 A, B 当作对象,以及 Swift 中所有的函数当作态射 A -> B,那么这些类型和函数就组成一个范畴 A。把 Array 类型当作对象 Array<A>, Array<B>,Array 上所有的函数当作态射 Array<A> -> Array<B>,那么也组成一个范畴 B。而 A 到 B 之间的函子是 Array,因为函子 Array 能将任意类型 T 转换为 Array<T>。Optional 同理。
funcparser(_string: String) -> (Int, String)? { guardlet head = string.characters.first, head =="4"else { returnnil } returnOptional.some((4, String(string.characters.dropFirst()))) }
Combinator
One of the distinguishing features of functional programming is the widespread use of combinators to construct programs. A combinator is a function which builds program fragments from program fragments; in a sense the programmer using combinators constructs much of the desired program automatically, rather that writing every detail by hand. – John Hughes
所以我们只需要给 satisfy 函数传入一个是否属于 X 的函数,就可以得到一个能够解析 x 的解析器。
Next
最基本的 character 有了,digit 有了,当我们需要解析一个字符串 alex 的时候,我们只需要把 alex 看成 alex 4 个字符,然后不断的用 character 进行解析,最后把每一步返回的结果合并起来就行了。考虑到解析一个字符串是一个基本功能,为了不用每次写重复的代码,把它封装成用来解析 string 的解析器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
funcstring(_str: String) -> Parser<String> { let parsers = str.characters.map { character($0) } returnParser { input in var results: [Character] = [] var stream = input for parser in parsers { guardlet (result, remainder) = parser.parse(stream) { returnnil } results.append(result) stream = remainder } return (String(results), stream) } }
因为不是任何泛型 A 类型,都能用 String 拍扁,也不一定能通过其他类型进行拍扁,所以这里把泛型 A 去掉,直接用 Character 代替。但是这样做并不理想,因为 many 解析器从一个泛型解析器,变成了一个只能解析 Character 类型的解析器,变成了 manyCharacter。后面考虑解析这个问题,重新把 many 变成通用的解析器。
Alternative 类似于 Swift Standard Library 中定义的运算符 ??,它有两个同类型的参数,第一个参数是偏爱的 parser,第二个参数是默认的 parser。它首先尝试使用第一个 parser 来进行解析,如果成功,则返回。如果不成功,则使用默认的 parser 进行解析。它的返回值类型也是同类型的 Parser。