Résumé des épisodes précédents : une image est une fonction bornée qu'il est possible de représenter à l'aide d'une classe Java. L'écriture de cette classe passe par un grand nombre de lignes de code, même en Clojure. Explorons aujourd'hui la possibilité d'une façon plus canonique de faire la même chose.
Une fonction bornée est une fonction comportant une information supplémentaire : son domaine de définition. Clojure fournit un façon pratique d'ajouter des données : la méta-donnée (metadata). Une méta-donnée est une map de données qui peut être étendue à l'aide de la fonction with-meta. Tentons d'ajouter un domaine de définition à une fonction :
user> (defn indom? [dom pt] (let [[start end] dom [x y] pt] (and (>= x (first start)) (<= x (first end)) (>= y (second start)) (<= y (second end)))))Malheureusement l'approche ne peut aboutir car'user/indom?
user> (defn f [x y] (+ x y))
'user/f
user> (def dom [[0 0] [4 4]])
'user/dom
user> (def bf (with-meta f {:domain dom})) java.lang.UnsupportedOperationException (NO_SOURCE_FILE:10)
metadata ne fonctionne qu'avec des symboles et des collections. Après quelques recherches (merci Google), j'ai déniché un contournement de la difficulté proposé par Philipp Calçado : utiliser proxy pour ajouter une méta-donnée à un objet Java quelconque. Le proxy doit étendre clojure.lang.IFn et clojure.lang.IObj, en implémentant les méthodes meta et withMeta. La méthode meta doit renvoyer la méta-donnée de l'objet. La méthode withMeta doit renvoyer une méta-donnée augmentée :
user> (def bf (let [metadata {:domain dom}] (proxy [clojure.lang.IFn clojure.lang.IObj] [] (withMeta [meta] (merge metadata meta)) (meta [] metadata) (invoke [x y] (if (indom? dom [x y]) (f x y) 0)))))Parfait! Mais pourquoi se restreindre à l'ajout d'un domaine de de définition? Prenons un peu de champ et généralisons l'ajout de méta-donnée à une fonction :'user/bf
user> (bf -1 1) 0 user> (bf 1 2) 3 user> (defn domain [img] (:domain (meta img)))
'user/domain
user> (domain bf) [[0 0] [4 4]] user> (with-meta bf {:a 1}) {:a 1, :domain [[0 0] [4 4]]} user>
user> (defn with-meta-fn [fun metadata] (proxy [clojure.lang.IFn clojure.lang.IObj] [] (withMeta [meta] (with-meta-fn fun (merge metadata meta))) (meta [] metadata) (invoke [& args] (apply fun args))))'user/with-meta-fn
La fonction with-meta-fn permet d'ajouter n'importe quelle méta-donnée à une fonction, comme le ferait la fonction with-meta. Ainsi la construction d'une fonction bornée est plus lisible :
user> (defn make-bounded-function [dom fun] (with-meta-fn (fn [x y] (if (indom? dom [x y]) (fun x y) 0)) {:domain dom}))FMN.'user/make-bounded-function
user> (def bf (make-bounded-function dom f))
'user/bf
user> (bf 1 1) 2 user> (domain bf) [[0 0] [4 4]]