kachayev / muse

Clojure library that makes remote data access code elegant and efficient at the same time

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Issue with traverse

Gonzih opened this issue · comments

Hello, I'm playing around with this library and I got stuck trying to reproduce traverse example.

(defrecord Country [iso-id]
  DataSource
  (fetch [_] (api-get (str "countries/" iso-id))))

(defrecord Region [country-iso-id url-id]
  DataSource
  (fetch [_] (api-get (str "countries/" country-iso-id "/" url-id))))

(->> (Country. "es")
     (fmap :regions)
     (traverse #(Region. "es" (:code %)))
     run!!)

With this code snippet I'm getting folowing error:

Exception in thread "async-dispatch-26" java.lang.IllegalArgumentException: No implementation of method: :done? of protocol: #'muse.core/MuseAST found for class: muse_playground.core.Region

Am I missing something here? Should I implement some additional protocols on my records?
Thanks!

Unfortunately, I can't reproduce it:

user=> (require '[muse.core :refer :all])
nil
user=> (require '[clojure.core.async :as a])
nil
user=> (defrecord Country [iso-id] DataSource (fetch [_] (go {:regions [{:code 1} {:code 2} {:code 3}]})))
user.Country
user=> (defrecord Region [country-iso-id url-id] DataSource (fetch [_] (a/go (inc url-id))) LabeledSource (resource-id [_] url-id))
user.Region
user=> (run!! (->> (Country. "es") (fmap :regions) (traverse #(Region. "es" (:code %)))))
[2 3 4]
user=> (->> (Country. "es") (fmap :regions) (traverse #(Region. "es" (:code %))) run!!)
[2 3 4]

Note, that I added LabeledSource to Region, but it doesn't make a difference with respect to exception that you've got.

Can you, please, provide more information:

  • version of muse, clojure and core.async that you use
  • import statement from current file

@Gonzih I ran few more tests. And in few scenarios when cache id = nil (as you have) it can throw IllegalArgumentException exception. As it's non-deterministic and hard to debug, I will update library to use explicit assert that both resource name and resource ID are resolvable.

In you case, muse library can calculate names: Country & Region, but IDs are not specified.

You can fix this as following:

user=> (defrecord Country [iso-id] DataSource (fetch [_] (go {:regions [{:code 1} {:code 2} {:code 3}]})) LabeledSource (resource-id [_] iso-id))
user.Country
user=> (defrecord Region [country-iso-id url-id] DataSource (fetch [_] (a/go (inc url-id))) LabeledSource (resource-id [_] (str country-iso-id "//" url-id)))
user.Region

or declare both records with an ID attr:

user=> (defrecord Country [id] DataSource (fetch [_] (go {:regions [{:code 1} {:code 2} {:code 3}]})))
user.Country
user=> (defrecord Region [country-iso-id id] DataSource (fetch [_] (a/go (inc id))))
user.Region

Yes, I was able to figure out that resources in traverse are cached very agressively. LabaledResource seems to be working, thanks! Aparetntly I wasn't reading readme very carefuly :) Sorry about that.

Also small followup question; Is it possible to use result of traverse in cats.core/mlet?

@Gonzih, like this?

user=> (run!! (m/mlet [regions (->> (Country. "es") (fmap :regions) (traverse #(Region. "es" (:code %))))] (m/return (count regions))))
3

Not exactyl, something like that:

(run!! (m/mlet [country (Country. "es")
                full-regions (traverse #(Region. (:code country)
                                                 (:code %))
                                       (:regions country))]
               (m/return (assoc country :regions full-regions))))

Still can't figure out how to use traverse and preserve original country monad to reuse it later.

That's a bit tricky... You have to know really well what's going on inside mlet to deal with it:

user=> (defrecord Country [id] DataSource (fetch [_] (go {:regions [{:code 1} {:code 2} {:code 3}]})))
user.Country
user=> (defrecord Region [country-iso-id id] DataSource (fetch [_] (go (inc id))))
user.Region
user=> (run!! (m/mlet [country (Country. "es")
                                    regions (collect (map #(Region. "es" (:code %)) (:regions country)))]
                (m/return (count regions))))
3

The problem with traverse is that is makes flat-map which is not an option inside mlet as you work with "pure" value.

@Gonzih does it make sense? Or should I provide further explanation?

Yes, this makes sense. My last question is how collect works?
Thanks for your patience :)

On 07/10/2015 09:27 AM, Alexey Kachayev wrote:

@Gonzih https://github.com/Gonzih does it make sense? Or should I
provide further explanation?


Reply to this email directly or view it on GitHub
#4 (comment).

collect takes a seq of MuseAST nodes (or resources to fetch) and convert them to a single MuseAST node. Definition with scala type system will look like following:

def collect[A](fs: Seq[MuseAST[A]]): MuseAST[Seq[A]]

Makes sense now, thank you!

On 07/10/2015 10:17 AM, Alexey Kachayev wrote:

|collect| takes a seq of MuseAST nodes (or resources to fetch) and
convert them to a single MuseAST node. Definition with |scala| type
system will look like following:

def collectA: MuseAST[Seq[A]]


Reply to this email directly or view it on GitHub
#4 (comment).

You are welcome!