Search code examples
clojureprotocolsabstractionhigher-order-functionsmultimethod

CLojure: Higher order functions vs protocols vs multimethods


there are plenty protocols vs multimethods comparisions, but why not to use higher order functions? Let's come with example: We have some data (record for example). And we have methods serialize and deserialize. Say that we want to save it into file, into json, and into database. Should we create protocol called SerializationMethod and records called database, json, file that implement them? It looks kind of hack to create records only to use protocol. Second solution - multimethod - could take string parameter with serialization output and decide how to do this. But I am not sure that is right way to go... And third way is to write function serialize and then pass there data and serializing function. But now I can not name serializing and deserializing method with same name (json fo example):

(defn serialize [method data]
  (method data))

(defn json[data]
  (...))

The question is how can I (or how should I) do this. Is there more generic way with higher order function? Or maybe I don't understand something well? That are my first steps with clojure so please be tolerant.


Solution

  • Converting to JSON is different from writing to a database or a file because the latter are IO operations, the first is a pure transformation of data. With that in mind, I wouldn't recommend to implement them under the same interface.

    Now assuming you had various serialization implementations, lets say json and fressian, it would certainly not be a good idea to implement them on every data structure that you want to (de-/)serialize. Your observation that that would be a hack is correct. More concisely, it would be limiting a record to be only (de-/)serializable with one implementation.

    Instead, it would be more effective to have different Serializers, each implementing the same interface:

    (defrecord JSONSerializer []
      SerializationMethod
      (serialize [this data] ...)
      (deserialize [this data] ...))
    
    (defrecord FressianSerializer []
      SerializationMethod
      ...)
    

    This way we end up having several serializer objects that can be passed to functions that require one. Those functions don't need to be concerned with the implementation.

    Could higher order functions be passed instead?

    (defn do-something 
      [params serialize deserialize]
      ...)
    

    It would work, too. Notice however that this style can quickly grow out of hand. E. g. consider a scenario where a function should be written that deserializes data from one format and serializes it to the other.