Skip to content

jafingerhut/funjible

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

66 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

funjible

A Clojure library designed to be nearly identical to some core Clojure libraries, but with more extensive error-checking and/or documentation.

Right now this includes only the namespace funjible.set. This is functionally identical to clojure.set, except its functions throw exceptions if you give them arguments of the wrong type.

This library exists to give you the following choice:

(a) use the built-in clojure.set functions that, if you pass them non-set collections, will quietly return results you may consider wrong.

user=> (require '[clojure.set :as set])
nil

user=> (set/difference #{4 5} #{4 5 6})
#{}       ; working as documented

user=> (set/difference #{4 5} [4 5 6])
#{4 5}    ; some people wish that this would throw an exception,
          ; or return #{}, but it doesn't.

ClojureDocs.org contains other examples of undesirable return values when you give non-set arguments to other function in clojure.set here: union intersection difference subset? superset?

(b) use the funjible.set versions, and get an exception if you pass them arguments of a type that the function is not documented to handle, at the expense of a little extra execution time to perform the run-time type checks.

user=> (require '[funjible.set :as set])
nil

user=> (set/difference #{4 5} #{4 5 6})
#{}       ; working as documented

user=> (set/difference #{4 5} [4 5 6])

AssertionError Assert failed: (set? s2)  funjible.set/difference (set.clj:88)

Wait, isn't this what clojure.spec is for?

You most definitely can write simple specs for most or all of the functions in the clojure.set namespace that, when instrumentation is enabled, throw exceptions when they are passed arguments of unsupported types, just as the modified versions in funjible.set do.

However, starting from the original versions in clojure.set, the extra run time of the funjible.set versions is much smaller than for the speced versions (see below). Thus you may be willing to use the funjible.set versions in long test runs, or even in production deployments.

The table below gives run times as elapsed wall clock time, measured using the criterium library on a 2015 MacBook Pro running OSX 10.12.6, Oracle JDK 1.8.0_181, and Clojure 1.9.0. The specs used in these measurements can be found here.

;; The larger input values below are defined as:

(def s0-999 (set (range 0 1000)))
(def s1000-1999 (set (range 1000 2000)))
Expression clojure.set funjible.set clojure.set with spec instrumentation enabled
(union #{} #{}) 54 nsec 132 nsec 8,768 nsec
(union #{0 1 2} #{0 1 2}) 263 nsec 280 nsec 9,182 nsec
(union #{0 1 2} #{1000 1001 1002}) 498 nsec 532 nsec 9,330 nsec
(union s0-999 #{0 1 2}) 285 nsec 308 nsec 9,107 nsec
(union s0-999 s0-999) 84,188 nsec 86,447 nsec 93,693 nsec
(union s0-999 #{1000 1001 1002}) 769 nsec 793 nsec 9,589 nsec
(union s0-999 s1000-1999) 226,786 nsec 225,932 nsec 250,610 nsec

Releases and Dependency Information

Latest stable release: 1.0.0

Leiningen / Boot dependency information:

[funjible "1.0.0"]

Maven dependency information:

<dependency>
  <groupId>funjible</groupId>
  <artifactId>funjible</artifactId>
  <version>1.0.0</version>
</dependency>

Usage

There are two primary ways to use the modified versions of clojure.set functions in this library.

One is to modify as many of your Clojure namespaces as you wish that currently require clojure.set, so that they instead require funjible.set. This gives you per-namespace control over which of your code uses the new versions, vs. the originals, but requires changing as many require clauses as you want to use the versions in funjible.set.

The other is to pick at least one namespace anywhere in your code, and require the namespace funjible.set-with-patching. Requiring that namespace not only loads the namespace funjible.set, it also modifies the functions in clojure.set to be the same as the corresponding ones in funjible.set. After requiring the funjible.set-with-patching namespace, any call you make to clojure.set/union will behave the same as a call to funjible.set/union, with the extra argument type checking. The only exceptions to this are any calls to clojure.set/union that were compiled with the Clojure compiler's direct linking option, compiled before the funjible.set-with-patching namespace was required. Such calls will still call the original versions in clojure.set.

An example in a project where you have not required the namespace funjible.set-with-patching anywhere:

user=> (require '[funjible.set :as set])
nil

user=> (set/difference #{4 5} #{4 5 6})
#{}        ; as expected


user=> (clojure.set/difference #{4 5} [4 5 6])
#{4 5}   ; definitely surprises and dismays some people, hence this library

;; funjible.set throws an exception instead of quietly returning the
;; unexpected value.
user=> (set/difference #{4 5} [4 5 6])

AssertionError Assert failed: (set? s2)  funjible.set/difference (set.clj:88)

user=> (doc set/difference)
-------------------------
funjible.set/difference
([s1] [s1 s2] [s1 s2 & sets])
  Return a set that is the first set without elements of the
  remaining sets.  Throws exception if any argument is not a set.  The
  returned set will have the same metadata as s1, and will have the
  same 'sortedness', i.e. the returned set will be sorted if and only
  if s1 is.

  Example:
  user=> (difference #{2 4 6 8 10 12} #{3 6 9 12})
  #{2 4 8 10}
nil

And here is the different behavior you get if anywhere in your project you have required funjible.set-with-patching.

;; This is done in some namespace in your project, _not_ the one where
;; the other expressions below are evaluated:

(require 'funjible.set-with-patching)


;; The interaction below is in some namespace that never mentioned
;; `funjible.set` nor `funjible.set-with-patching`:

user=> (require 'clojure.set)
nil

user=> (clojure.set/difference #{4 5} [4 5 6])

AssertionError Assert failed: (set? s2)  funjible.set/difference (set.clj:88)

Performance notes

A few performance tests show that at least some of the funjible.set functions are no more than 4% slower than their clojure.set counterparts, and usually the performance penalty is less percentage-wise than that. The performance penalty in funjible.set 1.0.0 is purely due to the extra run-time type checking of arguments using set? and map? See:

TBD: Investigate whether use of transients would speed things up in functions like union, intersection, difference, select, etc.

It might help speed things up if there were paths through the functions that never create a transient object if the return value is unchanged from input value. However, it may be not so good for code clarity.

If transients are used, remember to preserve metadata in the return values, in the same way that clojure.set does.

Other Clojure set implementations

You can use funjible.set without using these other implementations of sets, but note that the modified set operation functions in funjible.set will work given instances of these other set types, too, and any others not listed here, as long as those sets implement normal Clojure primitive operations on sets such as conj, disj, and seq.

  • Zach Tellman's immutable bitsets use less memory when you only want sets of integers, especially if those integers have values close together.

  • Michał Marczyk's sorted sets and maps using AVL trees can efficiently find the rank of elements/keys, and they have transient implementations for them, unlike clojure.core's sorted sets and maps.

Running benchmarks

See the project funjible-test-project.

License

Copyright © 2013-2018 Rich Hickey, Andy Fingerhut

Distributed under the Eclipse Public License version 1.0.

About

Almost, but not quite, exactly like Clojure core libraries

Resources

License

Stars

Watchers

Forks

Packages

No packages published