0

Je veux écrire un code générique traitant des groupes de réflexion, et donc besoin de mettre en place des types qui reflètent des structures mathématiques (espace vectoriel, espace affine, ...). Comme je veux vraiment refléter ces structures fidèlement dans les types, j'ai besoin d'un moyen de définir une sorte de type paramétrique.Types dépendants/polymorphisme paramétrique en Common Lisp?

Ainsi, en particulier, je veux être en mesure d'écrire le code suivant

(defclass RealVectorSpace() 
    ((V :accessor underlying-set 
     :type Set) 
    (vector-add :accessor add 
       :type (function ((set-as-type V) (set-as-type V)) (set-as-type V))) 
    (scalar-mult :accessor s-mult 
       :type (function (real (set-as-type V)) (set-as-type V))) 

qui devrait spécifier un nouveau RealVectorSpace type qui serait donné par un triple (V vecteur ajouter scalaire) où V peut être n'importe quoi, et vector-add est une fonction prenant deux paramètres de type V (sic) qui évalue à quelque chose de type V.

Bien sûr, ce type ne serait pas tout à fait un reflet fidèle du concept d'un l'espace vectoriel réel, car vector-add et scalar-mult devraient encore satisfaire certaines autres propriétés. Mais même transformer ce «rêve» ci-dessus en un vrai code m'échappe.

Edit: En réponse à la réponse de sds, permettez-moi de mettre en avant les précisions suivantes de ma question initiale: en un mot, il semble que je dois avoir types dépendants dans Lisp, qui est différent de demander seulement pour classes paramétriques. En fait, Haskell a des types paramétriques mais n'a pas (du moins c'est pas intégré de manière évidente) de types dépendants. Le manque de types dépendants dans Haskell est par exemple discuté here.

1. Quelqu'un peut-il m'aider à transformer mon rêve en code?

2. J'ai entendu dire que vous n'avez pas besoin de classes paramétriques en Lisp à cause des macros Lisp. Si c'est vrai, quelqu'un peut-il expliquer comment vous utiliseriez defmacro pour implémenter/simuler des classes paramétriques en Common Lisp?

+1

Puisque Common Lisp n'a aucun système de type utile pour cela et en particulier les types dépendants, cela n'a pas beaucoup de sens. Je propose d'utiliser un langage de programmation avec un système de type élaboré comme Haskell ou quelque chose comme Axiom (http://fricas.github.io/api/VectorSpaceBasis.html). –

+0

Bien sûr, Common Lisp n'a pas de système de type utile. Mais la question est: que signifie "a"? Par exemple, même sans CLOS, vous pouvez facilement créer votre propre OO à l'aide de fermetures lexicales. – BlenderBender

+0

si vous avez une question spécifique, mieux avec le code, puis demander. Sinon, je crains qu'une large question spéculative ne convienne pas à Stackoverflow, qui se concentre sur de vrais problèmes de programmation pratique. –

Répondre

0

On y va: une réponse partielle/solution à ma question (pourquoi partiel? Voir ci-dessous). Merci à SD pour m'avoir aidé à comprendre cela! Permettez-moi de commencer par clarifier. Lorsque j'ai posé ma question initialement, j'ai utilisé le terme «type paramétrique» de manière imprécise, n'ayant qu'une définition vague comme «un type de paramètre dépendant». Je essentiellement voulais un gadget me permettant d'écrire le pseudo-code suivant (dans un pseudo-langue):

class List<T> { 
    //implementation 
}; 
l = new List<string>; 
l.push("Hello World!"); 

Donner un sens du pseudo-code ci-dessus est assez straight-forward (voir la réponse de sds). Cependant, l'ambiguïté se pose si l'on commence à se demander si les expressions List<T> et List devraient avoir une signification. En effet, en C++ par exemple, les expressions seraient non définies, l'effet de la définition du modèle

template <typename T> 
class List { 
public: 
    T car; 
    List<T> *cdr; 
}; 

est comme définissant séparément, pour chaque type T, un type List<T>. En revanche, dans des langages comme Java qui implémentent types génériques, l'expression List<T> (où T est une variable libre) est logique et désigne un type, à savoir le type générique d'une liste sur un certain typeT, de sorte que l'on pouvait par exemple écrire une fonction polymorphes comme

T car(List<T> l) { 
return l.car; 
} 

En bref, en C++, nous avons seulement la collection (infinie) de tous les types List<T>T fonctionne sur tous les types, alors que dans Java cette collection elle-même existe comme un objet dans la langue, comme le type générique List<T>.

Maintenant à ma solution partielle proposée, dont je vais esquisser brièvement les mots avant d'écrire le code Lisp réel. La solution est basée sur familles de types et la somme dépendante de telles familles, à savoir nous allons interpréter un type paramétrique comme le type List<T> ci-dessus en tant que fonction List d'un argument dont les valeurs sont des types, et nous feignons le Java type générique de style List<T> comme type de somme dépendant DepSum(List), qui se compose simplement de paires (a,b)a est un certain type et b est de type List(b).

Revenant à l'exemple de définir un véritable espace vectoriel sur un ensemble X, je voudrais écrire quelque chose comme

(defclassfamily RealVectorSpaceOver (X)() 
    ((add :initarg :add 
     :reader add 
     :type (function (X X) X)) 
    (s-mult :initarg :s-mult 
      :reader s-mult 
      :type (function (real X) X))) 

déterminant pour moi une RealVectorSpaceOver de fonction qui, étant donné une classe A, renverrait une classe comme si elle est définie manuellement par

(defclass RealVectorSpaceOverA() 
    ((add :initarg :add 
     :reader add 
     :type (function (A A) A)) 
    (s-mult :initarg :s-mult 
      :reader s-mult 
      :type (function (real A) A))) 

Fondamentalement, je pourrais copier-coller-n la solution de sds ici, mais il y a deux problèmes. Tout d'abord, le résultat ne serait pas une fonction (effet secondaire sans), par exemple la forme

(typep (make-instance (RealVectorSpaceOver A) 
         :add (lambda (x y) nil) 
         :s-mult (lambda (x y) nil)) 
     (RealVectorSpaceOver A)) 

évaluerait à nil parce qu'il ya deux appels à RealVectorSpaceOver ici, et chaque appel crée une nouvelle (et donc différent) classe. Ainsi, nous devons envelopper cette fonction dans un code qui met en cache le résultat une fois qu'il a été appelé pour la première fois.

L'autre problème est que l'utilisation de defclass pour créer des classes a pour effet de modifier l'espace de nom de classe, redéfinissant éventuellement les classes existantes. Pour éviter cela, on peut créer de nouvelles classes directement en instanciant la méta-classe standard-class. Par exemple

(make-instance 'standard-class 
       :name (intern "B") 
       :direct-superclasses '(A) 
       :direct-slots '((x :initargs (:x) :readers (x)))) 

est équivalent à

(defclass B (A) 
     ((x :initarg :x :reader x))) 

mais ne redéfinir une classe préexistante B. Notez que le format de l'argument :direct-slots diffère légèrement du format defclass pour la définition des emplacements. En utilisant une fonction d'assistance canonicalize-direct-slot-defs qui transforme ce dernier dans l'ancien (tiré du livre « L'art du Protocole metaObject »), la macro defclassfamily peut être mis en œuvre comme suit:

(defmacro defclassfamily (name variables superclasses slot-defs) 
    (let ((stripped-variables (strip-variables variables)) 
     (variable-types (types-of-variables variables)) 
     (type-decls (type-decls-from-variables variables))) 

    `(flet ((f ,stripped-variables 
      (make-instance 'standard-class 
          :name (intern (format nil "~S<~S>" ',name (list ,@stripped-variables))) 
          :direct-superclasses ,superclasses 
          :direct-slots ,(canonicalize-direct-slots slot-defs)))) 
     (let ((g (cache-function #'f))) 
     (defun ,name ,stripped-variables 
      ,@type-decls 
      (the standard-class (funcall g ,@stripped-variables))) 
     (defmethod argument-signature ((x (eql #',name))) 
      ',variable-types))))) 

Le code ci-dessus d'abord définit une fonction f représentant la famille de types souhaitée, puis crée une version g en cache en utilisant une fonction d'assistance cache-function (insérez votre propre implémentation), puis définit une nouvelle fonction dans l'espace de nom en utilisant defun, en appliquant les types pour les arguments (defclassfamily accepte arguments typés similaires à defmethod, de sorte que (defclassfamily F ((X Set) Y) ... définirait une famille F de t wo paramètres, avec le premier étant de type Set) et la valeur de retour de la famille de classe. De plus, il existe une fonction d'assistance simple strip-variables, types-of-variables, et type-decls-from-variables qui transforme l'expression en fonction des variables de la famille de type ((X Set) Y dans l'exemple précédent). Ils sont définis comme suit:

(defun strip-variables (specialized-lambda-list) 
    (mapcar (lambda (x) 
      (if (consp x) 
       (car x) 
       x)) 
      specialized-lambda-list)) 
(defun types-of-variables (var-declarations) 
    (mapcar (lambda (var-declaration) (if (consp var-declaration) (second var-declaration) t)) var-declarations)) 
(defun type-decls-from-variables (var-declarations) 
    (mapcar (lambda (var-declaration) 
      (if (consp var-declaration) 
       `(declare (type ,(second var-declaration) ,(first var-declaration))) 
       `(declare (type t ,var-declaration)))) 
      var-declarations)) 

Enfin, nous enregistrons les types des arguments tirés par notre famille en utilisant une méthode argument-signature, de sorte que

(argument-signature (defclassfamily F ((X Set) Y) ...)) 

évaluerait à (Set t).

La somme dépendante d'une famille de type d'un paramètre est mis en œuvre par le code suivant:

(defclass DepSum (standard-class) 
    ((family :initarg :family :reader family) 
    (arg-type :initarg :arg-type :reader arg-type))) 
(defmethod make-instance :before ((sum-class DepSum) &key pr1 pr2) 
    (assert (and (typep pr1 (arg-type sum-class)) 
       (typep pr2 (funcall (family sum-class) pr1))))) 
(defmethod sb-mop:validate-superclass ((class DepSum) (super-class standard-class)) 
    t) 
(defun depsum (f) 
    (let ((arg-type (car (argument-signature f)))) 
    (make-instance 'DepSum 
        :name (intern (format nil "DepSum_{x:~A} ~A(x)" arg-type f)) 
        :direct-superclasses() 
        :direct-slots `((:name pr1 :initargs (:pr1) :readers (pr1) :type ,arg-type) 
            (:name pr2 :initargs (:pr2) :readers (pr2))) 
        :direct-slots `((:name pr1 :initargs (:pr1) :readers (pr1))) 
        :family f 
        :arg-type arg-type))) 

afin que nous puissions définir un type RealVectorSpace utilisant

(let ((rvs-type (depsum #'RealVectorSpaceOver))) 
    (deftype RealVectorSpace() 
    rvs-type)) 

et écrire

(make-instance (depsum #'RealVectorSpaceOver) :pr1 X :pr2 some-rvs-over-X) 

pour créer un objet de type RealVectorSpace. Le code ci-dessus fonctionne en créant une méta-classe (c'est-à-dire la sous-classe standard-class) DepSum, qui représente le type de toutes les sommes dépendantes et dont les instances sont des sommes dépendantes de familles spécifiques. La sécurité de type est renforcée par l'accrochage dans les appels comme (make-instance (depsum #'RealVectorSpaceOver) ...) via (defmethod make-instance :before ((sum-class DepSum .... Malheureusement, il semble que nous devons compter sur assert pour ce type de vérification (je n'étais pas en mesure de comprendre comment le faire fonctionner avec declare). Enfin, le code (defmethod sb-mop:validate-superclass ... est dépendant de l'implémentation (pour SBCL dans ce cas) et nécessaire pour pouvoir instancier des instances de DepSum comme (depsum #'RealVectorSpaceOver).

Pourquoi est-ce seulement une réponse partielle? Parce que je n'ai pas fait les axiomes de l'espace vectoriel du type de RealVectorSpaceOver (ou RealVectorSpace). En effet, une telle chose nécessiterait de donner une preuve réelle de ces axiomes en tant que partie de la donnée dans un appel à (make-instance (RealVectorSpaceOver X) .... Une telle chose est certainement possible dans des langages fantaisistes comme Coq, mais semble totalement hors de portée dans ce vieux désordre qui est Common Lisp.

3

Je doute que ce que vous voulez fait beaucoup de sens, mais comme un exemple de macro (ab) utiliser, ici vous allez:

(defmacro define-real-vector-space (type &optional name) 
    `(defclass ,(or name (intern (format nil "REAL-VECTOR-SPACE-~A" type)))() 
    ((V :reader underlying-set :initform ',type) 
     (vector-add :accessor add 
        :type (function ((x ,type) (y ,type)) => ,type)) 
     (scalar-mult :accessor s-mult 
        :type (function ((x real) (v ,type) => ,type)))))) 
;; sample underlying set: 
(deftype 3d() (array real (3))) 
;; use it: 
(macroexpand-1 '(define-real-vector-space 3d)) 
==> 
(DEFCLASS REAL-VECTOR-SPACE-3D NIL 
((V :READER UNDERLYING-SET :INITFORM '|3D|) 
    (VECTOR-ADD :ACCESSOR ADD :TYPE (FUNCTION ((X |3D|) (Y |3D|)) => |3D|)) 
    (SCALAR-MULT :ACCESSOR S-MULT :TYPE #'((X REAL) (V |3D|) => |3D|)))) 
(define-real-vector-space 3d) 

Répondre au commentaire:

Si vous voulez un vous êtes célibatairereal-vector-space classe, , essentiellement, demande pour les machines à sous et vector-addscalar-mult pour avoir le type qui dépend de la valeur du V emplacements
Cela impliquerait que (setf (underlying-set rvs) some-new-type) aurait doivent vérifier que (add rvs) et (s-mult rvs) doivent s'approprier type pour some-new-type. Essentiellement, cela signifie que soit chaque objet de type real-vector-space est immuable, de tous les emplacements sont modifiés simultanément. La première option peut être accomplie par un usage judicieux de MOP. Je ne suis pas sûr si ce dernier est possible en Lisp.

+0

C'est utile, mais pas ce que je veux: il devrait y avoir juste un type RealVectorSpace, pas un type RealVectorSpaceOverV pour chaque V. – BlenderBender

+0

@BlenderBender: s'il vous plaît voir modifier. – sds

+0

Merci. Pourriez-vous donner quelques indications sur la façon dont la MOP pourrait être utilisée pour réaliser ce que j'ai en tête? Le fait de parcourir la littérature en ligne sur le protocole de méta-objet ne m'a pas aidé à comprendre comment on pouvait l'utiliser. – BlenderBender

2

Vous pouvez lire LIL, la bibliothèque d'interface Lisp , décrit dans LIL: CLOS reaches higher-order, sheds identity and has a transformative experience de Faré Rideau. Le github page a plus de détails.Fondamentalement, LIL tente d'exprimer le polymorphisme paramétrique à travers un paramètre supplémentaire (l'interface , qui est comme une classe de type) qui peut être rendu implicite grâce à la portée dynamique. D'autre part, ce que vous voulez exprimer est très facile à faire avec les modules OCaml, donc en fonction de vos besoins, vous pouvez mieux suivre les conseils de Rainer Joswig (utiliser une autre langue).

module type VectorSpace = 
    functor (S : sig val dimension : int end) 
      (F : sig type scalar val zero : scalar end) -> 
    sig 
    type vector = F.scalar array 
    val add : vector -> vector -> vector 
    val mul : F.scalar -> vector -> vector 
    end 

En ce qui concerne les propriétés (comme demandé dans les commentaires), vous pourriez avoir besoin d'utiliser un système de type plus complexe (Coq?). Un exemple de la façon dont Common Lisp peut abstraitement bien sur les choses est Gábor MelisMGL-CUBE.

+0

Thx, semble intéressant. Vous dites "c'est vraiment facile à faire avec les modules OCaml". Comment feriez-vous les propriétés supplémentaires (axiomes d'espace vectoriel) une partie du type? – BlenderBender

+1

@BlenderBender Axiomes/propriétés sont plus difficiles à faire, mieux vaut poser une autre question aux experts OCaml. – coredump

+0

Je doute que ce serait même possible dans OCaml; Je ne pensais clairement pas directement quand j'ai posé cette question. – BlenderBender