I will use a simple example to illustrate my question. In Java, C, or any other OOP language, I could create a pie
class in a way similar to this:
class Apple{
public String flavor;
public int pieces;
private int tastiness;
public goodness(){
return tastiness*pieces;
}
}
What's the best way to do that with Scheme? I suppose I could do with something like this:
(define make-pie
(lambda (flavor pieces tastiness)
(list flavor pieces tastiness)))
(define pie-goodness
(lambda (pie)
(* (list-ref pie 1) (list-ref pie 2))))
(pie-goodness (make-pie 'cherry 2 5))
;output: 10
...where cherry is the flavor, 2 is the pieces, and 5 is the tastiness. However then there's no type-safety or visibility, and everything's just shoved in an unlabeled list. How can I improve that?
Sidenote: The make-pie procedure expects 3 arguments. If I want to make some of them optional (like I'd be able to in curly-brace languages like Java or C), is it good practice to just take the arguments in as a list (that is treat the arguments as a list - not require one argument which is a list) and deal with them that way?
Update:
I've received a couple answers with links to various extensions/libraries that can satisfy my hunger for OOP in scheme. That is helpful, so thank you.
However although I may not have communicated it well, I'm also wondering what the best way is to implement the pie
object above without such classes or libraries, so I can gain a better understanding of scheme best practices.
In some sense, closures and objects are equivalent, so it's certainly possible. There are a heaping helping of different OO systems for Scheme -- a better question might be which one to use!
On the other hand, if this is an educational exercise, you could even roll your own using the closure-object equivalency. (Please forgive any errors, my Scheme is rather rusty.)
(define (make-pie flavor pieces tastiness)
(lambda (selector)
(cond ((eqv? selector 'flavor) flavor)
((eqv? selector 'pieces) pieces)
((eqv? selector 'tastiness) tastiness)
((eqv? selector 'goodness) (* pieces tastiness))
(else '()))))
This is a simple constructor for a pie
object. The parameter variables flavor
, pieces
and tastiness
are closed over by the lambda expression, becoming fields of the object, and the first (and for simplicity's sake here, only) argument to the closure is the method selector.
That done, you can instantiate and poke at some:
> (define pie1 (make-pie "rhubarb" 8 4))
> (define pie2 (make-pie "pumpkin" 6 7))
> (pie1 'flavor)
"rhubarb"
> (pie1 'goodness)
32
> (pie2 'flavor)
"pumpkin"
Many Schemes allow you to define classes that contain fields and methods. For example, see:
- Bigloo Object System
- PLT scheme Classes and Objects
This is how I would recommend implementing this:
(define PersonClass (lambda (name age strength life)
(let ((name name)(age age) (life life) (strength strength))
(lambda (command data)
(cond
((< life 1)
"I am dead")
((equal? command "name")
name)
((equal? command "age")
age)
((equal? command "birthday")
(set! age(+ age 1)))
((equal? command "receive damage")
(begin (set! life(- life Data)) (display "I received damage\n")))
((equal? command "hit")
(data "receive damage" strength))
)))))
Use it like such: (Karl "name" 0)
Most schemes support SRFI-9 records or the similar R7RS records, and R6RS also provides records with slightly different syntax. These records are a way to make new types in scheme. In addition, most schemes, together with R6RS and R7RS, support modules or libraries, which are one way to encapsulate operations on such types.
Many scheme programmers use these instead of OOP to write their programs, depending on the nature of the application. The record provides the type and its fields; an associated procedure is provided which creates new objects of this type; other procedures which take the record as an argument (conventionally the first argument) provide the required operations on the type; and the module/library definition determines which of these are exported to user code and which are private to the implementation of the module/library.
Where a field of the record is itself a procedure, it can also have private data as a closure: but often you want to use the module definition for data hiding and encapsulation rather than closures (it is also usually more efficient).