7
votes

I'm looking at doing some interop between clojure and scala. As java itself now has lambdas, I was thinking of a generalisation between data and how to apply a function to a collection

  • clojure functions extend clojure.lang.IFn and generalises collection operations on clojure.lang.ISeq
  • scala functions extend scala.Function and generalises collection operations on scala.collection.Traversable
  • java lambdas extend java.util.function.Function and generalises collection operations on java.util.stream.Stream

Questions:

  • Would monads be useful in this case?
  • If so, would a map operation be implemented across all collection types and how might this be generalisable?

Example:

  (map (scala-fn +) 
       [1 2 3]
       (scala-seq [1 2 3]) 
       (.stream [1 2 3]))
  => (scala-seq [3 6 9])

Continued (added haskell as a tag just in case the hardcore type people might know)

There are operations in both Clojure, Scala and Java that take a collection, applies a function to that collection and returns a new collection.

  • All of these languages run on the JVM.
  • However, each language defines it's own class to represent a function.

I'm more familiar with clojure, so there are operations like:

 (into {} [[:a 1] [:b 2]]) => {:a 1 :b 2}

Which converts a clojure vector into a clojure map. Because the into operation generalises on java.util.List any datastructure that inherits java.util.List can be used.

I wish to work with some scala libraries in clojure and face certain obstacles:

  • Scala, like clojure also has immutable data structures, but they are defined very differently from clojure data structures
  • Scala functions inherit from scala.Function and so need to be wrapped to clojure.lang.IFn
  • Scala datastructures do not inherit from java.util.List which means that:

    (into {} (scala-list [:a 1] [:b 2])) will not work.

  • I'm looking to reimplement some basic clojure functions that also incorporate scala datastructures. (map, reduce, mapcat, etc...)

The functionality would look something like:

 (into {} (scala-list [:a 1] [:b 2])) => {:a 1 :b 2}

 (into (scala-map) [[:a 1] [:b 2]]) => (scala-map :a 1 :b 2)

 (concat (scala-list 1 2) [3 4]) => (scala-list 1 2 3 4)

 (concat [1 2] (scala-list 3 4)) => (1 2 3 4) ;lazy seq

 (map + [1 2] (scala-list 3 4)) => [4 6]

 (map (scala-fn +) [1 2] (scala-list 3 4)) => [4 6]
  • What I'm looking for is the ability to use both clojure and scala functions in collection operations.
  • I can do this without using monads (by checking the collection and function types and doing some coercing before function application)
  • What I'm asking here serves as a bit of a curiosity for me, as all the literature I've read on monads seem to presume that any function f:X->Y is universal.
  • However, in the case of clojure/scala/lambda interop, a clojure function, a scala function and a java lambda are not universal. I'm curious about how category theory might be used to solve this problem.
2
I see only: 1) Overly broad introduction paragraph about "some interop". 2) List of three sentences which I don't understand (apples do not "generalize" oranges, oranges do not "generalize" apples, scala Functions aren't tied to scala.collection.Traversable in any significant way). 3) "Would monads be useful" - for what case? You haven't asked anything yet. 4) "Would a map operation be implemented [...]" - it's already implemented on every thinkable and unthinkable collection, and also on everything that vaguely resembles a functor. Unclear what you're asking. - Andrey Tyukin
And also: the last code snippet is not a map. It seems like it's taking (scala-fn +) which reduces lists of integers to integers (ie. List[Int] => Int), and something that looks roughly like List[Seq[Int]], and produces a Seq[Int]. That corresponds to sequence operation on Traverse, followed by an ordinary map. Maybe take a look at Traverse to reformulate the question more clearly. - Andrey Tyukin
@AndreyTyukin, see additional explaination. - zcaudate
A map operation can be generalized over all container types (and more). In Haskell, this is called a Functor, and the generalized function is called fmap. However, it's impossible to derive this automatically for all interesting cases (though it could be done for some), so it has to be provided for each container. Monads are not really useful for that; but one of the requirements for Monads is that the underlying Monad type is a functor. So for every Monad, such a generalized map must exist. - dirkt
"all the literature I've read on monads seem to presume that any function f:X->Y is universal"... Now I see more technical terms used seemingly out of context, but I don't see how it's supposed to make the question clearer. What I don't see is any attempt to connect the actual problem with monads - what do monads have to do with anything of it? Is it used just as a placeholder for "some abstraction"? - Andrey Tyukin

2 Answers

1
votes

I can only give you the Haskell answer, since I don't speak any of those other languages. However to me it seems like you're mostly looking for a way to automatically convert the input to a function. It doesn't appear to have anything to do with monads.

(concat (scala-list 1 2) [3 4]) => (scala-list 1 2 3 4)

If I translate this to Haskell I would give it a type like this

concat :: (IsList l1, IsList l2) => l1 elem -> l2 elem -> [elem]

where ToList is a typeclass which will just convert this container to a list

class IsListOf a where
    toList :: a elem -> [elem]

It is not clear from your example how you would decide on an output type, so I can't help with that.

(map + [1 2] (scala-list 3 4)) => [4 6]

In Haskell this function is not called map but zipWith. If you want to automcatically convert the input you can do it like this.

zipWith :: (IsList l1, IsList l2) => (a -> b -> c) -> l1 a -> l2 b -> [c]

If you want to automatically convert the function you can do it just as well.

zipWith :: (IsList l1, IsList l2, Is2Function f) => f a b c -> l1 a -> l2 b -> [c]

Is2Function would again be a typeclass that just converts to a 2-ary function

class Is2Function f where
    toFunction :: f a b c -> a -> b -> c

There is also something to keep in mind concerning generalizations. I said in the previous that I didn't know how you would decide on the output. This is a problem the compiler also has from time to time (at least in haskell) when you do to many generalizations. Generalizations seem nice on the surface but they don't always make things more clear and can lead to ambiguity.

3
votes

scala functions extend scala.Function and generalises collection operations on scala.collection.Traversable

java lambdas extend java.util.function.Function and generalises collection operations on java.util.stream.Stream

First, the good news: this isn't correct, Java and Scala lambdas can implement any SAM (single abstract method) interface. This allows using Java lambdas with APIs expecting scala.FunctionN and Scala lambdas with APIs expecting java.util.function.* (including Java streams). This interoperability should be complete in Scala 2.12 and later (so far as I know).

The bad news (you knew this was coming): when talking about Scala collection API specifically, it also very much relies on implicit parameters, and those aren't really usable from Java or Clojure. Similarly, Clojure collection API relies on dynamic typing, and IFn isn't a SAM type (because it covers functions with different numbers of arguments). And of course, for use from Clojure, interoperation between Java and Scala lambdas doesn't help.

More generally speaking, the 3 collection APIs (4 if you count the redesing coming in Scala 2.13) are probably too different to be unified like that.

I don't see any way in which monads per se would be useful here. If I was trying to do something usable from Clojure, I'd go with "checking the collection and function types and doing some coercing before function application" as the solution. Protocols could simplify it, but with some performance cost.