Só quem desenvolve ou contribui com projetos open source sabe como é legal. A pior parte é definir sob qual licença soltar o código, e essa tarefa é muitas vezes negligenciada por desenvolvedores de software.
Para quem não viu, recentemente colocamos no ar o Licensator, uma aplicação web que, além de reunir informações sobre as licenças open source mais populares, é capaz de indicar as licenças apropriadas para cada caso.
A linguagem escolhida para implementar o projeto foi Clojure, que se mostrou excelente para resolver esse tipo de problema. Veremos hoje como o principal algoritmo da aplicação funciona.
REPL: onde tudo começa
Um dos pontos fortes das linguagens dinâmicas é a facilidade em transformar idéias em código funcional, e com Clojure não é diferente. A idéia por trás do Licensator surgiu justamente em uma dessas sessões de codificação no REPL, o ambiente interativo da linguagem. Após alguns minutos brincando, vi que o problema era simples de resolver.
Apesar do elevado número de licenças, podemos facilmente extrair informações comuns a todas elas, como: se a licença é copyleft, se pode ser usada por código fechado (proprietário), entre outras coisas.
Por exemplo, vamos pegar a licença Apache v2.0:
(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]}])
A lista completa de licenças pode ser vista aqui.
Considerando que todas as licenças têm esse mesmo conjunto de informações, como podemos saber quais licenças não são recíprocas e possuem cláusulas explícitas de copyright?
(map :id (filter #(and (:copyright %) (not (:copyleft %))) *licenses*)) => (:afl-v30 :apl-v20 :epl-v10 :freebsd :mit :new-bsd :bsd)
Se você conhece um pouco sobre programação funcional, este código deve ser fácil de acompanhar. De qualquer forma, o que ele faz é retornar o :id de todas as licenças onde :copyright é verdadeiro e :copyleft é falso.
Outro exemplo: quais licenças são compatíveis com Apache v2.0 e 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)
Aqui usamos a namespace clojure.set, que conta com algumas funções úteis para álgebra relacional. O código é muito parecido com o anterior; a mudança é que agora usamos a função subset? para buscar as licenças compatíveis com [:apl-v20 :cddl-v10].
Último exemplo: quais licenças recíprocas são compatíveis com 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)
Neste ponto já se pode perceber que não existe mágica nenhuma por trás do algoritmo; o que ele faz é filtrar as licenças com base numa função.
A solução
O código que acabamos de ver funciona, mas não é apropriado para resolver o problema uma vez que a lógica de consulta se encontra fixa no código.
O ideal seria se os critérios de busca fossem expressados como um map…
(def cmap {:patent true, :closed-source-linking true, :compatible-with [:mit :new-bsd]})
…portanto precisamos de uma função que faça a filtragem das licenças com base nesse mapa, algo como:
(defn find-matches [cmap data] (filter (where cmap) data))
No caso, essa função where, que ainda não definimos, deve retornar uma função que filtre os elementos (licenças) em data de acordo com o mapa de critérios cmap.
Vamos por partes. Quais condições em cmap são verdadeiras para a licença Apache v2.0?
(defn criteria-matches? [license [kval cval]] "Verifica se o critério 'centry' é verdadeiro para a licença '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)
Uma licença só pode ser considerada compatível com os critérios se a lista retornada contém apenas valores verdadeiros. Portanto, neste caso, a licença Apache v2.0 é considerada compatível com os critérios informados:
(every? true? (map (partial criteria-matches? apl-v20) cmap)) => true
Logo, uma possível implementação para a função where seria:
(defn where "Retorna função que diz se todos os critérios em 'cmap' são verdadeiros para a licença '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))))
Este código é uma junção dos dois anteriores. A função where recebe o mapa cmap com os critérios de pesquisa, e retorna uma segunda função. Esta função recebe uma licença lentry e verifica se todos os critérios informados são verdadeiros para esta licença.
Como where é usada em conjunto com filter na função find-matches, isso fará com que todas as licenças compatíveis sejam retornadas:
(map :id (find-matches cmap *licenses*)) => (:afl-v30 :apl-v20 :cddl-v10 :epl-v10 :lgpl-v30 :mpl-v11)
Uma das coisas legais de Clojure — e outras linguagens funcionais — é a quantidade ridícula de código necessário para resolver certos problemas. Este algoritmo, por exemplo, tem apenas 15 linhas (muito curtas) de código. Onde está seu Deus agora?!
O código da aplicação está no Github para quem tiver interesse em ver o restante do código.
Posts em Português
Posts in English
Pingback: Tweets that mention Entendendo Licenças Open Source com Clojure | DestaqueBlog -- Topsy.com