JSON mapping and reader monad [DRAFT]
Functional JSON mapping became a hot theme recently. Clear declarative syntax backed by cryptic concepts of monads and applicative functors has exploded many heads.
I’d like to talk about a little refinement of our mapping pattern to better isolate mapping algorithms from actual data.
Let’s start with some simple mapping code:
struct User {
let name: String
let age: Int
static func build(name: String)(age: Int) -> User {
return User(name: name, age: age)
}
}
// TODO: write operators
let JSON = [“name”: “Sergei”, “age”: 30]
let user = User.build >> stringForKey(JSON, “name”)
>>> intForKey(JSON, “age”)
The thing that bothers me is coupling of input JSON with our mapping logic. What if we could rectify our mapping to remove this dependency?
Let’s introduce a Reader monad. The simple idea behind is expressing our computation as a function of our JSON parameter and once we need it, “running” it bound with data.
struct Reader {
let value: T -> U
func run(t: T) -> U {
return value(t)
}
}
Here we express some value U as a function of T and store it in a type safe manner. Here’s how we use it:
func intForKey(key: String) -> Reader {
return Reader({ $0[key] as? Int })
}
See how our dependency on JSON is extracted and moved somewhere to a future?
Once we redefine our lifting and binding operators for Reader<>, we end up with the same mapping code:
let parse = User.build >> stringForKey(JSON, “name”)
>>> intForKey(JSON, “age”)
The only difference is it returns Reader instead of User?. To get User we need to bind it with JSON:
let user = parse.run(JSON)
This approach greatly facilitated code reuse via enabling to define algorithms instead of plain transforms on data.