I created Clojure shop, an open source project, with basic functionality of an online shop:
- Product list
- Authentication
- Cart
- User account
- Payment
- Image resolutions management
- md5
For the full description, please check the Github repository.
I also wrote an open source client application for iOS.
Hope this is useful, it's a learning project, everything working so far, a good start if you're interested in developing online shop / mobile REST api with Clojure. Also, contributions welcome!
Saturday, May 31, 2014
Clojure protocols example 2: Extending existing classes
In my past post "Clojure protocols example" I gave an example about using protocols on new types. But protocols also can be used to extend existing types.
A real world example of this can be found in this library, used to generate message digest (md5, etc) for different input types.
More specifically this file.
Here we see existing data types (Collection, String, File, etc) are extended to implement Digestible protocol. So we can call digest on them - a method which they don't support natively.
This is a different, simplified version, with just what we need to understand:
In this case, we could say, explaining it in object oriented fashion, that we are extending the existing map, vector and string classes in Clojure to support the digest function. A more functional explanation, and thus suitable for this context, would be that we are defining a function, which will execute implementation according to the type of the parameter.
Either way, we call digest with these types, and get the correct hash. In this example, probably needless to say, for demonstrative purposes, we don't return a hash but a text that helps us to see which method was called.
Note that in this example I'm using "digestible" instead of "this" to name the parameter (contrary to the first example I posted about protocols). There's no reason for this, besides I consider "this" maybe to be too engrained into object oriented world and misleading in this context. I may change the other post to use "data-provider" instead of "this". The source of the GitHub project I mentioned uses "message". I consider "digestible" better, since if we pass anything here that's not "digestible", we will get an error. It seems there is no (well known) convention about the naming of this parameter yet, since all the examples I have found use something different.
On a different note, this functionality can also be achieved using multimethods. This is the multimethod version of our example:
More info on when to use protocol or multimethod e.g. here.
A real world example of this can be found in this library, used to generate message digest (md5, etc) for different input types.
More specifically this file.
Here we see existing data types (Collection, String, File, etc) are extended to implement Digestible protocol. So we can call digest on them - a method which they don't support natively.
This is a different, simplified version, with just what we need to understand:
(defprotocol Digestible (digest [digestible])) (extend-protocol Digestible clojure.lang.PersistentVector (digest [digestible] (str "vector-md5-" digestible)) clojure.lang.PersistentArrayMap (digest [digestible] (str "map-md5-" digestible)) java.lang.String (digest [digestible] (str "string-md5-" digestible))) ;test (digest {:a 1 :b 2}) ;map-md5-{:a 1 :b 2} (digest [1 2 3]) ;vector-md5-[1 2 3] (digest "hellooo") ;string-md5-"hellooo"
In this case, we could say, explaining it in object oriented fashion, that we are extending the existing map, vector and string classes in Clojure to support the digest function. A more functional explanation, and thus suitable for this context, would be that we are defining a function, which will execute implementation according to the type of the parameter.
Either way, we call digest with these types, and get the correct hash. In this example, probably needless to say, for demonstrative purposes, we don't return a hash but a text that helps us to see which method was called.
Note that in this example I'm using "digestible" instead of "this" to name the parameter (contrary to the first example I posted about protocols). There's no reason for this, besides I consider "this" maybe to be too engrained into object oriented world and misleading in this context. I may change the other post to use "data-provider" instead of "this". The source of the GitHub project I mentioned uses "message". I consider "digestible" better, since if we pass anything here that's not "digestible", we will get an error. It seems there is no (well known) convention about the naming of this parameter yet, since all the examples I have found use something different.
On a different note, this functionality can also be achieved using multimethods. This is the multimethod version of our example:
(defmulti digest class) (defmethod digest clojure.lang.PersistentVector [digestible] (str "vector-md5-" digestible)) (defmethod digest clojure.lang.PersistentArrayMap [digestible] (str "map-md5-" digestible)) (defmethod digest java.lang.String [digestible] (str "string-md5-" digestible)) ;test (digest {:a 1 :b 2}) (digest [1 2 3]) (digest "hellooo")
More info on when to use protocol or multimethod e.g. here.
Clojure protocols example
Task: create a data provider with different implementations e.g. database / memory
Note: I'm new to Clojure, glad to receive corrections or suggestions in the case something can be improved.
These snippets can be found (together with others) here.
;We start with a basic in-memory implementation: (defprotocol DataProvider ;We pass the instance to the functions as "this" (the name is arbitrary) (find-user [this id])) (deftype MemoryDataProvider [] DataProvider (find-user [this id] (first (filter #(= (:id %) id) [ {:id 1 :name "betty"} {:id 2 :name "mike"}])))) ;Test: create an instance and find user with id 2 (def my-data-provider (MemoryDataProvider.)) (find-user my-data-provider 2) ;In the database implementation we need to pass e.g. host and port ;in order to establish a connection. We do this using the constructor ;function of the type (see DBDataProvider below). ;We also need to add a function to establish the connection. We have to ;do add it also to the protocol, since the client has to call it and doesn't know ;the implementation. We add a generic method "init" to the protocol, ;for this purpose. ;(Note that "init" means side effects, so this doesn't really make ;sense in the in-memory implementation, but don't see a better solution). (defprotocol DataProvider (init [this]) (find-user [this id])) ;We have to add the method to MemoryDataProvider. Not doing this will ;not cause compiler error, but will throw runtime error if init is ;called on this type (deftype MemoryDataProvider [] DataProvider (init [this]) (find-user [this id] (first (filter #(= (:id %) id) [ {:id 1 :name "betty"} {:id 2 :name "mike"}])))) ;Test (def my-data-provider (MemoryDataProvider.)) (init my-data-provider) ;nil (find-user my-data-provider 2) ;This is the database data provider type (deftype DBDataProvider [host port] DataProvider (init [this] (println "host: " host ", port: " port) ;connect... ) (find-user [this id] ;get it from database... {:id 0 :name "test"} ) ) (def my-data-provider (DBDataProvider. "127.0.0.1" "1234")) (init my-data-provider) ;nil (find-user my-data-provider 2) ;We can also make init return the type: (deftype DBDataProvider [host port] DataProvider (init [this] (println "host: " host ", port: " port) ;connect... this ) (find-user [this id] ;get it from database... {:id 0 :name "test"} ) ) ;then we can do: (def my-data-provider (init (DBDataProvider. "127.0.0.1" "1234"))) (find-user my-data-provider 2)
Note: I'm new to Clojure, glad to receive corrections or suggestions in the case something can be improved.
These snippets can be found (together with others) here.
Subscribe to:
Posts (Atom)