如何用 Swift 优雅地写一个 JSON 解析函数?(上)—— 泛型及扩展
阅读这一篇文章,你需要对函数式编程和协议有一定的了解。
这里用到的代码我都写在了 playground 里,放到了 Github 上。
如果你对函数式编程不甚了解,可以看一看我之前的文章:
Swift 函数式编程探索(1)—— Functor 和 Monad
Swift 函数式编程探索(2)——实践中使用 Functor 和 Monad
Swift 函数式编程探索(3)—— Applicative 和 Curry
我们接着往下说。
5、使用 faltMap 、 Applicative 、以及 Curry
这一部分,许多来自 @chriseidhof 大神的这篇文章 :Parsing JSON in Swift
我们先来看看这一步之后的最终结果:
func parseUser5(json: JSON?) -> User? {
let curryUser = curry(User.init)
return json >>- {
curryUser <*> ("uid".fatch($0) >>- toInt64)
<*> "nam".fatch($0)
<*> "ada".fatch($0)
<*> "sgt".fatch($0)
<*> ("rgt".fatch($0) >>- toDate)
}
}
let user5 = parseUser5(userJSON)
相比之下,是不是简洁了很多?
如果你觉得看不懂,那么我来一步一说明:
let curryUser = curry(User.init)
这是使用 curry 方法接收 User 的初始化器,返回一个柯里化的初始化方法,curry 是这样的:
func curry<A,B,C,D,E,R>(fun: (A,B,C,D,E) -> R) -> A -> B -> C -> D ->E -> R {
return { a in { b in { c in { d in { e in fun(a,b,c,d,e) } } } } }
}
curryUser其实是一个Int64 -> String -> Bool? -> String? -> NSDate? -> User
类型
继续往下看,>>-
也是 flatMap ,是这样定义的 :
infix operator >>- { associativity left }
public func >>- <U,T> (a : U? , @noescape f: U throws -> T?) rethrows -> T? {
return try a.flatMap(f)
}
parseUser5 中 return
的那一行,表示的是 json flatMap 了一个闭包
{
curryUser <*> ("uid".fatch($0) >>- toInt64)
<*> "nam".fatch($0)
<*> "ada".fatch($0)
<*> "sgt".fatch($0)
<*> ("rgt".fatch($0) >>- toDate)
} //(JSON) -> User?
<*> 符号是一个 Apply ,它是这样定义的:
extension Optional {
func apply<T>(f: (Wrapped -> T)?) -> T? {
if let f = f {
return self.map(f)
}
return nil
}
}
infix operator <*> { associativity left }
func <*><U, T>(f: (U -> T)?, a: U?) -> T? {
return a.apply(f)
}
("uid".fatch($0) >>- toInt64)
表示将闭包接收到的参数进行 fatch 操作后(可能得到 nil) flatMap 函数 toInt64
func toInt64(input: Int?) -> Int64? {
return input.map{ Int64($0) }
}
上面的那个闭包中,curryUser 先是接受了一个可能为空的 Int64 (空就返回 nil ),得到一个 String -> Bool? -> String? -> NSDate? -> User
类型。
如此类推,这个闭包接收一个 JSON 类型的参数,返回一个 User 或者 nil
但是这里其实是有一点问题的。
在接受了 Int64 和 String 各表示 userId 和 name 之后,再往下的参数,是可以为空的,但是我们这里实现的 apply ,只要是任意一边为空,就会返回空。
let otherJSON = [
"uid": 2,
"nam": "FrainTest",
// "sgt": "buzy",
"ada": true,
"rgt": "2016-03-04"
]
let other = parseUser5(otherJSON) // nil
换句话说,只要返回的 JSON 中不含有某一个参数即便是可以为空的参数,整个都会是空。
怎么解决这个问题呢?
我们可以这样再定义一个 apply 吗?
extension Optional {
func apply<T>(f: (Wrapped? -> T)?) -> T? {
if let f = f {
return f(self)
}
return nil
}
}
func <*><U, T>(f: (U? -> T)?, a: U?) -> T? {
return a.apply(f)
}
理论上来说,这是可以的。为什么说是理论上?
经过测试,如果单一使用,是可以的。
但是实际上,在这种情况,编译器并不能很容易地区分两个 apply ,在运行一段时间后,就会说这里 too complex
所以我们定义一个新的操作符执行这种特定的 Apply:
infix operator <%> { associativity left }
func <%><U, T>(f: (U? -> T)?, a: U?) -> T? {
return a.apply(f)
}
这样使用:
func parseUser6(json: JSON?) -> User? {
let curryUser = curry(User.init)
return json >>- {
curryUser <*> ("uid".fatch($0) >>- toInt64)
<*> "nam".fatch($0)
<%> "ada".fatch($0)
<%> "sgt".fatch($0)
<%> ("rgt".fatch($0) >>- toDate)
}
}
let user6 = parseUser6(otherJSON)
就没有问题了。
接下来,我们就可以着手解析整个 json 了,同样使用函数式编程,这一次我们需要用到 SequenceType 的 flatMap ,补充上:
public func >>- <S: SequenceType, T>(
a: S, @noescape f: S.Generator.Element throws -> T?) rethrows -> [T] {
return try a.flatMap(f)
}
于是我们就可以这样解析:
func parseJSON(json: JSON?) -> [User]? {
let jsons: [JSON]? = json >>- "data".fatch >>- "users".fatch
return jsons >>- { $0 >>- parseUser6 }
}
parseJSON(json)
这样,是不是足够优雅了呢?
其实还可以再优雅一点,我们结合前面提到的泛型和扩展:
6. 使用 Protocol
我们定义一个这样的协议:
protocol JSONDeserializable {
init?(json: JSON?)
}
这个解析要求实现一个可失败的接收 JSON 的初始化器,我们让 User 类 adopt 这个协议:
extension User: JSONDeserializable {
init?(json: JSON?) {
let curryUser = curry(User.init)
guard let user = json >>- {
curryUser <*> ("uid".fatch($0) >>- toInt64)
<*> "nam".fatch($0)
<%> "ada".fatch($0)
<%> "sgt".fatch($0)
<%> ("rgt".fatch($0) >>- toDate)
} else { return nil }
self = user
}
}
这有什么用呢?我们可以结合扩展和泛型,这样写:
extension String {
func fromParse<T: JSONDeserializable>(json: JSON?) -> [T]? {
let jsons: [JSON]? = json >>- "data".fatch >>- self.fatch
return jsons >>- { $0 >>- T.init }
}
}
然后这样用:
func doSomeThingWithUser(users: [User]) {
print(users)
}
"users".fromParse(json) >>- doSomeThingWithUser
假如我们以后又有了新的数据类型希望能够通过 JSON 创建,只要实现这个协议,就能通过这个泛型函数接受 JSON 来创建(毕竟一般JSON格式相同)
最后
到这里,我们的解析函数已经很简洁,也足够的优雅
就像我一开始说的,这里的目的,是分享一种将 Swift 的各种特性好好利用的一种可能性,我相信还有更好的解决方法,也欢迎大家交流。