Language Design
Posted 9 Dec 2000 at 22:43 UTC by apm 
This article describes the Suneido language. As the language designer,
I am looking for comments, criticisms, comparisons, and
suggestions. I am also looking for feedback on this article. Does it
adequately describe the language? Is it understandable? Is there
anything that could/should be taken out? Is there anything that
should be added?
Here is a simple example of Suneido code:
/*
multi-line comment
*/
calc = function (x, y = 0, dbl = False)
{
sum = x + y // single line comment
if (dbl is True)
sum *= 2
return "result is " $ sum
}
calc(123) => "result is 123"
calc(123, 456) => "result is 579"
calc(10, dbl:, y: 5) => "result is 30"
calc("123") => "result is 123"
Notice:
- C/C++/Java style comments, either /* */ or //
- Functions are declared using the keyword ``function''
- Functions (and classes) are "values" - they can be assigned to
variables
etc.
- Function arguments can have default values
- Function calls can use named arguments, in which case order is not
significant. If a value isn't supplied for named argument, it
defaults to
True.
- Semicolons are not required (but are allowed)
- Variables do not need to be declared, and may contain any type -
i.e.
Suneido is a dynamically typed language
- is, isnt, not, and, or are normally used but ==, !=, !,
&&, ||
are allowed
- Suneido includes all of the C/C++ operators, including assignment
operators (e.g. *=), increment, decrement, and bit manipulation,
and adds
regular expression operators ~~ and !~
- Automatic conversions between numbers and strings.
- String concatenation is done with the "$" operator, not by
overloading
"+". This makes "123" + 456 and 123 $ "456" unambiguous.
- Suneido includes the same control statements as C/C++ if-else,
while,
do-while, switch, but unlike C/C++ conditions must evaluate to true
or
false or you'll get an error. Switches are also slightly
different - you
can't "fall through" cases, and cases allow multiple expressions
rather
than integer constants. There is also a "foreach" statement (see
below).
Functions can take variable numbers of arguments:
max = function (@args)
{
max = args[0] // will throw exception if no args
foreach (value x in args)
if (x > max)
max = x
return max
}
max(3, 6, 2, 5) => 6
max("joe", "fred", "sam", "mike") => "sam"
@args puts all the arguments into a Suneido "object" (see below).
foreach
iterates through all the values in the object, in this case all the
arguments.
You can also pass a pre-assembled set of arguments, using @.
ages = #(23, 67, 34, 19)
max(@ages) => 67
Memory is garbage collected - there is no explicit allocation or
freeing.
However, finalization is not supported yet, so resources (e.g. Windows
handles) must be explicitly released.
The basic data types in Suneido include: boolean (True and False),
number,
string, and object.
Suneido has a single numeric type - decimal floating point. Keeping
numbers in decimal rather than binary allows exact representation of
decimals,
e.g. for amounts of money. Numbers have 16 digits of precision with an
exponent range of plus or minus 127.
Strings are not null terminated so they can store arbitrary binary
data as
well as text. Strings are immutable i.e. there is no way to "modify" an
existing string. (Objects are the only basic data type that is
mutable.)
This means that substring (s.Substr(i,n)) does not need to do any
copying - it
just creates a new string that points to part of the old one. For
speed,
concatenation is "lazy"; it creates a linked list of string segments,
which
are automatically combined when a single string is required. This
greatly
reduces the amount of allocation and copying required to manipulate
strings.
Suneido could be called a "pure" object-oriented language in the
sense that
all values (including literals) are "objects" that can have methods.
For
example:
"hello world".Substr(3,2) => "lo"
97.Chr() => "a"
However, unlike some object-oriented languages such as Smalltalk or
Java,
Suneido has standalone functions as well as methods in classes.
User defined methods can be added to the built-in classes by creating
methods in specially named classes: Numbers, Strings, Dates, Objects,
etc.
Suneido has only two "scopes", global and local. Global names must
be
capitalized i.e. start with an upper case letter. Globals are either
builtin
(to the executable) or user defined in libraries in the database.
Global names
are not variables - they cannot be assigned to by code. Names that
start with
a lower case letter are local to a single function. Currently there
are no
packages or modules with separate namespaces.
Like Java, Suneido compiles to a stack oriented "byte code" which is
then
interpreted. For example:
function (x) { return x * 100 }.Disasm()
=> push auto x
push literal 100
*
return
Linking is done dynamically at run time. This means there are no
restrictions on compiling code with calls to undefined functions or
inheriting
from undefined base classes. Of course, if you try to actually access
something undefined, you'll get a run time error.
Objects
Suneido has a single "universal" container type - objects. Unlike
other
languages like Python or Ruby, you don't have to worry about what type
of
container to use. Objects can be used as vectors (single dimensional
arrays)
or as "maps" or both at the same time. Internally, objects have a
vector part
and a hash map part. Classes and instances are implemented as objects
containing methods and data members.
x = Object()
for (i = 0; i < 6; ++i)
x[i] = i // access as a vector
x.Add("six") // same as x[6] = "six"
x => #(0, 1, 2, 3, 4, 5, "six")
x = Object(name: "fred", age: 25)
x.married = True
x => #(name: "fred", age: 25, married: True)
x.name => "fred"
m = "age"
x[m] => 25 // same as x.age
Exceptions
Suneido has exception handling similar to C++ or Java, with try,
catch, and
throw. However, Suneido exceptions are strings rather than class
instances.
try
...
catch (exception)
...
The catch portion is optional if you simply want to ignore
exceptions:
try Database("destroy mytable")
An uncaught exception calls a Handler function - defined in the
standard
library as the debugger.
You can throw your own exceptions:
if (x < 0)
throw "square root: invalid negative argument: " $ x
Blocks
Smalltalk style "blocks" are a recent addition to Suneido.
Basically, a
block is a section of code within a function, that can be called like a
function, but that operates within the context of the function call that
created it (i.e. shares its local variables).
Blocks can be used to implement user defined "control constructs".
(In
Smalltalk, all control constructs are implemented with blocks.) For
example,
you could implement your own version of "foreach":
for_each = function (list, block)
{
for (i = 0; i < list.Size(); ++i)
block(list[i])
}
list = #(12, 34, 56)
for_each(list)
{ |x| Print(x) }
=> 12
34
56
Suneido treats a block immediately following a function call as an
additional argument.
Blocks can also be used to execute sections of code in specific
"contexts". For example, the Catch function traps exceptions and
returns
them. (This is useful in unit tests to verify that expected exceptions
occur.)
catcher = function (block)
{
try
return block()
catch (x)
return x
}
catcher( { xyz } ) => "unitialized variable: xyz"
But the interesting part is that a block can outlive the function
call that
created it, and when it does so, it keeps its context (set of local
variables). For example:
make_counter = function (next)
{ return { next++ } }
counter = make_counter(10)
Print(counter())
Print(counter())
Print(counter())
=> 10
11
12
In this example, make_counter returns a block. The block returns
next++.
You see this type of code in Lisp / Scheme.
Classes
Classes are read-only objects containing methods (functions) and
members
(data). For example:
counter = class
{
New(start = 0)
{ .count = start }
Next()
{ return .count++ }
}
ctr = new counter
Print(ctr.Next())
Print(ctr.Next())
Print(ctr.Next())
=> 0
1
2
Within a method, the special name "this" refers to the current
object. So
members and methods of the current object can be referred to
as "this.name"
but this is normally shortened to simply ".name".
Member / method names are public if they start with an upper case
letter
and private if they start with a lower case letter. Private names can
only be
accessed by methods of the class. In the above example, "Next" is a
public
method and "count" is a private member.
New is the constructor. (C++ and Java use the name of the class
instead of
"New", but this wouldn't work for Suneido because it allows "anonymous"
classes i.e. with no name.) Base class New's are automatically called
first.
You can supply arguments to base class constructors by making the first
statement of New a call to "super". (Similar to Java.)
class
{
New(x, y, z)
{
super(x, y)
...
"super" can also be used to call base class versions of methods:
mystack = class : Stack
{
Push(x)
{
...
super.Push(x)
...
}
Normally, "calling" a class is the same as creating an instance.
(This can
be changed by defining a method called "CallClass".) So the following
are
equivalent:
ctr = counter(5)
ctr = new counter(5)
Any method in Suneido can be a "class" or "static" method, providing
it
doesn't need to set any members. If a method is called on a class
rather than
an instance, "this" will be the class itself, and therefore read-
only.
If a nonexistent member is accessed, Suneido will call a method with
the
same name with no arguments, if present. This allows you to change a
data
member to a method without having to alter code that uses it.
(Bertrand Meyer
calls this "Uniform Access" - a client should be able to access a
property of
an object using a single notation, whether the property is implemented
by
memory or computation.)
If a nonexistent method is called, Suneido will call a method called
"Default" if it is present. This can be used to implement delegation,
or to
handle methods that are not pre-determined.
Since classes and instances are objects, and have methods to get
their base
class, it is easy for code to inspect methods and members. Java calls
this
"reflection".
Stack.Members()
=> #("Pop", "Top", "Push")
Object syntax can also be used to access methods or members
"indirectly":
m = "Add"
ob[m](value) // equivalent to ob.Add(value)
"Missing" Features
- no declarations
- no preprocessor i.e. #define or #include
- no enums
- no multiple inheritance (although this may be added)
- no operator overloading
- no goto
- no explicit pointers
- no explicit free'ing of memory
- no separate module or package namespaces
- no non-local returns from blocks
- no "protected" members / methods (just private and public)
- no "static" class data members (just class methods)
Summary
The Suneido language is:
- small, simple
- object-oriented
- dynamically typed - no declarations
- functionally similar to Smalltalk
- familiar to anyone who has programmed in C, C++, Java etc.
- safe - automatic memory management (garbage collection) and no
pointers
PS., posted 9 Dec 2000 at 22:47 UTC by apm »
(Journeyer)
let's see, posted 9 Dec 2000 at 23:12 UTC by cmm »
(Journeyer)
a kinda nice looking language that picks up the following misfeatures:
- gaudy syntax (C/C++/Java/Perl)
- no macro facility (Java). hint: macros don't suck when done right. C preprocessor is not done right.
- automatic string<->number conversion (Perl/shells)
- a too simplistic class-based object system (well, everybody has one)
- floating-point-only arithmetic, with bitwise operations
I understand you are having fun. without luck, your next effort will produce a better language. with luck, your next effort will lie in improving some existing wheel instead.
but there's nothing wrong with having fun, of course.
Indeed., posted 9 Dec 2000 at 23:33 UTC by egnor »
(Journeyer)
Yes, cmm was on the mark.
You're adding a point somewhere in the middle of the space populated by
Java, ECMAscript, Python, Smalltalk, and so on. Unless I missed
something, you're not introducing any interesting new features, just
mixing and
matching the attributes of other languages. Some people will like some
of your
choices; others won't.
That space is already pretty crowded, and IMHO the benefit of accepting
a feature set that is slightly closer to one's "ideal language" is
nowhere near the staggering cost of adopting an entirely new language
all over again.
It's quite easy (and rather entertaining) to build your own toy
language, starting with a simple interpreter and adding features all
day. (These toys are easily recognized by their dynamic type systems,
interpreted environment, and lack of deviation from the well-established
programming paradigms with which the author happens to be familiar.)
I've done it myself. It's very important, however, that you don't take
yourself too seriously -- nobody is actually going to use your language,
and, no, you're not the next Guido van Rossum or Larry Wall.
If you really want to learn about programming language design, you'd do
well to leave your "comfort zone". Learn some non-mainstream languages
like Haskell and Mercury, and use them to write nontrivial programs.
You'll probably hate it, but the experience will forever change the way
you look at programming, and make all the languages I cited at the
beginning of this reply (including yours) seem like the endless
permutations on the same old boring theme that they are.
You have contributed one thing, though; your misformatted post exposes a
bug in Advogato's HTML sandbox, since the "Reply" link is also preformatted.
Toy languages, posted 10 Dec 2000 at 01:16 UTC by raph »
(Master)
Dennis Ritchie, in his
LinuxWorld interview, said:
At least for the people who send me mail about a new
language that they're designing, the general advice is: do it to
learn about how to write a compiler. Don't
have any expectations that anyone will use it, unless you hook up with
some sort of
organization in a position to push it hard.
It's a lottery, and some can buy a lot of the tickets. There are plenty
of beautiful languages
(more beautiful than C) that didn't catch
on. But someone does win the lottery, and doing a language at least
teaches you something.
Couldn't have said it better myself.
One other observation, though: in most applications these days, the
language per se pales in importance compared with the surrounding
runtime and available libraries. In almost all cases where a language
has succeeded, it's because of the surround, not the language itself.
For example, a lot of people like Java the language, but dislike the
huge, constantly changing cloud of libraries and so on surrounding the
language. Yet, even though Java has failed miserably as a desktop
platform, the libraries do give nice access to networking,
threads, and so on. Thus, you're seeing Java catch on quite nicely in
server applications.
Similarly, much of Perl's success comes from a surprisingly strong
library, much of which is a compatibility layer over the Unix API, so
that Perl apps are considerably more portable than in, say C. If you're
doing the kinds of things for which the Perl library is strong, it's an
excellent choice.
Thus, while your language looks fun, I would have been considerably more
interested if you had posted a plausible plan for making sure its
runtime and libraries were of consistently high quality.
Comments, posted 10 Dec 2000 at 04:16 UTC by nymia »
(Master)
Actually, I have no comments about the syntax and semantics of Suneido.
What I'd like to mention here are some of my ideas about intermediate
languages. I think you know where I'm getting at, a language generated
by your language that is capable of being compiled by a native
compiler. What I'm looking at is not about language grammar and its
built-ins, but, the ability of the language to generate libraries that
are callable by other languages.
Just imagine the opportunity for a given language, say Suneido, where
it can call a member function or even any function written in C, C++,
Java, Python, Perl, VB, MyLanguage, YourLanguage, Etc. Morever, these
languages can also call your methods and functions. That can
only happen if we as language designers can come up with a way of
building an intermediate language specification where native compilers
can just parse the language and generate a native binary format. Is
there anyone here interested in pursuing this idea?
Anyway, my only comments about the grammar of Suneido are too simple.
Just add more production rules to the parser so you can add inheritance
as well. Also, it would be more ok if you strictly target the
abstract stack machine. That way, you'll prevent
newbies from messing around with data scoping. Plus, you'll be able
to add recursion in Suneido as easy as pushing and popping local vars
off the stack. Another is try to provide a collection facility where
any object of any class can be added into a collection.
I wish you good luck on your language.
Language mixing, posted 10 Dec 2000 at 04:29 UTC by hp »
(Master)
nymia: Microsoft's .NET runtime does exactly the
language mixing you're talking about. You can write objects in
any language, then use them from any language.
I think nymia is describing two things.
One is the notion of an intermediate language.
Language front ends (for as many languages as you care to support)
generate intermediate "bytecode" which is then translated to native code
(for as many processors as you care to support). The advantage of this
approach is that if you want to support N languages on M processors, you
don't have to write NxM compilers. If the IL can be expressed in a
file, it also makes it possible to distribute a single "binary" that can
be compiled for the target processor.
While an IL is clearly a good idea in theory, its implementation is
harder than you might think. UNCOL is a famous old failed effort in
this area. The main challenge is that different languages are more
different than you might expect, and have their own requirements on the
optimizer and code generation system.
In practice, people implementing high-level languages often use C as an
IL; Simon Peyton Jones once proposed "C--", a subset of C designed
specifically for use as an IL. These days, the Java bytecode is often
considered an IL (compilation happens at runtime via a JIT). The GNU
Compiler Collection has had some success with its internal IL, called RTL.
Two, you're suggesting a way for code written in
different languages to call each other. We have that today, in that
almost every language can call C and be called by C. The problem is
that you need a specific calling model; forcing two
object-oriented languages to use a common C layer is constraining.
There are a number of object-oriented interoperability layers; COM and
CORBA are among the best known.
Any such layer is necessarily a "greatest common factor" solution, which
means that the calling model in question can't capture the full richness
of any particular language's features. (COM does not support
exceptions, for example.) In many cases, the "impedance mismatch" may
be large indeed; consider the issues when calling between purely
functional or declarative languages and conventional imperative languages.
Microsoft .NET happens to provide both of the above, but the idea is
hardly new -- I'm surprised you haven't heard of the systems I'm citing,
or observed the limitations they must of necessity embrace.
Here is an
interesting paper on the Inferno virtual machine, Dis. Stealing design
from the Java VM is not a very good idea, considering it's amazingly
poor performance, in large part due to its design.
That paper explains why a stack-based design is not very good.
In "The Practice of Programming" book (by Brian Kernighan and Rob Pike),
they show a table comparing an implementation of an example program with
some langages (C, two different C++ implementations, Java, AWK, Perl) on
two platforms (PC running Windows NT and SGI R10000-based machine). The
C implementation was clearly the fastest, with something along a third
of a second running time, in 150 lines of code. The Java code was the
slowest, at something like 9 seconds and only a bit less code (between
100 and 150 lines). C++ was around 1.7 second I think, and I don't
remember AWK (over 2 seconds, but still clearly faster than Java!), but
the real surprise was Perl: 1.0 and 1.8 seconds (on the two platforms),
in 18 lines of code (the smallest by large amount), similar or faster
than C++!
Credit a well-done virtual machine and intermediate code.
The speed of Perl is mostly due to its design, trying to do as much as
possible in C code. They made sure you could apply a regex to a whole
array in a single instruction, so that the array walking could be done
in fast, optimized C code instead of done in Perl, for example.
Also, do not underestimate the free speed that inlining can give you.
The dynamic linking can prevent that. What you can do is keep the
dynamic linking, but add a way to statically link code together, but
don't do it in the dumb C linker style (which just ties code together),
make the linker analyze the code and optimize it further, letting it
find places where the types are statically known and shortcircuiting the
dynamic call for example (or even inlining the code, if small enough).
The linker has to be "close" enough to the source code to have enough
information at hand though.
There is a good language which was a good IL 20 years ago: Scheme. Any
language can be compiled to Scheme, and Scheme's semantics are rich
enough to allow inter-calling between related languages.
Features Scheme has which makes it a very good language to compile code
into:
- lexical scoping built in
- high-level macro facilities
- garbage collection
- continuations
The last one makes it much better then Java, say, as an IL: with
continuations you can simulate exceptions, co-routines, iterators,
threads, restartable exceptions, etc.
Scheme as an IL, posted 10 Dec 2000 at 13:20 UTC by cmm »
(Journeyer)
yup, Scheme is often touted as a perfect IL, but so what I've seen so far suggests that this is true only in theory (or when implementing academia-oriented stuff where down-to-metal performance issues are not very important).
the following issues complicate using Scheme as a "production" IL:
- for most languages, continuations are too powerful -- and continuation support has a price which many people don't want to pay. like not being able to use the stack straightforwardly, for example.
- Scheme has manifest (aka "strong dynamic") typing model. this is actually my preferred typing model for lots of philosofical reasons, but it is bound to get in your way when you don't want all those tag bits around.
that said, I know that some people has implemented Java by translating to (extended) Scheme, with apparently pretty good results.
and the fact that the C-- effort has so far seen at least one major redesign (no, the project is not at all dead) is further evidence that a universal and performant IL is far, far from trivial.
raph: I read the article and I was gonna say that.
apm: What is the goal of your language ? At one point
all languages have a specific goal... I don't use anything apart from C
and Perl nowadays. Calling Perl scripts from C programs, launching C
programs from Perl script. C is fast, Perl is fast to write, what is
your language good at ?
comments, posted 10 Dec 2000 at 14:49 UTC by apm »
(Journeyer)
egnor wrote:
You're adding a point somewhere in the middle of the space
populated by Java, ECMAscript, Python, Smalltalk, and so on. Unless I
missed something, you're not introducing any interesting new features,
just mixing and matching the attributes of other languages.
Given my goals, that's not so bad. I wanted something that would be
familiar, and therefore easy to learn.
The obvious alternative would have been to use an available language
such as Python or Ruby. When I started this project several years ago,
that didn't seem as obvious as it might today.
raph wrote:
One other observation, though: in most applications these
days, the language per se pales in importance compared with the
surrounding runtime and available libraries. In almost all cases where
a language has succeeded, it's because of the surround, not the
language itself.
I agree, and that's actually where Suneido is coming from, although the
article isn't about that. The language is a component of an integrated
application development system including a database. The goal wasn't
to break new ground with the language, but to have something more
substantial than the "scripting" languages that are commonly supplied
with databases.
nymia wrote:
What I'd like to mention here are some of my ideas about
intermediate languages. I think you know where I'm getting at, a
language generated by your language that is capable of being compiled
by a native compiler.
Yes, that's an interesting area. I have thought about this, but have
not done any work on it. Both C and Java VM would be interesting
targets. Eifel is a good example of using C. However, it is not such
a good route for an interactive development environment. I hadn't
considered Scheme as an IL. It doesn't seem as "universal" as C.
Anyway, my only comments about the grammar of Suneido are
too simple. Just add more production rules to the parser so you can add
inheritance as well. Also, it would be more ok if you strictly target
the abstract stack machine. That way, you'll prevent newbies from
messing around with data scoping. Plus, you'll be able to add recursion
in Suneido as easy as pushing and popping local vars off the stack.
Another is try to provide a collection facility where any object of any
class can be added into a collection.
Suneido does support inheritance, and recursion, and collections do
allow adding values of any type.
Thanks for the comments everyone. I will continue to not take myself
too seriously, and will cancel my plans to change my name to Guido
Wall :-)
UNWIND-PROTECT ?, posted 10 Dec 2000 at 16:43 UTC by dan »
(Master)
If you don't support finalizers on objects (which is fair enough, it
seems that nobody ever gets it right anyway), you should seriously look
at something like Common Lisp's UNWIND-PROTECT, or the `finally' arm of
java's try/catch. That is, a construct with a body that is executed,
and a `cleanup form' that is executed afterwards, no matter how the
control flow leaves the body - even if the body throws an exception,
the cleanup form should be executed before the exeption handler is run
Actually, this is still necessary even if you do have finalizers when
the foreign resource is something like a network socket and needs to be
released in a timely fashion (i.e. something else is waiting for it).
Finalizers may not happen until the next gc
Good point, several people have asked about this and I plan to
add "finally" to Suneido's exception handling.
Also, I put some notes on weak references vs finalization in a diary
entry. (See apm)
It was the addition to Java of the "finally" clause that
proved beyond doubt that its designers really didn't
understand what exception handling is about.
The whole point of exceptions was to be able to ignore them
everywhere else, and concentrate on them in one place. In
Java, rather than collecting exception handling into one or
a few clearinghouse modules, you end up scattering it
throughout the system, cluttering up the logic about as
much as with error-code return values.
Adding random features from other languages won't improve
yours, particularly if you don't understand them or their
architectural consequences.
huh?, posted 11 Dec 2000 at 00:31 UTC by cmm »
(Journeyer)
ncm: what exactly is wrong with "finally", except that
you don't like it?
it does the same thing that the destructors of automatic variables do in
C++, basically, n'est pas?
FWIW, nobody has ever claimed that "unwind-protect" is a bad idea in
Lisp.
(I'm assuming that Java's "finally" is analogous to Lisp's
"unwind-protect". I may be wrong, 'cause I don't do Java and sometimes
guesses about such things just don't work. if that is the case, sorry).
Uniform Access, posted 11 Dec 2000 at 05:19 UTC by Dacta »
(Journeyer)
If a nonexistent member is accessed, Suneido will
call a method with the same name with no arguments, if present. This
allows you to change a data member to a method without having to alter
code that uses it. (Bertrand Meyer calls this "Uniform Access" - a
client should be able to access a property of an object using a single
notation, whether the property is implemented by memory or
computation.)
This is the biggest thing I liked about the Delphi Object Pascal
language. It has a concept called "properties" where you can declare an
object using syntax something like:
class TNewObject = class( TObject )
private
fNumPeople : Integer;
public
property NumPeople read fNumPeople write fNumPeople;
end;
Then, if you need to change how fNumPeople is calculated you can write:
class TNewObject = class( TObject )
private
function getNumPeople: Integer;
procedure setNumPeople( Value : Integer );
public
property NumPeople read getNumPeople write setNumPeople;
end;
and no class outside TNewObject needs to be changed.
You can also do things like having a read only property by not using
a "write" keyword.
re. Uniform Access, posted 11 Dec 2000 at 15:14 UTC by apm »
(Journeyer)
Doesn't C# have a similar feature? Not surprising since I believe it's
the same designer as Delphi.
I'm not totally convinced that the benefits of "properties" justify
adding another concept to the object model.
Something I have considered is providing a way to intercept / override
setting members with a method. The end result would be functionally
similar.
re. "finally", posted 11 Dec 2000 at 15:33 UTC by apm »
(Journeyer)
I can appreciate that finally is unnecessary in C++ (and probably
undesirable) since destructors can handle things like releasing
resources better and cleaner.
However, in non-reference-counting garbage-collected languages (e.g.
Java, Suneido) the equivalent to destructors - finalization, doesn't
work all that well - there are a lot of problems with it. So "finally"
or "unwind-protect" are perhaps more reasonable. Python and Ruby both
have the equivalent of "finally".
An alternative in some circumstances is where you pass a block to a
function that then aquires the resource(s), runs the block, and then
releases the resource(s). (Kent Beck in Smalltalk Best Practices calls
this pattern "Execute Around Method".) Presumably the block would be
executed within an exception handler (e.g. try-catch, ensure) so the
resources are released even if an exception occurs. Ruby uses this
technique. Of course, you need something like blocks for this.
In CLOS, you can do something similar, by using something like:
(defclass my-class () ; We inherit only the "standard class"
((my-slot :accessor my-slot)) ; And we have a "set" and a "get" method
; named "my-slot".
)
From outside, you get a value by using (my-slot instance)
and set it by using (setf (my-slot instance)
new-value).
Now, assume we want to do something (say, sanity checkin, logging,
wossname), we can change the class definition and define new access
methods:
(defclass my-class ()
((my-slot :accessor secret-my-slot))
)
(defmethod my-slot ((obj my-class))
(let ((oldval (secret-my-slot obj)))
(format t "Accessing my-slot in object ~a, value is ~a~%" obj oldval)
oldval))
(defmethod (setf my-slot) (new-val (obj my-class))
(let ((oldval (secret-my-slot obj)))
(format t "Setting my-slot (from ~a to ~a)~%" oldval new-val)
(setf (secret-my-slot obj) new-val)))
It's (perhaps) slightly more verbose than the same in Delphi, but it is
trivially doable.
Java programmers who use finalize liberally better rethink things a bit!
finalize has none of the determinism that one expects in C++ - there is no real guarantee that things you depend on in the finally code are still true/available, and in implementations where they are, you might end up interfering with other GCable objects.
finalize, unlike C++ destructors, should be used as sparingly
as gotos in C or C++, perhaps even less. Look at it this way - if your object is controlling a resource so important that you need to release it in finalize, it is probably better to release it manually in your code during a controlled cleanup() operation.
IMHO the only people who can look at finalize ever being the right thing to do are people implementing stuff with JNI or embedded systems folks using a VM with a fully deterministic GC.
Ever had a look at elastiC, at www.elasticworld.org? After a
quick glance, the two look sort of similar.
I don't think anyone disagrees with the need to be careful with the
use of finalize.
You're not confusing the discussion on try..finally blocks with the
only partially related issue of trying to use finalize like a
destructor, are you?
I don't quite understand the issue with try..finally blocks. Is
there a better way to guarrentee the release of some scarse resource
(like database connections, say) than a construct like:
getDatabaseConnectionFromPool()
try
do database stuff
finally
releaseDatabaseConnection
end // try..finally block
Of course, you would have that wrapped in some kind of exception
handling (probably so the exceptions all are handled in the same
location), but the handling of the exceptions isn't really related to
the fact that you always need to release that database connection (no
matter if an exception occurs or not).
Most popular modern languages seem to support somethign like this -
even the next version of VB is supposed to have it.
Since the discussion seems to have turned into a debate on the merits
og Java's "finally" construct, let me add that I actually don't like
the way that it's been made part of try/catch. try/catch is about
handling exceptional conditions; finally is about resource release.
The two things are different. You can quite legitimately want to
release a resource while leaving the exception to be handled later (an
example in Java might be that you want to call close() on a JDBC
ResultSet and then drop into the general SQLException handler several
levels up in the stack). This means catching SQLException and then
throwing it again, which is a bit silly.
Compare CL, in which HANDLER-CASE and UNWIND-PROTECT are two separate
forms for separate tasks.