如何用 Swift 优雅地写一个 JSON 解析函数?(下)—— 函数式编程以及协议

如何用 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(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 >>-  (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(f: (Wrapped -> T)?) -> T? {
        if let f = f {
            return self.map(f)
        }
        return nil
    }
}

infix operator <*> { associativity left }

func <*>(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(f: (Wrapped? -> T)?) -> T? {
        if let f = f {
            return f(self)
        }
        return nil
    }
}

func <*>(f: (U? -> T)?, a: U?) -> T? {
    return a.apply(f)
}

理论上来说,这是可以的。为什么说是理论上?
经过测试,如果单一使用,是可以的。
但是实际上,在这种情况,编译器并不能很容易地区分两个 apply ,在运行一段时间后,就会说这里 too complex
所以我们定义一个新的操作符执行这种特定的 Apply:

infix operator <%> { associativity left }

func <%>(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 >>- (
    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(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 的各种特性好好利用的一种可能性,我相信还有更好的解决方法,也欢迎大家交流。

你可能感兴趣的:(如何用 Swift 优雅地写一个 JSON 解析函数?(下)—— 函数式编程以及协议)