From 7a374bc93e6a103c3f446080ac0cb7bb77c82241 Mon Sep 17 00:00:00 2001 From: erp12 Date: Mon, 13 Sep 2021 12:27:47 -0400 Subject: [PATCH] Finish docs and alpha reflect --- README.md | 22 +- doc_src/faq.md | 16 +- doc_src/guide.md | 379 +++++++++++++++++++++++++++++- src/erp12/fijit/alpha/reflect.clj | 48 ++-- src/erp12/fijit/function.clj | 2 - src/erp12/fijit/tuple.clj | 4 +- src/erp12/fijit/version.clj | 4 +- test/erp12/fijit/tuple_test.clj | 8 +- 8 files changed, 420 insertions(+), 63 deletions(-) diff --git a/README.md b/README.md index 401a885..370c21d 100644 --- a/README.md +++ b/README.md @@ -3,22 +3,20 @@ Clojure is a great language, but some people use Scala... and that's okay! Fijit is a Clojure library for interop with Scala. The goal of this project is to make it easier and more idiomatic to -use Scala projects from within your Clojure projects. +use Scala projects from within your Clojure projects. Some key features include: + +- Conversions between Scala and Clojure collection types. +- Macros for writing Clojure code that targets multiple Scala versions. +- A Clojure implementation of the Scala function types. +- Idiomatic Clojure wrappers for common Scala types like `Option`, `Try`, `Tuple`, and more. +- An experimental Clojure API for Scala reflection and compilation. ## Documentation -Please see the [official documentation page](erp12.github.io/fijit/index.html) for more information on installation, usage, the API, and more. +Please see the [official documentation page](erp12.github.io/fijit/index.html) for more information on +installation, usage, the API, and more. Quick Links: - [Guide and Overview](erp12.github.io/fijit/guide.html) - [API](erp12.github.io/fijit/index.html) -- [Contributing](erp12.github.io/_fijit_/contributing.html) - -# Todo before first release - -- Write documentation guides. - - How to add to deps. - - Native interop - - Calling object methods. - - Calling package object methods. - - Touching path dependent types. +- [Contributing](erp12.github.io/fijit/contributing.html) diff --git a/doc_src/faq.md b/doc_src/faq.md index 201d471..c729b4f 100644 --- a/doc_src/faq.md +++ b/doc_src/faq.md @@ -1,23 +1,27 @@ # Frequently Asked Questions -## Will Fijit ever support Scala 3? +## Will fijit ever support Scala 3? The short answer is: probably not. -The original motivation for building Fijit was to facilitate the development of Clojure interfaces into Scala-native +The original motivation for building fijit was to facilitate the development of Clojure interfaces into Scala-native projects like [Apache Spark](https://spark.apache.org/) and [Akka](https://akka.io/). At the time of writing, neither of these tools run on Scala 3 and will likely not be switching for a long time. -That said, Fijit already leverages macros to delegate to different implementations depending on the Scala version. -It is possible that a Scala 3 implementation is possible for the entire Fijit API, it is possible that one day Fijit +That said, fijit already leverages macros to delegate to different implementations depending on the Scala version. +It is possible that a Scala 3 implementation is possible for the entire fijit API, it is possible that one day fijit could support Scala 3. -If you are inclinded to help with this effort, please see the [contributing guide](erp12.github.io/fijit/contributing.html). +If you are inclined to help with this effort, please see the [contributing guide](erp12.github.io/fijit/contributing.html). -## Why the name "Fijit"? +## Why the name "fijit"? Clojure and Scala are both: - **F**unctional. - **I**mmutable-first. - **J**vm hosted (primarily). + +## How to pronounce "fijit"? + +Same as "fidget". diff --git a/doc_src/guide.md b/doc_src/guide.md index 4cb7556..4b41947 100644 --- a/doc_src/guide.md +++ b/doc_src/guide.md @@ -4,30 +4,385 @@ Clojure is a great language, but some people use Scala... and that's okay! ## Rationale -Should you create a hybrid Clojure and Scala project? NO! +Should you create a hybrid Clojure and Scala project? ... probably not. Each language assumes a +wildly different opinion on what makes an effective program. -What if you are forced to call out to a Scala project? Plain old JVM interop usually works. +What if you are *forced* to call out to a Scala project? Maybe you want to sneak some Clojure into your +company's Scala stack! Luckily, Clojure's standard JVM interop features usually do a good job. -Fijit makes this easier by providing an idiomatic Clojure API into Scala constructs that commonly appear in -Scala interfaces. +So what is the value of Fijit? -### Goals +**Fijit makes interop easier by providing an idiomatic Clojure API into Scala constructs that commonly appear in +Scala interfaces.** -### Non-goals +In particular, Fijit was created to facilitate the development of idiomatic Clojure wrappers around the popular +information processing frameworks that are available in Scala. For example, +[Apache Spark](https://spark.apache.org/) and [Akka](https://akka.io/). +Clojure is a natural fit for these domains, but it can be difficult to leverage given the differences between +Clojure and Scala (the host language of these frameworks). +In addition, Fijit provides some useful macros that leverage Clojure's compile-on-load architecture to help you write +code that targets multiple Scala versions. Scala is famous for having an interesting relationship with binary +incompatibility [1]. + +is in a constant war with +[source and binary incompatibility](https://docs.scala-lang.org/overviews/core/binary-compatibility-for-library-authors.html) +(which stands in stark contrast to Clojure :smile:) and it isn't uncommon for Scala projects to stay change the upgrade Scala to the next ## Installation -Provide your own scala version. +Fijit does not assume a specific Scala version, thus you must declare both Fijit and Scala as dependencies to +your project. In particular, you need to depend on `scala-library`, `scala-reflect`, and `scala-compiler` +all at the same version. + +If we wanted to build our project against Scala 2.13.6 we would specify the following dependencies: + +```clojure +{:deps {io.github.erp12/fijit {???} + org.scala-lang/scala-library {:mvn/version "2.13.6"} + org.scala-lang/scala-reflect {:mvn/version "2.13.6"} + org.scala-lang/scala-compiler {:mvn/version "2.13.6"}}} +``` + +If we wanted to target multiple versions of Scala, we would use an alias for each version, +and depend on fijit at the project level. + +```clojure +{:deps {io.github.erp12/fijit {???}} + :aliases {:2.12 {:extra-deps {org.scala-lang/scala-library {:mvn/version "2.12.13"} + org.scala-lang/scala-reflect {:mvn/version "2.12.13"} + org.scala-lang/scala-compiler {:mvn/version "2.12.13"}}} + :2.13 {:extra-deps {org.scala-lang/scala-library {:mvn/version "2.13.6"} + org.scala-lang/scala-reflect {:mvn/version "2.13.6"} + org.scala-lang/scala-compiler {:mvn/version "2.13.6"}}}}} +``` + +We recommend declaring your fijit dependency with git coordinates. Future releases will likely also be +published to Clojars. ## Usage Overview -# Functions +The following sections will demonstrate some core features provided by fijit. +For the complete API, see [the documentation site](https://erp12.github.io/fijit/). + +### Collections + +Using the `erp12.fijit.collection` namespace, Scala collections can be created with `scala-*` functions. +These functions mirror their Clojure counterparts. + +> Note: Exact return types may change depending on your Scala version. + +```clojure +(use 'erp12.fijit.collection) + +(scala-list :a :b) +; => #object[scala.collection.immutable.$colon$colon 0x58acad38 "List(:a, :b)"] + +(scala-vector :a :b) +; => #object[scala.collection.immutable.Vector1 0x27571648 "Vector(:a, :b)"] + +(scala-set :a :b :a) +; => #object[scala.collection.immutable.Set$Set2 0x40104fb7 "Set(:a, :b)"] + +(scala-map :a 1 + :b 2 + :c 3) +; => #object[scala.collection.convert.JavaCollectionWrappers$JMapWrapper 0x28a5f7e1 "Map(:a -> 1, :b -> 2, :c -> 3)"] +``` + +Clojure collections can be converted to Scala collections using the `to-scala-*` functions. +Many of these functions are flexible with respect to the exact collection type passed as input. + +```clojure +(to-scala-list [:a :b]) +; => #object[scala.collection.immutable.$colon$colon 0x1320cfab "List(:a, :b)"] + +(to-scala-vector [:a :b]) +; => #object[scala.collection.immutable.Vector1 0x54816c33 "Vector(:a, :b)"] + +(to-scala-set #{1 1 1}) +; => #object[scala.collection.immutable.Set$Set1 0x48b97f32 "Set(:a)"] + +(to-scala-map {:a 1 :b 2}) +; => #object[scala.collection.convert.JavaCollectionWrappers$JMapWrapper 0x64be80db "Map(:a -> 1, :b -> 2)"] +``` + +Scala collections can be converted to Clojure collections. These functions mostly assume that the +type of the collection is compatible with the collection type mentioned in the function name. For +example, the `map->clj` function expects to be passed an instance of `scala.collection.Map`. + +```clojure +(seq->clj (scala-seq :a :b)) +; => (:a :b) + +(vector->clj (scala-vector :a :b)) +; => [:a :b] + +(set->clj (scala-set :a :b)) +; => #{:b :a} + +(map->clj (scala-map :a 1 :b 2)) +; => {:a 1, :b 2} +``` + +In some scenarios, there is no need to be explicit about the collection type we want to create, but +rather we would like the logical collection type for the other host language. This is more commonly +the case when converting Scala collections to Clojure collections. + +In these cases, the `->clj` and `->scala` functions can be used to generically convert collections +using a logically associated types. For example, Scala vectors to Clojure vectors and Scala maps to +Clojure maps. + +These mappings are not applies recursively because this could result in significant extra work +in scenarios where the inner collections are intended to be passed back to an abstraction written +in their types host language. For example, if we have a Scala `Seq[Map[String,Int]]` and would +like to write a some Clojure code that passed each `Map` to a Scala function, only the outer `Seq` +should be converted to a Clojure collection. + +### Functions + +Scala provides a different interface (trait) for function types of each arity. For example, a function +that takes 1 parameters will implement `scala.Function1`. Fijit provides a suite of `deftype` that implement +Scala's function interfaces, as well as `java.io.Serializable` and `clojure.lang.IFn`. These types simply +wrap a Clojure function that is assumed to have the same arity as the desired Scala function type. These +types can be constructed useing the `->fn*` functions. + +```clojure +(require '[erp12.fijit.function :as sf]) + +(def add-one + (sf/->fn1 #(+ 1 %))) + +add-one +; => #object[erp12.fijit.function.Function1 0x29429270 "erp12.fijit.function.Function1@29429270"] + +(instance? scala.Function1 add-one) +; => true + +; Invoked as a Scala function. +(.apply add-one 10) ; => 11 + +; Invoked as a Clojure function. +(add-one 10) ; => 11 + +(def bad-fn2 + ; The `identity` function takes 1 parameter, but we are building a `scala.Function2`. + (sf/->fn2 identity)) + +; The bad-fn2 is creatable... +bad-fn2 +; => #object[erp12.fijit.function.Function2 0x67539f2 "erp12.fijit.function.Function2@67539f2"] + +; ... but it cannot be invoked. +(.apply bad-fn2 :A) ; throws IllegalArgumentException +(bad-fn2 :A) ; throws AbstractMethodError +``` + +Fijit also provides a macro for declaring Scala functions that will implicitly create a Clojure funciton +and wrap it in the correct fijit funciton wrapper. The `scala-fn` macro can be called very similarly to the +Clojure core `fn` macro. + +```clojure +(def plus (sf/scala-fn [a b] (+ a b))) + +(instance? scala.Function2 plus) +; => true + +; Invoked as a Scala function. +(.apply plus 1 2) ; => 3 + +; Invoked as a Clojure function. +(plus 1 2) ; => 3 +``` + +### Option and Try + +Scala represents optional values with the wrapper type `scala.Option`. Many Scala interfaces +require `Option` objects as input and provide `Option` objects as returned values. + +The object for non-existing values (`scala.None`) is a singleton. Fijit provides +this as a `def`. + +```clojure +(use 'erp12.fijit.option) + +none +;=> #object[scala.None$ 0x14e04246 "None"] +``` + +Creating `Option`s can be done with `option` function. If provided `nil` or no arguments, it will +return `None`. Otherwise it will return a `Some` that wrapps the given value. + +```clojure +(option) +; => #object[scala.None$ 0x14e04246 "None"] + +(option nil) +; => #object[scala.None$ 0x14e04246 "None"] + +(option :A) +; => #object[scala.Some 0x6e742b26 "Some(:A)"] +``` + +Fijit also provides a variety of ways to handle `Option` values in our Clojure projects. +The `emtpy?` and `defined?` predicates can be used check if the `Option` is holding a +value. + +```clojure +(require '[erp12.fijit.option :as opt]) + +(opt/empty? opt/none) ; => true +(opt/empty? (opt/option :A)) ; => false + +(opt/defined? opt/none) ; => false +(opt/defined? (opt/option :A)) ; => true +``` + +Option objects can be unpacked with the `get` macro. If not passed an `or-else` form, a `NoSuchElementException` +will be thrown. + +```clojure +(opt/get (opt/option :A)) ; => :A +(opt/get none) ; throws NoSuchElementException +(opt/get none :not-found) ; => :not-found +``` + +Side effects and custom error handling can be implemented in the `or-else` form of the `get` macro because +it will not be evaluated unless the option is empty. + +```clojure +(opt/get opt/none (throw (ex-info "Oh no!" {}))) ; throws ExceptionInfo +``` + +The idiomatic Clojure representation of a missing value is `nil`, thus it is common to unpack options into +their held value or `nil` if the option is `None`. Fijit provides a function with this behavior. + +```clojure +(opt/get-or-nil (opt/option :A)) ; => :A +(opt/get-or-nil opt/none) ; => nil +``` + +### Try + +The Scala `Try` type represents the result of a computation that either raised an exception, or successfully +returned a computed value. + +We can create `Try` objects using the `scala-try` macro. + +```clojure +(require '[erp12.fijit.try :as t]) + +(t/scala-try (/ 4 2)) +; => #object[scala.util.Success 0x5c1bfc1c "Success(2)"] + +(t/scala-try (/ 4 0)) +; => #object[scala.util.Failure 0x1488471 "Failure(java.lang.ArithmeticException: Divide by zero)"] +``` + +The `failure?` and `success?` predicates can be used check if a `Try` is representing a raised exception or +a successfully computed value. + +```clojure +(t/failure? (t/scala-try (/ 4 2))) ; => false +(t/failure? (t/scala-ty (/ 4 0))) ; => true + +(t/success? (t/scala-try (/ 4 2))) ; => true +(t/success? (t/scala-ty (/ 4 0))) ; => false +``` + +Try objects can be unpacked with the `get` macro (different from the `get` macro in other fijit namespaces). + +If no `or-else` form is provided and the `Try` is a `Failure`, the exception held by the try will be thrown. +If an `or-else` is provided, it will be evaluated in the event the that try is a `Failure` instead of raising +the exception. Some use cases for this behavior include: throwing a more specific error or performing a "retry". + +```clojure +(t/get (t/scala-try (/ 4 2))) ; => 2 +(t/get (t/scala-try (/ 4 0))) ; throws ArithmeticException +(t/get (t/scala-try (/ 4 0)) 0) ; => 0 + +(defn my-div + [n d] + (t/get (t/scala-try (/ n d)) + (throw (ex-info "Failed division!" {:numerator n :denominator d})))) + +(my-div 3 0) ; throws clojure.lang.ExceptionInfo: Failed division! {:numerator 3, :denominator 0} +``` + +### Tuple + +Tuples are the canonical representation of Scala `Product` types. Scala uses a different type to denote a scala +of each size. For exaple, 2 element tuples are represented as instances of `Tuple2`. + +We can create Scala tuples with fijit in a couple ways. The first is the `scala-tuple` function, which will +accept a variable number of elements and return a Scala tuple. The second is the `to-tuple` function, which will +converts a sequential Clojure collection to a tuple. + +```clojure +(require '[erp12.fijit.tuple :as tup]) + +(tup/scala-tuple :a 1) +; => #object[scala.Tuple2 0x4fc03728 "(:a,1)"] + +(tup/scala-tuple :a) +; => #object[scala.Tuple1 0x5dbab3c5 "(:a)"] + +(tup/to-tuple [:a 1]) +; => #object[scala.Tuple2 0x6c62f09e "(:a,1)"] +``` + +Scala tuples can be converted to a Clojure vector using the `product->vec` function. This function can be used on +any product, including instances of case classes. + +```clojure +(tup/product->vec (tup/scala-tuple :a 1)) +; => [:a 1] +``` + +### Targeting multiple Scala versions + +Fijit helps us write Clojure code that safely runs on top multiple Scala versions! + +First, it can be helpful to know at runtime which exact version of Scala we are running. The `scala-version` +symbol will provide a map of version-parts and their numeric value. + +```clojure +(require '[erp12.fijit.version :as sv]) + +sv/scala-version +; => {:major 2, :minor 13, :patch 6} +``` + +More importantly, the `by-scala-version` macro will deligate to a different form depending on the Scala version. +We can supply pairs of scala versions and their implementations and let Clojure's compile-on-load architecture +sort out how to compile the byte code. + +Consider the following Clojure code: + +```clojure +(sv/by-scala-version :2.12 :A + :2.12.10 :B + :2.13.4 :C) +``` -# Collections +The behavior will be different depending on the Scala version. Specifically the above code will... -# Option and Try +- Return `:A` on Scala 2.12.0 through 2.13.9. +- Return `:B` for :2.12.10 and all other 2.12.x versions. +- Throw and exception for scala 2.13.0 through 2.13.3. +- Return `:C` for 2.13.4 and all other 2.13.x versions. -# Tuple +In other words, the form that corresponds to the highest compatible version will be executed. A compatible version +is one that has the same major and minor versions as the current version of Scala on the classpath. If a form's +version keyword contains a patch version, it will only be valid if the patch version of the active Scala version +is at least the same number. -# Targeting multiple Scala versions \ No newline at end of file +______________ + +[1] Want to read more about Scala and binary incompatibility? There are some official docs on the subject + [here](https://docs.scala-lang.org/overviews/core/binary-compatibility-for-library-authors.html) and + [here](https://docs.scala-lang.org/overviews/core/binary-compatibility-of-scala-releases.html). + The Apache Spark project has also served as a good example of how difficult upgrading to a new minor version + (or supporting multiple minor versions) can be. See [this discussion](https://contributors.scala-lang.org/t/spark-as-a-scala-gateway-drug-and-the-2-12-failure/1747) + about migrating from 2.11 to 2.12 as well as [this ticket](https://issues.apache.org/jira/browse/SPARK-25075) + about making Spark compatible with 2.13. diff --git a/src/erp12/fijit/alpha/reflect.clj b/src/erp12/fijit/alpha/reflect.clj index c48bb17..649c68f 100644 --- a/src/erp12/fijit/alpha/reflect.clj +++ b/src/erp12/fijit/alpha/reflect.clj @@ -1128,6 +1128,10 @@ This is used as a placeholder in the `self` parameter Template if there is no de [^Symbols$TypeSymbol symb] (.tpe symb)) +(defn info + [^Symbols$Symbol symb] + (.info symb)) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Shared @@ -1204,10 +1208,21 @@ This is used as a placeholder in the `self` parameter Template if there is no de ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Fijit +(defn- param-symbols + [symb] + (->> symb + .asMethod + .paramLists + sc/->clj + (core/map sc/->clj) + flatten)) + +;; @todo Somehow handle alternatives. (defn type-reflect "Reflect on a Scala type, returning a map with `:bases`, `:flags`, and `:members`. - Similar to `clojure.reflect/type-reflect` over Scala types, except that names are - represented as Scala reflect `Symbol` objects rather than Clojure symbols. + Inspired by `clojure.reflect/type-reflect` except for Scala types. Names are + represented as Scala reflect `Symbol` objects rather than Clojure symbols, and parameters + are represented as symbols rather than just their type. :bases a set of symbols of the type's bases :flags a set of keywords naming the boolean attributes of the type. @@ -1220,12 +1235,9 @@ This is used as a placeholder in the `self` parameter Template if there is no de :declaring-class Symbol of the declarer :flags keyword naming boolean attributes of the member - Keys specific to constructors: - :parameter-types vector of parameter type symbols - - Key specific to methods: - :parameter-types vector of parameter type symbols - :return-type return type symbol + Keys specific to constructors and methods: + :parameters vector of parameter symbols. + :return-type return type symbol Keys specific to fields: :type type name @@ -1257,11 +1269,10 @@ This is used as a placeholder in the `self` parameter Template if there is no de (when (final? member-symb) :final) (when (synthetic? member-symb) :synthetic)]))} (cond - (constructor? member-symb) {:parameter-types nil} ;; @todo Finish - (method? member-symb) {:parameter-types nil ;; @todo Finish - :return-type nil} ;; @todo Finish + (method? member-symb) {:parameters (param-symbols member-symb) + :return-type (.returnType (.asMethod member-symb))} (class? member-symb) {} ;; @todo What should be put here? Recursive type-reflect? - (module? member-symb) {} + (module? member-symb) {} ;; @todo What should be put here? Recursive type-reflect? :else {:type nil})))))})) ;;;;;;;;;;;; @@ -1269,20 +1280,11 @@ This is used as a placeholder in the `self` parameter Template if there is no de (comment - - - (let [tree (class-def {:name (type-name "Box") - :tparams [(type-def {:name (type-name "T")})] - :impl (template {:body [default-constructor - (def-def {:name (term-name "boxed") - :tpt (ident (type-name "T")) - :rhs empty-tree})]})})] - (println (pr-raw tree {})) - (println (pr-code tree {}))) + (type-reflect (scala-type scala.Option [Long])) ) ;; @todo Make this entire API easier by automatically converting Clj values to common Scala AST types. ;; Clojure keywords -> TermNames ;; Clojure Symbols -> TypeNames -;; Map -> *Def based on :kind \ No newline at end of file +;; Map -> *Def based on :kind diff --git a/src/erp12/fijit/function.clj b/src/erp12/fijit/function.clj index 7dedc54..a209212 100644 --- a/src/erp12/fijit/function.clj +++ b/src/erp12/fijit/function.clj @@ -10,8 +10,6 @@ instances can be called as normal Clojure functions."} (:import (java.io Serializable) (clojure.lang IFn))) -;; @todo Document that wrapped functions also extend IFn and thus are valid Clojure functions. - (deftype Function0 [f] scala.Function0 (apply [_] diff --git a/src/erp12/fijit/tuple.clj b/src/erp12/fijit/tuple.clj index 062ec22..e610c2b 100644 --- a/src/erp12/fijit/tuple.clj +++ b/src/erp12/fijit/tuple.clj @@ -3,7 +3,7 @@ (:require [erp12.fijit.collection :as sc]) (:import (scala Product))) -(defn tuple +(defn scala-tuple "Creates a Scala `Tuple` from the elements. Scala uses a different `Tuple` class depending on the number of elements and thus the type of the returned value @@ -63,7 +63,7 @@ will different depending on the size of the collection. " [coll] - (apply tuple coll)) + (apply scala-tuple coll)) (defn product->vec "Converts a Scala `Product` type (for example, any instance of any `Tuple` class) to a Clojure vector." diff --git a/src/erp12/fijit/version.clj b/src/erp12/fijit/version.clj index 2bb4665..13623d9 100644 --- a/src/erp12/fijit/version.clj +++ b/src/erp12/fijit/version.clj @@ -75,9 +75,9 @@ version of Scala on the classpath."} For example the above code will - Return `:A` on Scala 2.12.0 through 2.13.9. - - Return`:B` for :2.12.10 and all other 2.12.x versions. + - Return `:B` for :2.12.10 and all other 2.12.x versions. - Throw and exception for scala 2.13.0 through 2.13.3. - - Return `:C` for 2.13.4 and all other 2.13.x versions. + - Return `:C` for 2.13.4 and all other 2.13.x versions. " [& versions-and-forms] diff --git a/test/erp12/fijit/tuple_test.clj b/test/erp12/fijit/tuple_test.clj index 7d380c4..0a4dc0a 100644 --- a/test/erp12/fijit/tuple_test.clj +++ b/test/erp12/fijit/tuple_test.clj @@ -5,13 +5,13 @@ (deftest tuple-test (testing "Tuple creation" - (let [tup (tuple 1 :a "")] + (let [tup (scala-tuple 1 :a "")] (expect 1 (._1 tup)) (expect :a (._2 tup)) (expect "" (._3 tup))) ;; Create tuples from sequential collections. - (expect (tuple :a 1) (to-tuple [:a 1])) - (expect (tuple 1 "A") (to-tuple '(1 "A")))) + (expect (scala-tuple :a 1) (to-tuple [:a 1])) + (expect (scala-tuple 1 "A") (to-tuple '(1 "A")))) (testing "Converting tuples to vectors" - (expect (product->vec (tuple 1 :a "")) + (expect (product->vec (scala-tuple 1 :a "")) [1 :a ""])))