Samstag, 15. März 2008

DLisp

I have chosen a scripting language for my game, and after having brief uninformed looks at MiniD, PyD and DLisp - I choose the most obscure, least maintained of the bunch - DLisp.

What is it?

It's a minimal Lisp implementation in D, taking full use of the builtin garbage collector of D. Sadly the author kind of faded away, I just hope he is healthy and alive. So for now I forked the codebase and will try to maintain and extend it. After some fiddling around unittests run again - but the test suite is fairly limited.

Since I want to use it as a scripting language for an object oriented game written in D, I decided to add builtin object orientation since I hope this will ease the exposition of game objects.

Edit: Why DLisp over the others?
So why did I choose the least maintained and complete scripting language available? Basically two reasons - First I started this Project to learn new stuff - and Python I use regularly, the languages inspired by D or simply Lua don't offer something new. So choosing a Lisp version and a tiny one offers the opportunity to learn something completely new. Second I needed a data description language - I know that lua and python can be used like that (though using Python using as a DSL feels like an abuse.) - but the others didn't struck me as such.

A third reason I might add, is that having intimate knowledge over the script languages
implementation enables me to fix bugs and odd behaviour right away.

Status

DLisp is fairly usable already, although the OOP code and syntax is still under heavy development.

Syntax snippets
;; Level Data
;; Mar. 2008
(setf *level*
(make-instance level-class "My Test Level"))

(defun place-tile (x y name)
"place a tile at x,y created from tile-prototype name"
(call-method *level* place-tile
(x y (make-instance (get-attr *dataset* name)))))

(map place-tile
((5 5 "red")
(5 5 "red")))

;; Method calling syntax?

;; Explicit - Works but ugly syntax
(call-method object 'method-name args)

;; Implicit
;; Problem is that we need 1 lookahead
;; and one cannot override macros.
(method-name object args)

;; Special syntax
;; Looks a bit unlispy - and overriding functions
;; would still need the implicit syntax ...
(object:method-name args)


Binding snippets
  class MyClass {

// Some constructors.
this() ...
this(string name) ...

// Some methods
void methodName(int arg1, int arg2) ...
string returnsString() ...

// BINDING CODE

// Generate the core binding
mixin BindClass!("MYCLASS");

// Default constructor is autogenerated
mixin BindConstructor!(string);

// Bind the methods.
mixin BindMethods!(methodName,returnsString);
}

9 Kommentare:

flithm hat gesagt…

I'm curious... did you check out Monster?

phoku hat gesagt…

Oh yes, I had a brief look at Monster, too - I forgot to mention that.

flithm hat gesagt…

Also out of curiosity... why DLisp over the others? Any particular reason(s)?

phoku hat gesagt…

Heya - the REAL reason for using DLisp is obviously a perverted hidden love for parentheses.

Apart from that I have added some puny throwaway explanation to the article itself.

h3r3tic hat gesagt…

*h3r3tic slaps phoku around a bit with a smelly trout for not using xpose

Sjoerd hat gesagt…

This is probably a bit of a large comment, anyway, this is a nice API I think to get dlisp OOP. If you need any help, I'd like to create a new macro editor, and dlisp could be quite practical.

; This model doesn't differentiate between objects and classes,
; it however does use a storage model in the form of namespaces
; or modules. This is not only practical for maintainability,
; but also for factories, which can be supplied by a certain
; namespace to use.

; Creates a namespace within other namespaces (an empty list
; indicates that the namespace is created in the root)

(new-namespace `namespace-name
`(parent-namespace parent-sub-namespace ...))

; Kills a namespace. All child namespaces and objects are
; killed first

(kill-namespace `namespace-name
`(parent-namespace parent-sub-namespace ...))

; The bare method for creating objects. Note that if there are
; no parents specified, "object" is taken as the default parent.

(new-slotted-object-ex `(slot-name-1 slot-name-2 slot-name-3)
`(parent-1 parent-2 parent-3)
`(
(prop-1 value-prop-1)
(prop-2 value-prop-2)
(prop-3 value-prop-3)))

; Same as above, stores the object into the most nested
; namespace with name "name-of-object"

(def-slotted-object-ex `name-of-object
`(namespace sub-namespace sub-sub-namespace)
`(slot-name-1 slot-name-2 slot-name-3)
`(parent-1 parent-2 parent-3)
`(
(prop-1 value-prop-1)
(prop-2 value-prop-2)
(prop-3 value-prop-3)))

; Same as new-slotted-object-ex, without properties (metadata)

(new-slotted-object `(slot-name-1 slot-name-2 slot-name-3)
`(parent-1 parent-2 parent-3))

; Same as def-slotted-object-ex, without properties (metadata)

(def-slotted-object `name-of-object
`(namespace sub-namespace sub-sub-namespace)
`(slot-name-1 slot-name-2 slot-name-3)
`(parent-1 parent-2 parent-3))

; Same as new-slotted-object-ex, with the property "locked" set
; Locked objects cannot be cloned

(new-slotted-object-locked `(slot-name-1 slot-name-2 slot-name-3)
`(parent-1 parent-2 parent-3))

; Same as def-slotted-object-ex, with the property "locked" set
; Locked objects cannot be cloned (this basically creates a singleton)

(def-slotted-object-locked `name-of-object
`(namespace sub-namespace sub-sub-namespace)
`(slot-name-1 slot-name-2 slot-name-3)
`(parent-1 parent-2 parent-3))

; Objects can be cloned (this is basically the way to create new objects
; from slotted objects), it uses new-slotted-object-locked as default
; instantiator.

(new-cloned-object `name-of-object
`(namespace sub-namespace sub-sub-namespace))

; Clones objects with new-cloned-object, and stores the object in the same
; namespace as the original object where the clone came from. This is
; practical for global objects.

(def-cloned-object `name-of-cloned-object
`name-of-object
`(namespace sub-namespace sub-sub-namespace))

; Retrieves an object from the namespace system

(get-object `name-of-object
`(namespace sub-namespace sub-sub-namespace))

; Kills an object from a namespace

(kill-object `name-of-object
`(namespace sub-namespace sub-sub-namespace))

; Appends a slot to an existing object.

(append-slot `slot-name
object)

; Same as append-slot, retrieves the object through the namespace system

(append-slot-indirect `slot-name
`name-of-object
`(namespace sub-namespace sub-sub-namespace))

; Kills a slot from an object, when an object

(kill-slot `slot-name
object)

; Same as kill-slot, retrieves the object through the namespace system

(kill-slot-indirect `slot-name
`name-of-object
`(namespace sub-namespace sub-sub-namespace))


; Gets the value from a slot. If the slot is not present, the parent slots
; are searched, until a match is found.

(get-slot-value `slot-name
object)

; Gets the value from a slot, retrieves the object through the namespace system

(get-slot-value-indirect `slot-name
`name-of-object
`(namespace sub-namespace sub-sub-namespace))

; Evaluates a method, The object is defined as "context" wihtin
; the method. Methods are typically stored in slots. Uses get-slot-value

(call `slot-name
object
`(param-1 param-2 param-n))

; Defines a method on a slot (if the slot does not exist, the slot is appended)

(def-method `slot-name
object
`(param-name-1 param-name-2 param-name-n)
"Documentation"
...code to execute...)

; Gets the parents from an object

(get-object-parents `slot-name
object)

Sjoerd hat gesagt…

By the way, for calling a method in a lisp way, you need to be able to put either the method at the beginning of the list, or you need to temporary switch contexts. With help of some changes to the interpreter, this might actually be quite nice:

(with 'obj-name
(obj-method p1 p2 pn)
(obj-method p1 p2 pn)
...)

Another way would be to intercept calls to non-defun'd methods in a way that automatically redirects calls to the upcoming argument, as thus:

(my-method obj p1 p2 pn)

which would redirect my-method to the correct object.

One could also do this by defining the method, and automatically defun-ing the function. If the function already exists, it should not be touched. An automatically generated defun can be used on any object defining the method with the same name in it's slots.

phoku hat gesagt…

Wow. Long comment indeed :-)

