Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How does Mori deal with functors, types, and interfaces in JS? #181

Closed
ccorcos opened this issue Mar 3, 2016 · 9 comments
Closed

How does Mori deal with functors, types, and interfaces in JS? #181

ccorcos opened this issue Mar 3, 2016 · 9 comments

Comments

@ccorcos
Copy link

ccorcos commented Mar 3, 2016

When Mori builds a list, how does it define a functor interface saying that the list has a map? And more specifically, how is this transpiled into JS? does it use .prototype methods?

@danprince
Copy link

Just a passing enthusiast, but here are my thoughts.

There's no real concept of a native functor interface in Clojure's because map is defined as a standalone function rather than as a protocol. There's no way for a new type to implement a functor interface.

Instead, map attempts to create a seq from your collection and then iterates over it, applying your function and always returns a mapped list. A true functor would always return a collection of the type it was operating on. From the seq docs:

Returns a seq on the collection. If the collection is empty, returns nil. (seq nil) returns nil. seq also works on Strings, native Java arrays (of reference types) and any objects that implement Iterable. Note that seqs cache values, thus seq should not be used on any Iterable whose iterator repeatedly returns the same mutable object.

So, anything that can be seq'd can also be mapped. The closest thing we can get to a functor interface is to implement Iterable, which allows our data to be seq'd. For instance, list is an instance of clojure.lang.PersistentList, which implements java.util.List, which implements java.lang.Iterable.

Because map is a completely separate entity from list, it wouldn't make much sense to try and express it in terms of Javascript's prototypal inheritance either.

// this style of code is a good fit for prototypes
list(1, 2, 3).map(f)

// this style is bad fit for prototypes
map(f, list(1, 2, 3))

You would have to do some serious voodoo to get map onto a prototype of list for Mori and you'd be going against the semantics of the Clojure.

In Mori's case, the ClojureScript compiler simply compiles the native versions of map and list and the same check happens in Javascript code instead.

All this said, there's no reason you couldn't implement your own functor protocol (in fact someone already has) which I think (in theory) could be compiled by the ClojureScript compiler. I'm not exactly sure how the compiler handles protocols, but I doubt it's with prototypes.

I'm fairly new and still learning a lot about the language too. These are mostly just my guesses after a few months of playing with both. If there are any blunders in, please point them out and put me on the right track.

@ccorcos
Copy link
Author

ccorcos commented Mar 5, 2016

My real motivation is evident in your example here:

// this style of code is a good fit for prototypes
list(1, 2, 3).map(f)

// this style is bad fit for prototypes
map(f, list(1, 2, 3))

It seems odd that map needs to be specific to your list. Map is like +, it a general concept. So you should be able to use it to swap out your own date structures...

map(f, MyList(1, 2, 3))
map(f, Set(1, 2, 3))
map(f, OrderedSet(1, 2, 3))
map(f, HashMap(1, 2, 3))

It seems to me like you should be able to use the same map function for mapping over anything thats mappable..

@danprince
Copy link

It seems to me like you should be able to use the same map function for mapping over anything thats mappable..

And that's the way it already is in Clojure and Mori.

The example was there to show that only the first of the two styles is appropriate for expressing with prototypes. Because Clojure's functions are kept separate from the data structures, the second example is the way you'd use them in JS and it doesn't fit with the model of prototypal inheritance.

@ccorcos
Copy link
Author

ccorcos commented Mar 6, 2016

Interesting. So if I wanted to create my own functor in Clojure, how would I specify the fact that it is mappable, so that the generic map function knows that to do with it?

@hallettj
Copy link

hallettj commented Mar 6, 2016

@ccorcos: The short answer is to implement the Iterable protocol from ES2015. The long answer follows.

Actually, Clojure has a feature called protocols, which are a lot like interfaces. Many of the functions in Mori (including map) take arguments that implement the ISeqable protocol.

Try applying map to a value that cannot be mapped, and see what happens:

> mori.map(x => x, 3)
Error: 3 is not ISeqable

ClojureScript (and thus Mori) looks up protocol implementations via properties that are added onto collections. If you load up a development build of Mori, you can inspect values to see this:

> Object.keys(vector)
[ 'meta',
  'cnt',
  'shift',
  'root',
  'tail',
  '__hash',
  'cljs$lang$protocol_mask$partition0$',
  'cljs$lang$protocol_mask$partition1$' ]

I would bet that if the right properties were added to a prototype, then values of the corresponding class could be used as ISeqable values.

But the production build of Mori has been run through aggressive optimizations via Google Closure Compiler, which rewrites property names. So if you try inspecting values created from the production build, this is what you see:

> Object.keys(vector)
[ 'k', 'g', 'shift', 'root', 'W', 'p', 'j', 'q' ]

There is another pull request on Mori, #108, that adds a feature for registering plain JavaScript values as implementations of ClojureScript protocols. But if you just want your types to be usable as ISeqable values, then there is an easier way.

It happens that for compatibility with JavaScript, Mori accepts Iterable values in place of ISeqable. So if you put an @@iterator method in your prototype, or individually in your objects, you are set.

It happens that I wrote Flow type definitions for Mori that provide a clear picture of which functions take Seqable (a.k.a. ISeqable) arguments vs more specific argument types.

@ccorcos
Copy link
Author

ccorcos commented Mar 8, 2016

@hallettj Awesome! thanks for the explanation. Thats makes more sense. It looks like you have the please of living in your own world in Clojurescript. I'm trying to make due in JS land. Looks like I'll be resorting to using some sort of prototype methods though...

@hallettj
Copy link

hallettj commented Mar 9, 2016

After further experimentation, I think that I may have been wrong about functions that take ISeqable values also taking ES2016 Iterable values. Anywhere an ISeqable value is expected, you can use a plain javascript array or a javascript string (which is treated as a list of characters). But it looks like that support for non-Clojure values does not generalize to other Iterable types. For example, collections from Immutable do not work, even though they are iterable. It would be nice to update Mori to treat arbitrary iterable values as ISeqable.

@ccorcos
Copy link
Author

ccorcos commented Mar 9, 2016

The problem with Iterables is they're mutable... I'm not sure Clojure would like that...

@ccorcos ccorcos closed this as completed Mar 9, 2016
@hallettj
Copy link

Javascript arrays are also mutable, but Mori functions consume them without a problem. My point is that converting a mutable iterable into an immutable ISeq is just as easy as converting a mutable array into an ISeq - but working with any iterable input is a more general solution.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants