Sign in to follow this  

C&C on a language design

This topic is 4131 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

The following is a bit detailed, please skip down to the considerations if you're not up for reading all of it. I am very interested in programming languages and programming paradigms. From time to time I've been designing and implementing small scripting languages mostly for the fun of it. So far, none of the scripting languages I've actually got around to implement have been languages that I'd actually want to use later; however, I'm currently designing a general purpose dynamic scripting language that I hope will be of use to me and possibly others as well. The language supports multiple programming paradigms, namely object oriented, procedural, functional and possibly aspect oriented programming to some degree. These days I think it goes without saying that it's garbage collected. The most important construct in the language is the closure, which is defined like this (I think I stole the syntax from somewhere):
{|argument1, argument2| ... }
Where ... is the code of the closure. The value of the last statement in the closure is the return value. The language has no keywords; there are a number of closures that are available from the beginning however, including the if-closure:
# This is a comment
# syntax: if(condition, thenClosure, elseClosure)
if(x < 23, {
    y = -1
}, {
    y = 1
})
# or functional style:
y = if(x < 23, {-1}, {1})
Implementing 'else if' seems impossible using this scheme, hence consideration 1 below. The language does not support any other constructs containing code than the closure; in particular, it doesn't support regular functions. Instead of using a function, you'd do this:
fib := {|n| if(n < 2, {1}, {fib(n-2) + fib(n-1)})}
So fib now contains a closure that takes n as an argument, and can be called like this:
fib(7) # result = 13 
This has one important side effect, which leads to consideration 2. Since the language is dynamically - but strongly - typed, there are a number of runtime errors that may occur. I'd like to be able to handle these in the language, and exceptions is a natural way to handle this. This will require another couple of built in closures, namely:
throw(MyException()) # throws an exception
try(closure, handlerClosure) # catches exceptions, ie.
try({
    ...
}, {|exception|
    if(exception.isA(SomeException), {
        ...
    }, {
        throw(exception)
    })
})
Checking if an object is of some class doesn't seem very elegant, which leads me to consideration 3. Objects and classes are supported through another built-in closure:
MyClass = class(SuperClass, constructorClosure)
This closure constructs an object of the SuperClass type and passes it to the consructorClosure for modification (addition and overriding of methods and member variables), ie.
Vector = class(Object, {|self, x:0, y:0|
    self.add = {|vector| Vector(self.x + vector.x, self.y + vector.y)}
    self.addToSelf = {|vector|
        self.x += vector.x
        self.y += vector.y
    }
    # etc.
})
Vector is now a class - that is, a closure that when applied will return an object of type Vector. [code] v = Vector(3, 5) v.addToSelf(Vector(0, 1)) It should be noted that a class can only inherit from one other class, and that there is no notion of interfaces. A problematic side effect of this approach is that there is no way to call the constructor of the parent class with custom arguments from the new constructor, which is consideration 4. The last thing that I'm going to talk about are the scoping rules. As you probably know, a closure has access to any enclosing scopes; in this language in particular, it has write access. The problem is, if you're assigning to a variable in a closure, how does the computer figure if you ment to assign to variable local to this scope or a variable from one of the enclosing scopes? This is consideration 5. Considerations These are things I'm having trouble deciding on or coming up with. I'd appriciate comments and critisism on the purposed solutions (if any) as well as the language in general. 1) Implementation of 'else if'. I'm not sure how to go about this at all. It's not strictly necessary of course. 2) Implementation of a return statement. One solution is to ignore the return statement alltogether and force the user to model the control flow so that it'll allways reach the last line. I'm not sure I want to to that, but since you'll often be using a closure inside a closure inside that closure that is a function in your mind, the program needs a clue on where to return from. This could be done by throwing a ReturnException with the return value that any closure wrapped by the function closure automatically catches, ie.:
function := {|closure|
    {|*args|                 # the * syntax is for a variable number of args
        try({
            closure(*args)   # the * syntax here is to expand the args list into arguments
        }, {|exception|
            if(exception.isA(ReturnException), {
                exception.returnValue
            }, {
                throw(exception)
            })
        })
    })
}
return := {|value| throw(ReturnException(value))}
# The resulting syntax for creating a function would be:
f = function({|a, b|
    return(100 * a - b)
})
3) Try/catch-syntax. A 'case of' pattern matching construct like they have in SML would be nice, but then again, quite hard to implement. Any ideas? 4) Calling the non-default constructor of the parent class. It's not strictly necessary to be able to do this, but if it can be done without killing the syntax, it would be a wonderful ability. On a side node, I think I'm going to make objects's member variables write protected outside of it's methods, but making everything fully readable (and callable of course). Does this seem like a good idea to you? 5) Local variables vs. variables from enclosing scopes. My solution to distinguishing these two is to require that you use a special assignment operator the first time you assign to a variable (and thereby create it). The scope that this operator is used in is the scope of the variable, ie.
x := 99
{
    x := 10
    print("Inner closure x = " + x.toString())
    # prints "Inner closure x = 10"

    # x has already been 'declared', so now you 
    # have to use the normal assignment operator.
    x = 20   
    print("Inner closure x = " + x.toString())
    # prints "Inner closure x = 20"

}() # <- call this closure immediatly

print("Outer closure x = " + x.toString())
# prints "Outer closure x = 99"
Does this seem like a good or a bad approach? Is there any better alternatives you can think of? I'd also be happy to discuss any of the missing details of the language (like loops) if anybody is interested. Thank you for your comments and critisism! Edit: I was possibly a tad sleepy when I typed this. The 'consideration' references were all messed up! Fixed. I hope. [Edited by - Joakim_ar on August 21, 2006 10:08:57 AM]

Share this post


Link to post
Share on other sites
Guest Anonymous Poster
I don't think you're using 'closure' right. A closure is a specific instance of a block with bound variables and implies lexical scoping. I don't see you taking advantage of that anywhere... The syntax is a little weird to me, too. There are a lot of places where you need to "double nest" things (put them in parentheses and braces).

Share this post


Link to post
Share on other sites
Thank you for the ciritisism :)
Yes the double nesting is a bid weird, there's no denying it. It does simplify the actual language a great deal though (very few language constructs).

On the closure side, I am actually taking advantage of it in the class example:
Vector = class(Object, {|self, x:0, y:0|
self.add = {|vector| Vector(self.x + vector.x, self.y + vector.y)}
self.addToSelf = {|vector|
self.x += vector.x
self.y += vector.y
}
# etc.
})
In the addToSelf method, self is from the outer scope, and as it is a closure, it will keep 'knowing' self as long as the closure exists. I believe this defines a true closure.

Share this post


Link to post
Share on other sites
Guest Anonymous Poster
Okay, I see. I like how the closure mechanism is also an anonymous function.

Share this post


Link to post
Share on other sites
A possible solution to the 'else if' problem:

Instead of making the 'else if' mechanism related to the if closure, another closure that supports multiple alternatives can be introduced, with this syntax:
alternatives({condition1}, {action1}, {condition2}, {action2}, ...)

The first condition will be evaluated, and if it's true, action1 will be executed. Otherwise, it checks condition2 and executes action2 if it's true. This is repeated until there are no more condition/action pairs. The resulting syntax would be:
alternatives({x < 100}, {
print("x is less than one hundred")
}, {x == 100}, {
print("x is equal to one hundred")
}, {x > 100}, {
print("x is greater than one hundred")
})

An unconditional else branch is obtained by replacing the condition with {true}. What do you think of this approach? And what should happen if there are no valid alternatives?

And a possible solution to the try/catch problem:
try({action}, ExceptionClass1, {action1}, ExceptionClass2, {action2}, ...)

It'll see if there's an exception thrown of the class ExceptionClass1, and if there is, execute action1. If not then proceed to ExceptionClass2/action2 and repeat, etc.
try({
# do some file input (kinda inspired by ruby,
# not necessarily the final approach)
readFile("test.txt", {|f|
while(!f.eof(), {
print(f.readLine())
})
})
}, FileNotFoundException, {|e|
print("There was an exception: " + e.toString())
})

I'm still very eager to read any comments, suggestions or critique!

Share this post


Link to post
Share on other sites
Two other useful closures:
# A case/switch like mechanism
# if x = 0 then ... else if x = 1 then ... else ...
case(x,
0, {},
1, {},
2, {},
{}
)

# A closure that allows the user to execute
# specific code based on class
dispatch(x,
Vector, {},
String, {},
Number, {},
{}
)

Share this post


Link to post
Share on other sites
Nope, I invented the term "closure" all by myself.

...

It lends from a great deal of languages; all languages does this - when Sun wrote the Java specification, did you ask them if they had taken a look at C++?*

In fact, I think it's quite a discouraging comment, but if you meant to provide constructive critisism, feel free to clearify.

* Not implying that this language will have a fraction of Javas significance.

Share this post


Link to post
Share on other sites
It'd work, but it isn't quite a solution to the problem I had in mind, since it doesn't represent the functionality of else if, which is there to avoid having to nest all the alternatives inside each other, ie. (BASIC style)
if x = a then
print "a"
else
if x = b then
print "b"
else
if x = c then
print "c"
end if
end if
end if
vs.
if x = a then
print "a"
else if x = b then
print "b"
else if x = c then
print "c"
end if

But I appriciate the feedback :)

Share this post


Link to post
Share on other sites
You could do something similar to Smalltalk's method:

booleanObject ifTrue: [ ... ] ifFalse: [ ... ].
" ... "
booleanObject ifTrue: [ ... ].


The first line would call the ifTrue:ifFalse: method of the boolean object, and the second would call the ifTrue: method of the boolean object. Blocks in Smalltalk are in square brackets.

Share this post


Link to post
Share on other sites
Thank you for your input!

I have a shallow knowlegde of smalltalk, and the ifTrue, ifFalse methods of Boolean sure is clever. In fact, I earlier posted a reply to this post asking if a smalltalkISH version would look better, ie.
(x = 23).if({...do this if true}, {...do this if false})

I decided it wasn't prettier, didn't reslove the elsif problem and deleted it. The alternatives-closure is a solution I can easily live with.

Share this post


Link to post
Share on other sites
Looks like some of the more advanced stuff from ECMAScript.

Hmmm... I'm actually kind of morbidly interested in language design and implementation. Do you have a BNF/EBNF spec for your grammar yet?

PS: The closure syntax you "borrowed" is from Ruby.

Share this post


Link to post
Share on other sites
Not yet; I'm going to use the ANTLR tool for the lexing/parsing/traversing the syntax tree, so there's going to be one. Right now I'm focusing on the vm side of things though.

I've decided to leave objects out of the first prototype, and I think the remaining syntax is quite standard from a parsing perspective (maybe with the exception of not having a line delimiter, but lua does that too).

I'll post the ANTLR without too much semantic code when I've done it if you're interested :)

Oh yeah, that's right. I haven't been doing a lot of ruby myself, so thats why it didn't cross my mind to mention it. I have been reading about it though. I like that particular syntax :)

[Edited by - Joakim_ar on August 25, 2006 4:13:30 AM]

Share this post


Link to post
Share on other sites
Here's the LL(2) grammar presented in ANTLR syntax. I have not tested this yet and there are no guarantees that it's 100% correct. I have removed the syntax for building parser trees and replaced most tokens with string literals for convenience.

start
: (statement)* EOF
;

statement
: bindStatement
| rebindStatement
| expression
;

bindStatement
: ID ":=" expression
;

rebindStatement
: ID "=" expression
;

apply
: atom "(" (argument ("," argument)*)? ")"
;

argument
: ID ":" expression
| "*" expression
| expression
;

parameter
: ID ":" expression
| "*" ID
| ID
;

closure
: "{" (statement)* "}"
| "{" "|" parameter ("," parameter)* "|" (statement)* "}"
;

expression
: notExpression
;

notExpression
: ("!")? andExpression
;

andExpression
: relExpression (("&&"|"||") relExpression)*
;

relExpression
: addExpression (("=="|"!="|">"|">="|"<"|"<=") addExpression)*
;

addExpression
: multExpression (("+"|"-") multExpression)*
;

multExpression
: powExpression (("*"|"&#47;") powExpression)*
;

powExpression
: negExpression (("^") negExpression)*
;

negExpression
: "-" atom
| atom
;

atom
: NUMBER
| ID
| "(" expression ")"
| closure
| apply
;




The dot-syntax for object isn't there yet as explained in my previous post.

[Edited by - Joakim_ar on August 27, 2006 5:39:43 PM]

Share this post


Link to post
Share on other sites

This topic is 4131 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

Sign in to follow this