I have taken the liberty and saved it as
dlisp oop.

I sure would like some help :) my Lisp
knowledge is small and as you maybe guessed I basically copied parts of
python's object design (simplified though)

Basically there seem to be 3 topics to discuss.

1 Namespaces.

I would very much like to have namespaces available, but currently DLisp uses one large symbol table as global namespace - maybe one has to change that?

My take on namespaces would be by
switching namespaces with some
(in-namespace) function.

The neccessity to be able to 'kill'
a namespace is not clear to me?

2 Object model.
The current working (and used) object model is quite simple:

- A cell with type 'object' has an own symbol table.

- The functions 'set-attr' and 'get-attr' set/lookup in these symbol tables. A special symbol 'parent' is used by get-attr if an attribute can not be found.

- Methods are by convention functions in these symbol tables, which take the object as first argument (called self)

- By convention objects are instantiated from other objects (classes :)) by a method "make-instance". The D bindings hook here to actually instantiate D objects (which are attached to the cell, too).

- For convenience the syntax:
(method-name object args) is intercepted. This also overrides normal functions - which is good ((length vector) is possible) and bad (macros can't be handled this way).

3 Using DLisp as MacroEditor ...
Hm the OOP extension I am adding basically is extremely useful to
ease binding generation for a script language (in my game).
BUT I have no intention (and neither had the original author) to make DLisp compatible to common lisp. Maybe I should call it a 'Lispy Scripting Language'.

...so if I didn't scare you away I would very much like your help. :)

Sjoerd hat gesagt…

Obviously I would like to give a hand. Currently I don't have an internet connection at my new house (which is a bit problematic), however, you can contact me at using my e-mail address. I've posted this in a PM on dsource.