technoblogy / ulisp-esp

A version of the Lisp programming language for ESP32-based boards.
MIT License
110 stars 37 forks source link

object-oriented uLisp #72

Open dragoncoder047 opened 1 year ago

dragoncoder047 commented 1 year ago

Continuing the discussion from https://github.com/technoblogy/ulisp-esp/issues/71#issuecomment-1523873453:

I also have some ideas for how to make uLisp object-oriented

Have you seen this?

Simple object system

Yes, but a) it only supports single inheritance because it's prototype-based (can't have multiple parents / "mixins") and b) it doesn't support setf'ing slots ((setf (value obj 'foo) new-value) would kind of make sense but it doesn't work so you have to use update). Also, calling a method on an object is more verbose because you have to do ((value obj 'key) obj arg1 arg2 ...) instead of just (obj :key arg1 arg2 ...). You went to the length of defining a "generic area function" too but that just seems unnecessary. I'm all for reducing the number of required parenthesis especially in a parenthesis-heavy language like Lisp.

Another potential problem with this I can see is having the special key 'parent being used to point to the prototype of the object, seems like it would be prone to collisions if someone did something where it makes sense to point to a parent, such as nested parsers for something (e.g. JSON).

But I do think this is a good starting point: an object can be represented as an assoc list of its slots, and the tail of the assoc list somehow points to the prototype(s).


Okay, my idea here:

First off, generic keyword support needs to be added.

My idea is that a new type (in the type enum) called OBJECT would be defined. The cdr of the actual OBJECT cell would point to a cons pair. The car of that cons pair would point to the slots array, and the cdr would point to an (improper) list of prototypes.

Then two changes need to be made to eval(), right before the not valid here error case:

  1. If the called value is a keyword, there must be one argument, an object, and the keyword is looked up in the object's slots array and returned.
  2. If the called value is an object, there must be at least one argument, and the first argument must be a keyword which is looked up in the object's slots array as usual, and the slot must contain a lambda with at least one argument, the first argument will be the object (i.e. a "this" or "self" parameter). Basically, (obj :foo 1 2 3) desugars into ((:foo obj) obj 1 2 3). Of course this longer syntax would still be possible and would also be the only way to get the equivalent of Javascript's Function.call.

Hope that helps.

technoblogy commented 1 year ago

I welcome your comments on my simple object system.

can't have multiple parents / "mixins" it doesn't support setf'ing slots

Yes, obviously CLOS has these, but I was trying to create the minimum system that would actually be useful for something like my Mini text adventure game.

You went to the length of defining a "generic area function" too but that just seems unnecessary.

How would you implement the example without this?

First off, generic keyword support needs to be added.

Do you mean add keyword support to uLisp, or add keyword support to the simple object system?

What extra would this give you beyond using symbols starting with a colon as markers?

dragoncoder047 commented 1 year ago

How would you implement the example without this?

Ideally it would be with keywords - you would do (:area object) if it is just a property, or (object :area) if it is a method with no arguments (as opposed to (area object) whose only advantage in terms of readability is it doesn't have a colon).

Do you mean add keyword support to uLisp, or add keyword support to the simple object system?

What extra would this give you beyond using symbols starting with a colon as markers?

Keywords to uLisp as a whole. Any symbol starting with a colon evaluates to itself -- that's all.

The advantage is (besides also being able to do &key arguments) is that special forms that don't evaluate their arguments won't get confused if someone puts in 'foo instead of just foo. That way, since keywords evaluate to themselves, the special form CAN evaluate some of its arguments so the user can use variables.

dragoncoder047 commented 1 year ago

I just had another idea last night.

Consider this line:

https://github.com/technoblogy/ulisp-esp/blob/f22e87e8abbec16fdb72ea59a7feef160e4db70b/ulisp-esp-comments.ino#L6826

The meaning of this is that builtin symbols evaluate to themselves, that is:

> foo
Error: undefined: foo
> print
print

However, I was kind of expecting the output of what Python and Common Lisp do when printing a function:

>>> print(print)
<built-in function print>
CL-USER> (print #'print)
#<SYSTEM-FUNCTION PRINT> 

But within uLisp's execution model, this ends up working. It is because of this if case:

https://github.com/technoblogy/ulisp-esp/blob/f22e87e8abbec16fdb72ea59a7feef160e4db70b/ulisp-esp-comments.ino#L6839

Essentially what ends up happening is that the symbol evaluates to itself, and then the symbol is called as if it was a lambda.

A good start on objects, I think, would be to implement another type, C_FUNCTION, which is identical to SYMBOL and would be treated as such by functions that deal with and expect symbols, but when a true SYMBOL is evaluated, it is changed into a C_FUNCTION, and when a C_FUNCTION is printed, it prints <c-function print> instead of just print.

What do you think?

technoblogy commented 1 year ago

I think that would work, but I'm not sure what the benefit would be.

Also, would you still be able to do this?

http://forum.ulisp.com/t/extending-ulisps-built-in-operators/1181

dragoncoder047 commented 1 year ago

Also, would you still be able to do this?

http://forum.ulisp.com/t/extending-ulisps-built-in-operators/1181

Certainly, since the mechanism would be the same (all the change really does is change how the built-in symbols are printed). The point here is that it seems a bit strange to be calling a symbol as if it was a lambda or function in a language like Lisp.

Another thing this kind of "does" is make it as if each symbol is bound by default in the global environment to a C function, just in a way that takes zero workspace, and also prevents them from being accidentally makunbound'ed.