I have no map

Dropping the map operation on JValue

If this [book of mine] fails to take a straight course, it is because I am lost in a strange region; I have no map.

-- Graham Greene

One of the things I considered for SON of JSON is to have the ability to walk the node structure with a for comprehension, and to have the ability to map and filter elements of objects and arrays in a Scala idiomatic way.

First attempt

Now, this would obviously suck:

arr(1, 3, 1, 5) match {
   case JArray(elements) => JArray(elements.map {
      case JInt(value) =>  JInt(value * 2)
   }
}  

First of all, I don't want to check if it's a JArray first. Secondly, I also don't want to extract the elements and then call map on the nested collection of elements.

So I implemented something that allows you to call map(…) on any JValue directly. Clearly, in some circumstances that doesn't make any sense at all, but in case of JObject and JArray it does. Then the next question is, what's the signature of the function that map should accept?

  • In case of an array, it would have to be JValue => T
  • If T could be implicitly converted to a JValue, then it should result in a new JArray.
  • In other cases, it should have resulted into something sensible. A Seq[T] would be sensible.
  • In case of an array, it could also have been (JValue, Int) => T, if we want make sure the function has the ability to act upon the index of the element as well. (Which would also more closely alligned with how you deal with it in JavaScript.
  • In case of an object, it would have to be JValue => T, if you expect the map operation only to operate on the values of the attributes.
  • However, with objects, it would be more likely to have that map operation receive a (JValue, String) => T argument, and then turn the result into a new object if T is (JValue, String).

I did try to get a version of map that would accept any of these arguments and work correctly. It worked. But then the compiler got confused when I would try to use for comprehensions, which made the whole thing considerably less useful.

Second attempt

In the latest version, I still have the map operation, but I'm planning to drop it. You heard that right: "drop it". The issue is that it just doesn't make any sense to have a map operation that is capable of addressing all these different concerns. Originally, I had one map operation that assumed the receiving object to be an array or a key value pair type of object, depending on the function passed in. But I changed my mind. If you know that the type of JValue is something that can be represented as a sequence of name value pairs, then it actually makes sense to explicitly state your expectations on the underlying type. And as it turns out, we already had an operation for that:

person.scores.as[List[Int]].map(_ * 2)
person.grades.as[Map[String, Int]].sortBy(_._2)

So I'm expecting not only to drop JValue.map, but I already stripped out a JValue.filter operation as well. The one thing I am considering to put in is something that corresponds to a collection collect operation, like the one on TraversableLike.

By dropping my SON of JSON's own map implementation, you will loose some capabilities that you might have used in some circumstances. However, since you cannot define a map operations that supports all use cases and meanwhile make sure it's supporting a for comprehension, it would never be an implementation that is complete, and in many cases still force you to be explicit about your type expectations. Without the dedicated map operation, you still need to state your expecatations explicitly, but it doesn't cost you all that much in terms of code, and there's one big benefit: there is only one way to get things done.