make js->clj work with purnam, or with extending collection protocols in general?

198 views
Skip to first unread message

Feng Hou

unread,
Dec 2, 2013, 6:19:44 PM12/2/13
to clojur...@googlegroups.com
Hello,

Chris Zhang's cljs lib https://github.com/zcaudate/purnam extends native object and array (goog closure) with collection protocols. However, it appears js->clj does not play well with them, patricianly coll? clause match first and empty does structure preserving copy (ie. native object/array are returned as the same native objects).

Here is a simple test

(deftest js->clj-with-purnam
(is (= {} (js->clj (obj))))
(is (= [] (js->clj (arr))))
(is (= [{}] (js->clj (arr {}))))
(is (= {"a" [1 2 3] "b" {"c" nil}} (js->clj (obj "a" [1 2 3] "b" {"c" (seq ())}))))
(is (= {"a" 1, "b" 2} (js->clj (js* "{\"a\":1,\"b\":2}"))))
(is (= {"a" nil} (js->clj (js* "{\"a\":null}"))))
(is (= {} (js->clj (js* "{}"))))
(is (= {"a" true, "b" false} (js->clj (js* "{\"a\":true,\"b\":false}"))))
(is (= {:a 1, :b 2} (js->clj (js* "{\"a\":1,\"b\":2}") :keywordize-keys true)))
(is (= [[{:a 1, :b 2} {:a 1, :b 2}]]
(js->clj (js* "[[{\"a\":1,\"b\":2}, {\"a\":1,\"b\":2}]]") :keywordize-keys true)))
(is (= [[{:a 1, :b 2} {:a 1, :b 2}]]
(js->clj [[{:a 1, :b 2} {:a 1, :b 2}]])))
(is (= (js->clj nil) nil)))

Test Report with clojurescript-0.0-2080. Note that even the later 3 assertions not using purnam obj/arr fns are also failing.

