re-design classes to support automatic code generation
dedbox opened this issue · 1 comments
The "old" (or current as of this writing) implementation of classes and class instances eschews types entirely, so I wind up manually resolving each use of a class member to a particular instance of the class. This makes for an awkward programming experience.
In practice, old classes are approximately first-order abstractions -- a simple class like Functor
is useful in limited contexts, but higher order classes like Arrow
or V
(abstract vector spaces) are impractical. To fully express the constructs in Haskell's diagrams library as classes, Algebraic Racket needs a "new" class syntax that actively supports higher order abstractions.
An illustrative example in the old syntax:
;;; functor.rkt
(class Functor
[fmap]
minimal ([fmap]))
;;; box-functor.rkt
(define-syntax box-Functor
(instance Functor
[fmap (λ (f b) (box (f (unbox b))))]))
;;; list-functor.rkt
(define-syntax list-Functor
(instance Functor
[fmap map]))
;;; maybe-functor.rkt
(define-syntax Maybe-Functor
(instance Functor
[fmap (function*
[(_ Nothing) Nothing]
[(f (Just a)) (Just (f a))])]))
;;; functor-dispatch.rkt
(define fmap
(let ([ box-fmap (with-instance box-Functor fmap)]
[ list-fmap (with-instance list-Functor fmap)]
[Maybe-fmap (with-instance Maybe-Functor fmap)])
(λ (f a)
((cond [( box? a) box-fmap]
[(list? a) list-fmap]
[(Maybe? a) Maybe-fmap]
[else (error "fmap: no instance for Functor")])
f a))))
Dynamic dispatch boilerplate is tedious to write by hand, but it can be generated automatically whenever what to generate and where can be deduced from the code. To this end, the "new" class form will move to Racket-style internal definitions and a generic form (:
) for member "type" annotations.
Here's the same example in the new syntax:
;;; functor.rkt
(require algebraic/types/dynamic)
(class Functor
#:minimal fmap
(: fmap (-> procedure? Functor? ... Functor?)))
;;; box-functor.rkt
(instance Functor Box
(: fmap (-> procedure? box? ... box?))
(define (fmap f . bs) (box ($ f (map unbox bs)))))
;;; list-functor.rkt
(instance Functor List
(define fmap map))
;;; maybe-functor.rkt
(require algebraic/types/simple)
(instance Functor Maybe
(: fmap (-> (-> a b) (Maybe a) (Maybe b)))
(define fmap
(function*
[(_ Nothing) Nothing]
[(f (Just a)) (Just (f a))])))
Note the absence of functor-dispatch.rkt
in this version, which uses a hypothetical algebraic/types/dynamic
module to associate contracts bound by :
to member definitions. It would generate code that looks something like this:
;;; functor.rkt
(define-syntax Functor (class-transformer #'Functor ...))
(define Functor? (|| box? list? Maybe?))
(define/contract (fmap f . args) (-> procedure? Functor? ... Functor?)
($ (cond [(box? f) box-fmap]
[(list? f) list-fmap]
[(Maybe? f) Maybe-fmap])
f args))
;;; box-functor.rkt
(define-syntax Box-Functor (class-instance-transformer #'Functor #'Box ...))
(define/contract (box-fmap f . bs) (-> procedure? box? ... box?)
(box ($ f (map unbox bs))))
;;; list-functor.rkt
(define-syntax List-Functor (class-instance-transformer #'Functor #'List ...))
(define/contract list-fmap (-> procedure? Functor? ... Functor?) map)
;;; maybe-functor.rkt
(define-syntax Maybe-Functor (class-instance-transformer #'Functor #'Maybe ...))
(define Maybe-fmap
(function*
[(_ Nothing) Nothing]
[(f (Just a)) (Just (f a))]))
How to handle unconstrained domain?
Ex:
(define return #:-> (unconstrained-domain-> Monad?) pure)
I see two obvious choices:
- add keyword argument
#:unconstrained-domain->
- treat
unconstrained-domain->
at the top level as a special case
Either way, each of the many kinds of ->
-like contracts would require work.
A more generic keyword argument (e.g. #::
) avoids the issue and broadens its applicability to non-procedure values.
Ex:
(define return #:: (unconstrained-domain-> Monad?) pure)