It may be usefull to have methods on attributes. This is a different issue to having support for Aspect Oriented Programming.
class 'a ResourcePool def get_resouce: 'a [private, synchronize] while _free_list.is_empty { self.wait } i = _free_list.pop return _resources[i] end end
The difference here is that the method attributes follow after the method definition. Another possibility is to place them on a subsequent line.
class 'a ResourcePool def get_resource: 'a [private, synchronize] while _free_list.is_empty { self.wait } i = _free_list.pop return _resources[i] end end
This second option looks less cluttered than the previous version. However it has the difficulty that it looks just like the expression for an array. Perhaps a better syntax is the following:
class 'a ResourcePool def get_resource: 'a attributes :private, :synchronize while _free_list.is_empty { self.wait } i = _free_list.pop return _resources[i] end end
The attributes becomes a reserved word. Using syntax highlighting the reserved word can be emphasised. Having more reserved words could make programming more difficult as these words cannot be used for variables or methods.
Even better is rather than having labels as in ruby to use the :private only for attributes applied to methods, classes and such.
class 'a ResourcePool def get_resource: 'a :private, :synchronized while _free_list.is_empty { self.wait } i = _free_list.pop return _resources[i] end end
It should be possible to define functions. Sometimes one wants to define functions that are not assigned to any class.
def fold(lst: 'a Enumerable, fun: 'b * 'a -> 'b, z: 'b) for x in lst { z = fun(z,x) } return z end
This function could have been added onto an extension of Enumerable as in
module 'a Enumerator def fold(z: 'b, fun: 'b * 'a -> 'b) each { x | z = fun(z, x) } end end [1, 2, 3].fold(0) { z, x | z + x }
We note here that the blocks need to return the last expression in order to allow for both returns and non-local returns.
We also note the use of extended typing information. Can the method call to plus be checked though? It can be checked once the type 'a has been nailed down as it has in the above expression.
Lets look at another example of typing.
a = [] // type of a is Object Array a.push(1) // type remains Object Array since push(o: Object) matches a.fold(0) { z, x | z + x }
Conside the final expression. Passing 0 to fold will bind Int to 'b, and so to z. Since z is an Int, Int will be bound to x, and so to 'a. Now since 'a is also in the type of a, a will become Int Array, and a type check will be extended to push.
Having both blocks and hash tables introduced using {} is a complication for the compiler. There are however not many more options for delimiters. Having single character delimiters means that delimiters can be nested as required.
Nesting with regard to hash tables is suspicious. Something like:
a = [{'a' => ['1', '2']}, {'b' => { 'c' => 'd' }}]
Such structures are a bit weird to say the least. It is not clear how the ruby parser distinguishes between hashes and blocks.
Another problem is how does the syntax for method calling work, since method calls don't require brackets it can get pretty nasty I think.
puts (a + b), c
How about the following:
. <methodcall> ::= <ident> <expr> { <comma> <expr> }* . <methodcall> ::= <ident> '(' <expr> { <comma> <expr> }* ')'
There is a complication since methods should be able to return multiple values, and we would also like to support hash tables and named parameters.
grab :x => 2.3, :y => 8.9
So an argument expression can be one of these mapping structures. The mapping structures also pop up in hash tables.
. <mapping> ::= <expr> '=>' <expr> . <mapping_list> ::= . | <mapping> { <comma> <mapping> }* . <hash> ::= '{' <mapping> '}' . <block> ::= '{' '|'? <args> '|' <stmt_list> '}'
In the block production the first '|' is optional. So for example one can have:
(1 .. n).each { x | puts x }
In the above expression the brackets are cosmetic to make the expression easier to read. The '.' operator binds last, but not after itself.
1 .. n . sort { x | x < 10 }, :ascending => true
This reduces to
[call [call, 1, .., n], sort, [block ...], [named_arg, :ascending, true]]
It is clear that the parse tree will need to be a bit xml like. I.e. it should be a tree in which the nodes are typed, and the branching factor is variable, and there are constraints limiting the types of tree that can be produced.
A process of walking the expression tree for the program can then hopefully derive flesh the expressions with type information.
. <stmt_end> ::= '\n' | ';' . <class_decl> ::= 'class' <type_params> <name> [ <parents> ] <stmt_end> { <classdef> }* 'end' . <typeparams> ::= <type_param> { , <type_param> } * . <class_def> ::= <method_def> | <field_def> | <attribute>
Class, like methods can have attributes. So for example we could have:
class 'a, 'b Map # Implements a mapping from a domain objects of type 'a to a range of # objects of type 'b. The map can be coerced into a set in which case # it is interpreted as a relation expressing a functional dependence. attribute :public end
There is the question of whether the language should admit such things as invarients, preconditions, postconditions, or even comment blocks.
Hierarchical name spaces can be usefull but they can also be tiresome, especially when one want to pick up classes from various packages. Let's look at some examples.
package net.sf.griff.sarl3.test use std.collection, std.net use net.sf.griff
Each module should start with a statement of the package name. Packages are expected to align with a directory structure. Modules are essentially names spaces enfolding classes and functions.
The rule for resolving class and function names is: find the first package which contains a matching class or function and use that.
Should there be support for runtime configuration via factories. Perhaps such functionality should be brought in by the standard library.
package std.config _class_mapping_config_string = "std.config.class_mapping" def initialize begin for class_name, class_alias in Config.resolve(class_mapping_config_string) do begin class_object = load_class class_name alias class_alias, class_object rescue e: NoMatch log_error \ "Missing configuration string for #{_class_mapping_config_string}" rescue e log_error \ "Problem loading class mapping #{e}" end end rescue e: NoMatch log_error \ "Missing configuration string for #{_class_mapping_config_string}" resuce e: MethodMissing log_error \ "Invalid configuration for #{_class_mapping_config_string}" end end
In the above, a list of pairs is extracted from the configuration space. The configuration space should probably be an xml file and the thing returned should probably be a dom. Better though is to use YAML which admits lists of strings for example.
This will allow the std.config class upon initialisation to register class aliases according to a mapping defined in the configuration.
The rescue and logging code is larger than one might expect. Another possibility is to use a function that logs and ignores expections.
package std.config use std.exception.logging _class_mapping_config_string = "std.config.class_mapping" def initialize log_exception { for class_name, class_alias in Config.resolve(class_mapping_config_string) do log_exception { class_object = load_class class_name alias class_alias, class_object } end } end
It is potentially possible to attach to the code some logic for decoding the exceptions that may be raised by the block of code to make messages that are meaningfull to the user. There are different perspectives: library programmer, application programmer, application user. Another idea: In a logging framework one may want to turn some logging off, especially at the code level.
The above code example raises an interesting question: if blocks are so readily usefull, then how would they in practice be optimised. First lets look at the definition of log_exception.
def log_exception(block: VoidBlock) begin block.call rescue e: Exception log "Exception Raised: e.to_s" end end
This definition however obscures the stack trace perhaps, the point at which the exception was raised and caught. It would be nice to have access to the calling context. It is this that all those macros in the programming language Nice are targetted at (amongst other things).
The type VoidBlock is assumed to represent the type "void, void Block", that is a block that takes no parameters and returns nothing. Also we assume that Exception is the root of all exceptions. Basically you can't throw something which doesn't derived from Exception.
def log_exception(block: VoidBlock, context: implicit CallingContext) begin block.call rescue e: Exception log context.to_s + e.to_s end end
Here we assume that an argument context representing the context in which a method is called can be made available. This is an aid to debugging. Another mechanism to yeild the calling context would be to make it available through some standard function.
def log_exception(block: VoidBlock) begin block.call rescue e: Exception log e.to_s log System.callstack[1].to_s + " in this context." end end
So the function is passed a block which it duely executes. Since the method is likely to be exactly resolved and it is relatively short, it can most likely be inlined. Hopefully this would lead to the inlining of the call to block.
It is interesting to ask how this would be handled by LLVM, but that is a question for later.
The previous example of using blocks and the possibility for optimisation given that the functions being called can be resolved.
def sum range : 'a Iterable, func: 'a -> 'b Sumable, x: 'b = 0.0 returns 'b range.each { y | x += func(y) } return x end interface 'a Sumable def += (x: 'a) returns Void end class Integer < Float Sumable, Integer Sumable ... end class Float < Float Sumable, Integer Sumable def += (x: Float) # builtin end def += (x: Integer) self += x.to_f end def +(x: Integer) return self + x.to_f end end
Typing the function becomes rather generic. One can ask to what extent can type inference be used. Particularly if it is possible to add for example an integer to a float.
Calling a method becomes more complex because dispatch is based on both the incident class and the argument types. If one also admits implicit conversion the calls becomes hopelessly ambigious. On the other hand, argument based dispatch becomes a problem when linked with hierarchy perhaps. Although it can be resolved by defining a search order for the dispatch. Diamonds have always been a problem, but if they are strictly ordered by selection of the first match, then the dispatch is well defined, even if non-obvious in some cases, for example when a class extends both Integer and Float, which arguably is asking for trouble.
Anyway it now becomes possible to call the sum function (I think).
result = sum 1 .. n, { x | x*x }, 1.0
The sourcecode should be a default unicode encoding. No other encodings should be allowed for the source code. The string handling libraries however should support unicode.