本文基于《函数式swift》中《解析器组合算子》章节拓展,实现了一个JSON解析器。
JSON定义
结构
json中包含string、int、double、bool、null五种基础类型以及array和object两种组合类型,我们定义一个枚举来描述JSON结构:
public enum JSON {
case object([String: JSON?]?)
case array([JSON?]?)
case string(String?)
case int(Int?)
case double(Double?)
case bool(Bool?)
case null
}
构造器
此处我们将所有的关联值都定义为optional类型,用以处理null的情况。接下来我们为JSON定义构造器:
extension JSON {
public init(_ value: Int?) { self = .int(value) }
public init(_ value: Double?) { self = .double(value) }
public init(_ value: String?) { self = .string(value) }
public init(_ value: Bool?) { self = .bool(value) }
public init(_ value: [JSON?]?) { self = .array(value) }
public init(_ value: [String: JSON?]?) { self = .object(value) }
}
JSON → JSONString 生成 JSON字符串
接下来我们简单实现一下 JSON 到 JSONString 的解析方法:
-
optional:首先我们要对其关联值解包,为optional写一个拓展方法:
extension Optional { func toJSONString(_ functor: (Wrapped) -> String) -> String { switch self { case .none: return JSON.null.JSONString case .some(let wrapped): return functor(wrapped) } } }在解包失败时,返回 null 的 JSONString,否则以 functor 对其解析。
-
string: 只需要在首尾拼接上
”。 -
bool、int、double:转化成 String 。
-
null:返回
null字符串。 -
array:首尾拼接
[ ],内部递归调用 JSONString:private func prettyArray(_ array: [JSON?]) -> String { "[" + array.map{ $0.toJSONString { $0.JSONString } }.joined(separator: ", ") + "]" } -
object:首尾拼接
{ },key 和 value 之间加上:,内部对 value 递归调用JSONString:private func prettyObject(_ object: [String: JSON?]) -> String { "{" + object.map { refString($0) + ": " + $1.toJSONString{ $0.JSONString } }.joined(separator: ", ") + "}" }
完整代码:
extension JSON {
public var JSONString: String {
switch self {
case let .string(v): return v.toJSONString { refString($0) }
case let .double(v): return v.toJSONString { "\($0)" }
case let .int(v): return v.toJSONString { "\($0)" }
case let .bool(v): return v.toJSONString { "\($0)" }
case let .array(arr): return arr.toJSONString { prettyArray($0) }
case let .object(obj): return obj.toJSONString { prettyObject($0) }
case .null: return "null"
}
}
private func refString(_ value: String) -> String { "\"\(value)\"" }
private func prettyArray(_ array: [JSON?]) -> String {
"[" + array.map{ $0.toJSONString { $0.JSONString } }.joined(separator: ", ") + "]"
}
private func prettyObject(_ object: [String: JSON?]) -> String {
"{" + object.map { refString($0) + ": " + $1.toJSONString{ $0.JSONString } }.joined(separator: ", ") + "}"
}
}
extension Optional {
func toJSONString(_ functor: (Wrapped) -> String) -> String {
switch self {
case .none:
return JSON.null.JSONString
case .some(let wrapped):
return functor(wrapped)
}
}
}
Parser 解析器
Parser 定义
经过上面的铺垫,我们终于可以开始正文,如何优雅地解析一个符合标准规范的JSON字符串,我强烈建议先阅读 函数式swift 中有关解析器构造的相关章节,本文中不会对有关解析器组合算子的部分做过多讨论。下面我直接给出有关解析器Parser的相关代码并给出一些简单的解释:
public typealias Stream = String
precedencegroup CustomerPrecedence {
associativity: left
higherThan: MultiplicationPrecedence
assignment: false
}
infix operator <*>: CustomerPrecedence
func <*><A, B>(lhs: Parser<(A) -> B>, rhs: Parser<A>) -> Parser<B> {
return lhs.followed(by: rhs).map { f, x in f(x) }
}
infix operator <^>: CustomerPrecedence
func <^><A, B>(lhs: @escaping (A) -> B, rhs: Parser<A>) -> Parser<B> {
return rhs.map(lhs)
}
public struct Parser<Result> {
public let parse: (Stream) -> (Result, Stream)?
}
extension Parser {
private var _many: Parser<[Result]> {
return Parser<[Result]> { input in
var result: [Result] = []
var remainder = input
while let (element, newRemainder) = self.parse(remainder) {
result.append(element)
remainder = newRemainder
}
return (result, remainder)
}
}
var many: Parser<[Result]> {
return curry({ [$0] + $1 }) <^> self <*> self._many
}
var optional: Parser<Result?> {
return Parser<Result?> { input in
guard let (result, remainder) = self.parse(input) else { return (nil, input) }
return (result, remainder)
}
}
func map<T>(_ transform: @escaping (Result) -> T) -> Parser<T> {
return Parser<T> { input in
guard let (result, remainder) = self.parse(input) else { return nil }
return (transform(result), remainder)
}
}
func followed<A>(by other: Parser<A>) -> Parser<(Result, A)> {
return Parser<(Result, A)> { input in
guard let (result1, remainder1) = self.parse(input) else { return nil }
guard let (result2, remainder2) = other.parse(remainder1) else { return nil }
return ((result1, result2), remainder2)
}
}
func or(_ other: Parser<Result>) -> Parser<Result> {
return Parser<Result> { input in
return self.parse(input) ?? other.parse(input)
}
}
}
Parser 组合算子
-
Parser 接受一个String作为输入流参数进行解析,将解析结果和剩余的未被解析的部分输出。
-
Parser 定义了若干组合算子:
- many:进行一次或多次解析,首先进行单次解析,失败后直接返回nil,否则继续解析,将结果以一个数组的形式返回。
- optional:可选解析,解析成功或失败都会继续往下解析,不会中断解析链。
- map:将一种解析器转化为另一种解析器。
- followedBy:顺序解析,将两个解析器按顺序拼接,形成一个解析链,按顺序对输入流进行解析,当任一解析器解析失败时,整个解析链将会结束解析。
- or:选择解析,按优先级依次尝试应用解析器,直到一个解析成功或全部失败。
-
Parser 定义了两个解析运算符:<^> 和 <*> ,他们是用来做什么的呢,我们来看follwedBy组合算子:
func followed<A>(by other: Parser<A>) -> Parser<(Result, A)> { return Parser<(Result, A)> { input in guard let (result1, remainder1) = self.parse(input) else { return nil } guard let (result2, remainder2) = other.parse(remainder1) else { return nil } return (**(result1, result2)**, remainder2) } }可以看到两个解析的结果被组合成一个元组返回了,可以想象到每进行一次followedBy组合运算,元组嵌套都会变深一层,这使得我们对最终结果的读取造成困难,于是我们试图寻找一个可以将嵌套元组结构打平的方法。此处我们巧妙的运用了柯里化的思想,将一个符合多重运算解析参数列表的函数进行柯里化,然后对原本的解析器以map运算转换成一个此柯里化函数类型的解析器,如此最终得到的结果被从重重嵌套的多重元组中摘取了出来。此处的详细解释请参见
《函数式swift》之《 解析器组合算子》 中改进顺序解析部分,此处不做过多讨论和解析。- <^> :此运算符用于将一个解析器转化成一个柯里化函数类型的解析器。
- <*> :此运算符将一个解析器拼接在一个柯里化类型解析器之后,再用map将其转化为一个参数更少的柯里化函数类型的解析器。
柯里化
此处不对柯里化进行过多的讨论,为了方便上一节中提到的运算符,我们需要定义一些辅助函数用于将普通的函数转化为柯里化函数,比如:
func curry<A, B, C>(_ f: @escaping (A, B) -> C) -> (A) -> (B) -> C { return { x in return { y in return f(x, y) } } } func curry<A, B, C, D>(_ f: @escaping (A, B, C) -> D) -> (A) -> (B) -> (C) -> D { return { x in return { y in return { z in f(x, y, z) } } } } ...由于swift无法获得函数的参数列表,我们无法定义一个适用于任意数量参数函数的柯里化转化方法,所以我们按照需要定义了很多不同参数数量版本的柯里化转化函数。
JSON 解析
好了,有了以上的工具,我们可以很容易的去写出若干的JSON解析器,接下来让我们由浅至深地去定义我们需要的JSON解析器。
首先我们定义一个辅助函数 character 用来帮我们解析单个字符:
extension JSONParser {
static func character(condition: @escaping (Character) -> Bool) -> Parser<Character> {
return Parser { input in
guard let char = input.first, condition(char) else { return nil }
return (char, Stream(input.dropFirst()))
}
}
}
null 解析器
首先从最简单的 null 开始,显而易见,解析得到一个null,我们只需要依次解析 n、u、l、l 四个character即可。那么我们首先定义这三个字符的解析器:
extension JSONParser {
static let letterN = character { $0 == "n" }
static let letterU = character { $0 == "u" }
static let letterL = character { $0 == "l" }
}
为了将这四个解析器(需要两个letterL解析器)以顺序解析的方式组合起来,我们需要定义一个assemble函数用来柯里化之后打平多重元组:
static func assembleNull(_ n: Character, _ u: Character, _ l1: Character, _ l2: Character) -> JSON { JSON.null }
可以看到这个函数有四个Character类型的参数,返回类型是一个JSON,我们为什么要定义这样的一个函数呢,为什么这个函数的参数和输出结果毫无关系呢,这是因为我们最终的 null 解析器需要依次解析四个Character类型的输入流,然后最终给出一个 JSON 类型的输出流,所以尽管最终返回的结果 JSON.null 和四个参数毫无关系,为了得到一个符合条件的柯里化函数,这四个参数是不能省略的。接下来我们就可以用已有的组合算子来得到 null 解析器了:
static let null = curry(assembleNull) <^> letterN <*> letterU <*> letterL <*> letterL
null 解析器对输入的字符串首先尝试解析字符N,成功之后对剩下的字符串尝试解析字符U… 直到一个解析失败后返回nil,或者全部解析成功之后返回一个JSON.null,整个解析链结束。怎么样,是不是很有趣,仿照这个思路我们可以轻松的得到 bool 解析器。
bool 解析器
-
true 解析器:
static let `true` = curry(assembleTrue) <^> letterT <*> letterR <*> letterU <*> letterE -
false 解析器:
static let `false` = curry(assembleFalse) <^> letterF <*> letterA <*> letterL <*> letterS <*> letterE
然后我们使用 or 组合算子将他们组合到一起得到 bool 解析器
static let bool = `false`.or(`true`)
此处我们优先解析 false,因为大部分的 bool 默认值为 false,false 出现的频率会比 true 高一些,这样会提高我们的解析效率。
integer 解析器
为了解析 integer,我们首先定义一个解析单个数字的解析器 digit :
static let digit = character { CharacterSet.decimalDigits.contains($0) }
...
extension CharacterSet {
func contains(_ c: Character) -> Bool {
let scalars = String(c).unicodeScalars
guard scalars.count == 1 else { return false }
return contains(scalars.first!)
}
}
此处是为CharacterSet写了一个拓展,你也可以用别的方式来实现这个逻辑。
然后我们得到 integerNum 解析器:
static let integerNum = digit.many.map { Int(String($0))! }
此处使用了强制解包,你可能会担心是否会有崩溃的问题,请不要担心,之前的解析链已经保证了此处一定可以解包成功。但是这还不是我们最终想要的 integer 解析器,因为我们需要的是一个 Parser,所以我们仍然需要一个 assemble 函数辅助:
static func assembleInteger(num: Int) -> JSON { JSON(num) }
现在我们终于得到了 integer 解析器:
static let integer = curry(assembleInteger) <^> integerNum
当然,此处不必使用 curry 来做柯里化转化,因为 assembleInteger 只有一个参数,但是为了美观和统一,我仍然写了一个单参数的柯里化转化方法。
double 解析器
有了 integer 解析器之后,我们可以轻松的运用组合算子得到 double 解析器:
static let point = character { $0 == "." }
static let decimal = integerNum
static let double = curry(assembleDouble) <^> integerNum <*> point <*> decimal
static func assembleDouble(integer: Int, point: Character, decimal: Int) -> JSON { JSON(Double("\(integer)\(point)\(decimal)")!) }
只需要将两个 integerNum 和一个小数点按顺序组合即可,你可以发现在assembleDouble 中,我们定义了三个参数,但是最终的返回结果中只用到了两个,point参数被我们丢掉了,这是因为小数点不是我们所需要的信息,当然 assembleNull 中我们丢掉了所有的参数。所以我们要明确一件事:assemble函数中的参数是为了解析过程所需的,并不一定是结果所需要的。
string 解析器
此处我们暂时不考虑 字符串中包含 字符 /“ 的情况,于是可以定义单个字符和多个字符的解析器:
static let singleString = character { $0 != "\"" }
static let multString = singleString.many.map { String($0) }
JSON中字符串的特征是被两个引号包裹,于是我们最终可以得到 string 解析器:
static let string = curry(assembleString) <^> stringStart <*> multString <*> stringEnd
static let stringStart = character { $0 == "\"" }
static let stringEnd = stringStart
static func assembleString(leftMark: Character, value: String, rightMark: Character) -> JSON { JSON(value) }
如果你感兴趣的话,不妨自己尝试下如何处理字符串中包含引号的情况。
匿名函数的递归调用
我们已经将五种基本类型的解析器处理完了,现在我们要处理 复合类型的解析器 array 和 object。但在这之前,我首先想讨论另外一件事情,这关乎到之后逻辑是否能够顺利的在swift之上落实。
很明显,由于 array 的定义,array解析器无法避免的要去递归的调用自己,但是在上面解析器的定义中,我们都是使用了匿名函数的方式来定义解析器,那么问题来了,swift的匿名函数支持递归调用吗,我们直觉上认为它是支持的,但是为了类型安全,swift禁止变量在自己的初始化方法中调用自己,编译器会给出一个循环引用的报错。类似这样:
尽管swift也可以像OC那样允许我们这样做,然后最后在运行时给出一个无限递归的爆栈,但为了安全,swift直接在编译阶段禁止了这种做法。
不过我们仍然有办法绕过编译器,通过自由函数的形式封装一层,使编译器就可以像对待普通函数那样处理匿名函数的递归逻辑了。
let testFunc: (Int) -> Void = { i in
func f(_ num: Int) {
if num <= 0 {
return
}
f(num - 1)
}
return f(i)
}
或者我们只需要给其声明参数类型即可:
enum Test {
static let b: Int = b + 1
static let testA = { testB() }
static let testB: () -> Void = { testA() }
static let testF: () -> Void = { testF() }
}
上面的代码可以通过编译,也可以正常的运行 → 无限递归下去。但有时会发生运行时错误,像是这样:
导致这个问题的具体原因我还没有找到,我还是建议使用自由函数封装的方式,更加可靠。
array 解析器
好了,现在让我们开始处理 array 解析器。首先我们定义几个解析器用以辅助:
static let number = double.or(integer)
static let metaElement: Parser<JSON> = string.or(number).or(bool).or(null).or(referenceArray).or(referenceObject)
static let element = curry(assembleElement) <^> whiteSpace <*> metaElement <*> whiteSpace
我来简单解释一下:
-
number 解析器用来解析数字,double 的解析优先级要高于 int,当解析 double 失败时才会再去尝试解析 integer,若反过来,double 的整数部分会直接被解析为 integer。
-
metaElemenet 解析器是所有类型集合,string 的解析优先级最高,因为 JSON 中 string 出现的频率最高,不但要做为 object 的 key,而且大部分的数据也是以 string 的形式存在,这样会提高解析效率。另外我们可以看到,基本类型的解析优先级高于复合类型,这其中隐含了递归结束的条件。
-
referenceArray 和 referenceObject 是 array 和 object 的一层自由函数封装,它帮助我们完成了匿名函数的递归调用:
// MARK: - 解决匿名函数递归调用问题 extension JSONParser { static let referenceArray = Parser<JSON> { input in // 通过自由函数的形式封装一层,使编译器特殊处理匿名函数的递归逻辑 func f(_ stream: Stream) -> (JSON, Stream)? { array.parse(stream) } return f(input) } static let referenceObject = Parser<JSON> { input in func f(_ stream: Stream) -> (JSON, Stream)? { object.parse(stream) } return f(input) } } -
element 最后在 metaElemenet 前后分别拼接了 white space ,ws 通常用于使 JSONString 更加美观,但并没有实际意义。
static let singleWhiteSpace = character { [" ", "\n", "\r", "\t"].contains($0) } static let whiteSpace = singleWhiteSpace.many.map { String($0) }.optional
然后我们还需要定义一个 arrayElement 解析器,它代表了一个 element 拼接上一个 分隔符,分隔符由 ws 和 逗号组成:
static let separator = curry(assembleSeparator) <^> whiteSpace <*> metaSeparator <*> whiteSpace
static let arrayElement = curry(assembleArrayElement) <^> element <*> separator
最后我们得到最终的 array 解析器:
static let array = curry(assembleArray) <^> arrayStart <*> arrayElement.many.optional <*> element.optional <*> arrayEnd
static func assembleArray(start: Character, list: [JSON]?, last: JSON?, end: Character) -> JSON {
var jsonList: [JSON] = []
if let list = list {
jsonList += list
}
if let last = last {
jsonList += [last]
}
return JSON(jsonList)
}
它以 [ 开始,以 ] 结束,中间有零到多个 arrayElement 和 零或一个 element
object 解析器
类似 array ,此处给出实现:
static let key = curry(assembleKey) <^> whiteSpace <*> string <*> whiteSpace
static let metaObject = curry(assembleMetaObject) <^> key <*> colon <*> element
static let objectElement = curry(assembleObjectElement) <^> metaObject <*> separator
static let object = curry(assembleObject) <^> objectStart <*> objectElement.many.optional <*> metaObject.optional <*> objectEnd
// 思考:为什么此处用JSON作为key而不是String
static func assembleMetaObject(key: JSON, colon: Character, value: JSON) -> (String, JSON) { (key.rawValue as! String, value) }
static func assembleKey(aheadSpace: String?, key: JSON, hinderSpace: String?) -> JSON { key }
static func assembleObjectElement(element: (String, JSON), separator: Character) -> (String, JSON) { element }
static func assembleObject(start: Character, list: [(String, JSON)]?, last:(String, JSON)?, end: Character) -> JSON {
var jsonDic: [String: JSON] = [:]
if let list = list {
jsonDic = list.reduce(jsonDic) { partialResult, pair in
jsonDic[pair.0] = pair.1
return jsonDic
}
}
if let last = last {
jsonDic[last.0] = last.1
}
return JSON(jsonDic)
}
这里给出一个思考问题:为什么assembleMetaObject 中以 JSON 作为 key 的参数类型而不是 String。
json 解析器
最终我们得到的 json 解析器,其实就是 element 解析器
public static let json = element
如此,我们的 json解析器已经可以解析全部的JSON字符串了。
demo
此处附上 demo 供大家参考: github.com/ssly1997/IL…