Ryan got me thinking about the Racket class system

Ryan mentioned something along the lines of Racket's class system being under appreciated in his RacketCon talk, so I've begun to look into it a bit more.

I spent three decades, one in each of C++, Java & Ruby, mostly focused on object oriented programming. Then my pendulum swung heavy into the functional world, and I didn't really look back.

The timing of Ryan's comment was perfect. I've just realized I've been creating "an ad hoc, informally-specified, [ hopefully not ] bug-ridden" implementation of some OO concepts for a recent project. Functional programming works really well for the vast majority of my code, but sometimes the nail really is an OO nail :slight_smile:

I'm currently reading Scheme with Classes, Mixins, and Traits which I found in the Racket Guide. Feel free to respond with any other sources of info on Racket OO.

5 Likes

Probably a bit too tangential but I thought this was cool: http://shivers.com/~shivers/scheme04/tmp/scheme04/article/06-java2scheme.pdf

1 Like

@soegaard passed on another paper:

Super and Inner — Together at Last!

1 Like

If you don't mind sharing, I'm curious what aspects of racket/class you found yourself to be Greenspun-10th-ruling-ing?


Why I ask:

Your history sounds very similar to mine. :slight_smile:

For me the pendulum is still very much away from OO: I find struct (usually plain, sometimes with struct inheritance or generics) + modules to be sufficient and preferable.

But enough time has elapsed to make me wonder if that's justifiably due to the kinds of programs I'm writing (e.g. I haven't done something like the db package's support of various engines), or unjustifiable bias.

I'm not trying to "debate OO" or asking you to defend or persuade or anything like that! I just like to hear other people's experiences and think about them.

2 Likes

I have a new application that is mostly generic. Customers have Users. User's add Providers. The Providers are notified they have been requested to provide information. Providers either accept and submit a form of fields, or decline.

Although it's mostly generic, there is some specialization per Customer. For the most part, I was able to handle the specialization by parameterizing Customers with static data (e.g. a list of questions, field types, min/max number of providers per user, etc.); however, I also need some simple behavioral specialization.

I've always used structs for my database models, so to accomplish the latter, I had a struct that subtyped the Customer struct and added a number of functions. A factory function would create and populate this struct with functions specific to the customer. For example (we need to fix the code highlighter!):

(struct q customer (activate-user
                    get-user-data
                    display-link?
                    application-css
                    application-name
                    validate-provider-meta-data
                    validate-provider-customer-data
                    ...)
  #:transparent)

(define/contract (build-q customer-obj)
  (-> customer? q?)
  (match (customer-username customer-obj)
    [ "tla" (build-q-tla customer-obj) ]
    [ _     (error "build-q: invalid customer") ]))

(define (build-q-tla obj)
  (q (customer-id obj)
     ; other customer fields
     ...
     activate-user  ; not quest specific
     get-user-data  ; not quest specific
     display-link?  ; not quest specific
     tla:application-css
     tla:application-name
     tla:validate-provider-meta-data
     tla:validate-provider-quest-data
     ...))

It was efficient (no dynamic method lookup), simple, and worked ok because it was a flat class hierarchy of just one level. A typical use:

((q-get-user-data obj) conn obj reference-key type)

Which is now:

(send obj get-user-data conn obj reference-key type)

And... as I typed that, I just realized I probably don't need the second obj now since get-user-data can refer to this :slight_smile:

I'm pretty sure I'll need a deeper class hierarchy, and while I was contemplating this, I saw Ryan's talk :slight_smile: Adding method dispatch for a deeper hierarchy is where I saw I was re-inventing the wheel too much.

It was trivial to convert my code to use Racket classes. For the time being, I'm going to continue to use simple structs for my database records, and there was one small advantage of my previous approach - I could use a q where a customer was needed, but with my current approach, rather than q subtyping customer it composes a customer.

Although Rails use of object orientation has some niceties, for the most part, I felt the magical implicit aspects went too far. I'll need further research to determine whether I want to use classes for database records vs. my current structs, but for the simple behavioral specialization I needed, classes make sense to me, and my code is now much more maintainable.

Kudos to the folks who created Racket's class system! It's nice to have that tool for the cases where it fits well, and I love how it feels more Rackety than previous OO systems I've used.

Brian

2 Likes

I asked about the performance of Racket classes in this post on Reddit. One enlightening answer came from /u/ryan017

A class is implemented as a struct type with a struct type property that holds the method tables, etc. An object is implemented as an instance of the class's struct type. The OO features add some overhead, so using structs directly and using functions instead of methods will generally be faster. The overhead is not huge, though, so don't avoid objects if an OO solution fits your problem.
More details:

  • A call to a public method using send is slower than a function call. The method call (usually) turns into a struct property lookup, a hash table lookup, a vector access, and then a function call. The final function call is to a computed function, which is not optimized the same way that direct calls to known functions are.
  • A call to a public method within a class (on the same object, without using send ) is faster. It is equivalent to a vector access and then a function call.
  • A call to a private (or public-final , I think) method within a class is the same as a function call.
  • Object construction is more complicated than struct instance construction. IIRC simple struct constructors (ie, without guards) are especially optimized by Racket's compiler.
  • A field access using get-field is slower than call to a struct accessor. It is roughly the same as a public method call. Likewise for set-field! .
  • A field access within a class is the same as a struct accessor call. Likewise, a field assignment is a struct mutator call.
  • If contracts are involved, then read the paper soegaard mentioned to find out what happens.

The paper in question from the last point is the one linked by @badkins

4 Likes