Understanding Open Source Licenses with Clojure

You only know how cool it is to develop or contribute to an open source project until you do so. The worst part is to define which license to use, and this task is frequently neglected by software developers.

Recently we released Licensator, a simple web application that gathers information on the most popular open source licenses and helps you choose the right license for your own projects.

The language behind Licensator is Clojure, which proved to be great to solve this sort of problem. We’ll see today how the application’s main algorithm works.

REPL: where it all begins

One of the strengths of dynamic languages is how easily we can transform ideas into working code, and Clojure is no exception. In fact, I decided to write Licensator in one of these coding sessions in the REPL, Clojure’s interactive shell. After a few minutes playing with it I saw the problem was indeed easy to solve.

Despite the huge number of licenses, we can easily extract information common to all of them, i.e., if it’s copyleft, or if a licensed work can be used by closed source software, etc.

Let’s take the Apache v2.0 license:

(def *licenses*
     [{:id :apl-v20
       :long-name "Apache License v2.0"
       :short-name "Apache v2.0"
       :url "http://www.opensource.org/licenses/apache2.0.php"
       :copyright true
       :patent true
       :closed-source-linking true
       :derivative-work true
       :affero false
       :copyleft false
       :charge-for-distribution true
       :charge-for-use true
       :compatible-with [:apl-v20 :afl-v30 :cddl-v10 :epl-v10 :freebsd
                         :new-bsd :mit :mpl-v11 :public-domain]}])

You can see the remaining licenses here.

Considering all licenses have the same set of information, how can we find the licenses that are not reciprocal and have explicit copyright terms?

(map :id
  (filter #(and (:copyright %) (not (:copyleft %))) *licenses*))
=> (:afl-v30 :apl-v20 :epl-v10 :freebsd :mit :new-bsd :bsd)

If you know a little bit about functional programming, this code should be easy to follow. In any case, what this code does is return the :id of all licenses where :copyright is true and :copyleft is false.

Another example: which licenses are compatible with Apache v2.0 and CDDL v1.0?

(use 'clojure.set)
 
(map :id
  (filter #(subset? (set [:apl-v20 :cddl-v10])
                    (set (:compatible-with %))) *licenses*))
=> (:apl-v20 :cddl-v10 :epl-v10)

Here we used the clojure.set namespace, which provides a few useful functions to deal with relational algebra. The code is very similar to the previous one; the only difference is that now we used the subset? function to get the licenses compatible with [:apl-v20 :cddl-v10].

One last example: which reciprocal licenses are compatible with Apache v2.0?

(map :id
  (filter #(and (subset? (set [:apl-v20]) (set (:compatible-with %)))
                (:copyleft %)) *licenses*))
=> (:cddl-v10 :agpl-v30 :gpl-v30 :lgpl-v30 :mpl-v11)

At this point we can see that there is no magic behind the algorithm; what it does is to filter the list of licenses based on a function.

The solution

The code we saw up until now works fine, but it’s not appropriate to solve the problem since the matching logic is spread all over the place.

It would be better to express the search criteria as a map object…

(def cmap
     {:patent true, :closed-source-linking true, :compatible-with [:mit :new-bsd]})

…so we need a function to filter the licenses based on that input, like this:

(defn find-matches [cmap data]
  (filter (where cmap) data))

Here we have where, yet to be coded, that returns a function that filters the elements (licenses) in data according to the criteria map cmap.

First things first. Which conditions of cmap match the Apache v2.0 license?

(defn criteria-matches? [license [kval cval]]
  "Checks whether the criteria condition centry is true for license."
  (let [lval (kval license)]
    (if (nil? cval)
      true
      (if (coll? lval)
        (subset? (set cval) (set lval))
        (= cval lval)))))
 
(map (partial criteria-matches? apl-v20) cmap)
=> (true true true)

A license can only be considered compatible with the given criteria if the list only yields true. Therefore, in this case, the Apache v2.0 license is considered to be compatible:

(every? true? (map (partial criteria-matches? apl-v20) cmap))
=> true

Given all this, a possible implementation of the where function might be:

(defn where
  "Returns a function that checks whether all conditions in cmap match for license lentry."
  [cmap]
  (fn [lentry]
    (let [res (map (fn [centry]
		     (let [cval (val centry)
			   lval ((key centry) lentry)]
		       (if (nil? cval)
			 true
			 (if (coll? lval)
			   (subset? (set cval) (set lval))
			   (= cval lval))))) cmap)]
      (every? true? res))))

This code is a combination of the previous two snippets; where gets the criteria map cmap and returns a second function, which in turn gets a license entry lentry and checks whether all criteria conditions match.

Since where is used along with filter in find-matches, all licenses compatible with the criteria map cmap are returned:

(map :id (find-matches cmap *licenses*))
=> (:afl-v30 :apl-v20 :cddl-v10 :epl-v10 :lgpl-v30 :mpl-v11)

What I like about Clojure — and functional programming languages in general — is the ridiculous amount of code it takes to solve certain problems. This algorithm, for example, has only 15 (really short) lines of code. Where is your God now?! :-P

You can see the whole thing in Github.

Sobre Daniel Martins

Fundador da Destaquenet, ele é graduado em Sistemas de Informação e desenvolve softwares como hobby e profissão desde 2000. Especializado na plataforma Java, ele utiliza a tecnologia há vários anos, sendo programador e desenvolvedor web certificado pela Sun Microsystems, recentemente adquirida pela Oracle. Também se interessa por assuntos ligados à cultura open source, metodologias ágeis, engenharia de software, frameworks e linguagens dinâmicas tais como Python, Ruby e Smalltalk.
Esta entrada foi publicada em English, Programming e marcada com a tag , , , , , , , , . Adicione o link permanente aos seus favoritos.

Deixe um Comentário

O seu endereço de email não será publicado Campos obrigatórios são marcados *

*

Você pode usar estas tags e atributos de HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre lang="" line="" escaped="" highlight="">