FAIL in (js->clj-with-purnam) (:)
expected: (= [] (js->clj (arr)))
actual: (not (= [] #<Array []>))

FAIL in (js->clj-with-purnam) (:)
expected: (= [{}] (js->clj (arr {})))
actual: (not (= [{}] #<Array [#<[object Object]>]>))

FAIL in (js->clj-with-purnam) (:)
expected: (= {"a" [1 2 3], "b" {"c" nil}} (js->clj (obj "a" [1 2 3] "b" {"c" (seq ())})))
actual: (not (= {"a" [1 2 3], "b" {"c" nil}} #<[object Object]>))

FAIL in (js->clj-with-purnam) (:)
expected: (= {"a" nil} (js->clj (js* "{\"a\":null}")))
actual: (not (= {"a" nil} #<[object Object]>))

FAIL in (js->clj-with-purnam) (:)
expected: (= {"a" true, "b" false} (js->clj (js* "{\"a\":true,\"b\":false}")))
actual: (not (= {"a" true, "b" false} #<[object Object]>))

FAIL in (js->clj-with-purnam) (:)
expected: (= [[{:a 1, :b 2} {:a 1, :b 2}]] (js->clj (js* "[[{\"a\":1,\"b\":2}, {\"a\":1,\"b\":2}]]") :keywordize-keys true))
actual: (not (= [[{:a 1, :b 2} {:a 1, :b 2}]] #<Array [#<Array [#<[object Object]>, #<[object Object]>]>]>))

It seems to me that js->clj cond clauses simply assumed a flat disjoin protocol/type hierarchy (or it's by design in this linear order, seq?, coll? first, then array?, s/Object?). I was able to get the same set tests pass by reordering them here

https://github.com/fengh/clojurescript/commit/aedb0bb

Is is a sensible change?

Thanks,
Feng

Feng Hou

unread,
Dec 2, 2013, 6:22:36 PM12/2/13
to clojur...@googlegroups.com

Sorry for misspelling, s/patricianly/particularly/

David Nolen

unread,
Dec 2, 2013, 6:43:39 PM12/2/13
to clojur...@googlegroups.com
Libraries that extend types *and* protocols that they don't control are fundamentally broken. Such extensions should be limited to application code only.

David



--
Note that posts from new members are moderated - please be patient with your first post.
---
You received this message because you are subscribed to the Google Groups "ClojureScript" group.
To unsubscribe from this group and stop receiving emails from it, send an email to clojurescrip...@googlegroups.com.
To post to this group, send email to clojur...@googlegroups.com.
Visit this group at http://groups.google.com/group/clojurescript.

David Nolen

unread,
Dec 2, 2013, 6:56:38 PM12/2/13
to clojur...@googlegroups.com
And to clarify a bit more - extending Object and Array to ICollection at all seems very wrong to me - neither are persistent collections. Internally in CLJS in order for Arrays to participate in protocols they must first be wrapped in IndexedSeq.

David

Gary Trakhman

unread,
Dec 2, 2013, 7:15:28 PM12/2/13
to clojur...@googlegroups.com
I agree that these global extensions shouldn't be a library because it creates the possibility that another library would depend on a particular scheme, which is terrible, but is there another way for us to to avoid reinventing the wheel in every application?

If there's one approach that satisfies most use-cases, it could be documented somewhere, noting where it falls short and where it's convenient.  But maybe there are multiple.

What seems to me as the real issue is that these static, global approaches are not composable.  I've home-cooked my own in an app, but it feels like playing a jenga game, any change is likely to have unintended consequences.  Different tradeoffs of ease/mutability/performance might also drive different choices.

David Nolen

unread,
Dec 2, 2013, 7:38:49 PM12/2/13
to clojur...@googlegroups.com
Sounds like an great opportunity for a library! clj->js and js->clj already crossed into territory that probably should have been left outside of ClojureScript proper.

David

Gary Trakhman

unread,
Dec 3, 2013, 10:18:25 AM12/3/13
to clojur...@googlegroups.com
I created some hare-brained design notes for you all to tear apart:

* Clojure collection operations over JS arrays/objects
  - Promote convenient and obvious interop strategies
  - Create composable approaches

* Tradeoffs
  - Ease
  - Mutability
  - Performance
  - Idiomaticity

* Impl Approaches

** JS-dominant
  - fully mutable
    - manual Copy-On-Write
  - auto-COW
  - Subsuming (converts incoming values to JS representations)
  - Non converting

** CLJS-dominant
  - Functional (Subsuming to CLJ)
  - Functional (No conversion) (closest to current core)

* Interface Approaches
  - Everything must be dynamic or namespace-local.  A default strategy on JS types may be set by an app, not a downstream library.

** Dynamic
  - Wrapping?
    - Annoying
  - Wrap JS collections with deftype/defrecord/reify, expect client code to call an
    unwrap function
  - Wrap-once: Ducktype-Mutate a collection with a custom metadata to
    describe how it'll work.
    - The particular object owns the semantics.
  - Wrap-once: similar to duck-typing, keep a global map of
    objects-to-strategies, 
    - WeakMap?
    - probably a terrible idea.
  - Participate in standard clojure protocols where possible
    - is it possible to mutably implement a protocol without defining
      a type?*


** Static
  - Isolate collection semantics by multiple namespaces (not sure what the state of vars is)
    - Tedious to create and use.
  - Create collection functions that mirror clojure-proper.


If it's possible to set protocol impl strategy on an object itself, that might be the
most convenient(ly abusive) thing, since we can continue to pass around a single value representing the collection, instead of a clojure-context-specific thing that we'd have to wrap and unwrap.

Please let me know if there's any other potential approaches I'm missing.  Currently I'm using a global, subsuming, JS-dominant approach for my angular app, and that's all I've tried to do.  I'd like to find a way to make it not global and avoid wrapping gymnastics.

David Nolen

unread,
Dec 3, 2013, 11:31:23 AM12/3/13
to clojur...@googlegroups.com
Few of these sound appealing to me. That said it sounds like you might like specify/specify!. There's a design page for this and I believe Herwig Hochleitner had started on an implementation of feature at one point.

David

Feng Hou

unread,
Dec 3, 2013, 1:58:23 PM12/3/13
to clojur...@googlegroups.com
Hi Gary and David,

Thanks for sharing your thoughts. Thinking about more on this topic (data structure interop between js and cljs world in general), I'm leaning towards following practical approach at this moment.

1. Js natives vs. cljs collections are fundamentally different, ie. mutable vs persistent structurally , imperative vs. functional operationally. Anything trying to unify these two worlds would be either suboptimal (COW), or broken (hiding js objects/arrays behind cljs collection protocols).

2. Js natives being mutable and imperative are useful properties. What we need in cljs world is set of operators that keep these properties *explicit*, at the same time syntactically *concise*. I can see purnam lib already started an experiment with monadic operators.

3. David's mori lib provides a good way for Js to use cljs persistent collections, however, values created in different worlds can't be shared in mori advanced build. mori simple build does provide interoperability between js and cljs world, but not sure it is intent for production use (size is not a concern since gzip takes care it well).

Thanks,
Feng

David Nolen

unread,
Dec 3, 2013, 2:27:29 PM12/3/13
to clojur...@googlegroups.com
I personally don't see a problem with just working with wrapped mutable JS types if the library I'm integrating with warrants it. Wrapping / unwrapping seems at worst a minor annoyance. I've been experimenting with treating JS values as a limited form of transients - http://gist.github.com/swannodette/7644500. Very half baked but between something like this and specify / specify! and I think interop would be more or less pleasant.

David

Gary Trakhman

unread,
Dec 3, 2013, 10:44:10 PM12/3/13
to clojur...@googlegroups.com
Specify(!) would be really cool, it opens up a lot of possibilities.   It makes the idea of java types feel even more arbitrary and awkward than it did before.

David Nolen

unread,
Dec 3, 2013, 10:50:16 PM12/3/13
to clojur...@googlegroups.com
I'm not planning on working on specify/specify! myself anytime soon. It's a good place as any to jump in and get your hands dirty ;)
Reply all
Reply to author
Forward
0 new messages