racket-algebraic icon indicating copy to clipboard operation
racket-algebraic copied to clipboard

re-design classes to support automatic code generation

Open dedbox opened this issue 4 years ago • 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))]))

dedbox avatar Aug 14 '19 01:08 dedbox

How to handle unconstrained domain?

Ex:

(define return #:-> (unconstrained-domain-> Monad?) pure)

I see two obvious choices:

  1. add keyword argument #:unconstrained-domain->
  2. 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)

dedbox avatar Aug 14 '19 16:08 dedbox