Skip to main content
GameDev.net gamedev.net
🔒 Locked

Discussion for 'defn', the language I'm working on.

Started by Zahlman Nov 1, 2006 at 1:33 PM 7 replies 2.6k views
Original Post
Zahlman
Zahlman
Continuing from this rather unrelated thread, wherein I began talking about one of my personal projects - creating a (serious) language. I would just contribute my thoughts to Epoch, but I have very much my own vision [wink] (Actually, I have started work on it under the thin guise of doing a really nice version of the C++ Workshop Project 2, although my work on the actual project there has pretty much succumbed to 'analysis paralysis' :( Regardless, I have all of these interesting widgets - various smart pointers, primitive code generation stuff etc. - in a folder called 'n00bproj2'. This amuses me. [smile]) Notes: - Examples of the syntax I have in mind can be found in the other thread. - The name comes from a type unique to the language: the 'defn' type, which has four values - (d)efault, (e)rror, (f)alse and (n)ull. This was intended to unify all of the "common, built-in, finitely enumerable" types that you find in other langauges - or rather, all the literal values that don't belong to infinite (or at least large) sets. At the time I got the idea of doing a language at all (many years ago), this seemed like a really neat idea. I'm intending to keep it, but de-emphasize it quite a bit. The original idea was that 'defn' is a "bottom type"; it has every capability, implemented as follows: - default and false construct temporary instances of the default implementation of the requested capability, and delegate. - error throws an exception for every request. - null does nothing and simply returns a constructed-from-null instance of the return type for every request. Lots more to come (I hope).
Zahlman
Zahlman
Imported from the other thread.

But first, the golden rule of the defn design, because I forgot it in the first post.

In C++, the guiding philosophy is "you don't pay for what you don't ask for". Which results in a *lot* of asking. And forgetting. And discovering, the hard way, that the protocol for asking is more complex than you thought.

In many higher level languages, it seems to be "here's a bunch of stuff that will make it Just Work(TM); good luck getting rid of it yourself. With luck and a few more years of computer science, the runtime will do it for you automatically!!!11". Except that I really don't see how you're supposed to optimize certain things away at runtime, in particular, unnecessary, automatically-generated levels of pointer-indirection. And anyway, I think static typing has its place; it just doesn't have to be as annoying as *manifest* typing gets.

So basically, I think both of these are the wrong tradeoff.

In defn, the goal is "Here's most of the stuff you're likely to want - and here's how to get rid of it, on a per-thing basis, as directly as possible."

As a corollary, I also reverse one item from the Zen of Python, and claim: implicit is better than explicit, except when there really is more than one obvious interpretation, or the obvious interpretation is wrong.

Quote:
Original post by NotAYakk
Quote:
Original post by Zahlman
(I do want to avoid Python-style "explicit self" within member function implementations, though.)


And why not have a Python-style "explicit self"?

It reduces confusing for new folk. It adds a bit of typing, admittedly.


TBH, when I first learned C++, I had no difficulty with the idea at all, and being able to be implicit about 'self' seems like one of the key objective benefits of making member functions (instead of free functions with a foo* or foo& first parameter) in the first place. It's actually caused me a lot of slowdown in writing Python code! ("Oops, I forgot to pass 'self' when calling a helper function. Oops, I forgot to *accept* 'self' in the helper function.")

I would like stricter scoping than Python offers, too. Once that's in, might as well take advantage of the lack of ambiguity.

Quote:
Admittedly, I'd rather have the ability to mutate a function definition, and add implicit parameters...

So you have a function:
void foo(int x) {  this->y = x;}


foo has a "dangling variable" called "this".

In most languages, it would look for it in the global scope, and if it couldn't find it, it would fail to compile. What if didn't look for it in the global scope, and simply noted the dangling variable named this?

Then you could have:

mutator member(object this_, incomplete_function f) {  return bind( f, this, 'this' );}


That would be stealing a page out of mathematical logic. A function would have to be compile-time completed before it could be called.


That's an interesting way of doing it. :) Of course, allowing for implicit-self doesn't really prevent that; it would just become an instance where you do need to be explicit (and C++ and Java have some of those too, anyway).

I'd want to do binding 'anonymously', though. Ideally that would be an option and you could also do it through a function, but I'm not too sure how to do functions-as-objects...

Actually, I started writing out possible syntax, and I got something with a lot of problems, and which suggested something much more bleedingly obvious when I looked at it a little funny ;)

Since we're going to implicitly translate

class Foo:  to Bar(Bazzable b): # return type    # implementation    # If you have to refer to the entire current-object in here for some reason,    # then there is some way to do that. The usual way would be a keyword like    # 'self' or 'this'. Maybe using the current class name could work too?


into:

class Foo:to Bar(Foo _internal_name, Bazzable b): # return type  # within the implementation, names not resolvable as locals  # are tried to be resolved as members of _internal_name, and failing that,  # as globals.


All we need to do is allow the reverse transformation as well.

Questions:
- Should we require a special name for the first parameter for the reverse-transformation to work (i.e. to be able to call a free function with member function syntax)?
- Should we give classes some means of disallowing their use in this way?

Anyway I think you already suggested something like this, and I think we get diminishing returns by trying to improve upon that with complex binding logic.

Although I do want *some* form of lambda. Preferably one where you don't have to jump through hoops to get side effects (like with Python).

Quote:
I'm somewhat against a proliferation of keywords. When possible, I think primitives should be used, and 'keywords' build up out of them.

If your languages has a compile-time grammer that generates code and exposes/hides things, why not let the programmer write it, and include a standard grammer?

Of course, that shouldn't nessicarially be in a first implementation.


I was thinking of allowing user-defined keyword aliases and/or redefinitions via pragmas (obviously much neater than #define hackery ;) )

I do think you have a point, and I do want to keep the reserved-word count down (I think the low count vs. C++ is one of the more understated selling points of Java). I'm not really sure how to accomplish that sort of thing by "building up via primitives", though. Or have you already been giving me examples, with me being too dense for the enlightenment? :)

Quote:
{int x, int y, double d} = foo();

The function foo could be given access to x,y and d for construction only.


It'd become something like:

to foo(): _int(), _int(), _double() # there would be no 'comma operator'  # implementationx, y, d: foo()# And since the counts are known, we can generalize that:to bar(): _int(), _int()to baz(): _double()x,y,d : bar(), baz() # or bar(), , baz() by convention# extra commas could be used to make things "line up", with no effect on the# compiler's interpretation - an empty expression-list-item simply adds 0# expressions to the total. :)


Hmm, I like it. :) Because my assignment syntax is restricted (you can only assign via a statement consisting of {variable-names}:{expressions} - just pluralized right now ;) - and possibly in loop initializers, although I'm still thinking about syntax there), there should be no problems with strange parses.

Quote:
Quote:
I should explain about function returns: the idea is that an expression after the ':' on the first line provides an initialization of the "default return value" (from which the type is inferred). Within the function, the function name can be used, VB-style, to refer to current default return value, and at end of function, or at a bald 'return' statement, said value is returned. 'return ' gets translated into 'function_name: expr; return;', basically.


I rather dislike this plan. It makes it harder to have compile-time errors.

Nearly every time you eliminate a compile-time error, you introduce a possible run-time error.

If the person writing the function forgets to return the proper value, what would be a compile-time error in most sensible languages becomes a run-time "we returned default without meaning to".


I'm sorry, but you don't really convince me. The thing is that this syntax is consistent with the intended variable-declaration syntax (which has the benefit that you're forced to initialize things). You would normally specify the default via a constructor, so that the type is right there, along with a default. I think it's rather hard to explicitly return when you want to return some expression, and forget to type in the expression. If you just forget it at the end of the function - well, when I do that, it's almost always because I was accumulating my return in a result parameter and forgot to return the result parameter - which is exactly the problem I'm trying to fix. ;)

But there is a simple workaround: in the grammar, just allow expression OR capability-list, and flag an error if (capability-list is specified AND (there exists a bald 'return' OR there exists a way to get past the end of the function)). But that's more work for me, and also one of my original hopes was that building the result-parameter paradigm into the language would make it easier to do (N?)RVO.

Quote:
I like the ability to do type introspection of expressions (ie, typeof), but the requirement that you make an expression that returns the type you want seems obtuse.

And it seems pretty damn easy to accidentally say "this function returns 0 by default. Crap, I meant 0 in a 32 bit floating point number, not a 0 integer!"


I think my language is just not typed the way you'd like a language to be typed. :) (It's interesting, though, isn't it, how there is so much more to typing than just strong versus weak, static versus dynamic...) Anyway, that last example isn't a problem because numeric literals would be of a type like 'scalar_numeric'; if you wanted _int or _float, you'd need either an explicit suffix ('i' for _int literals) or a constructor call (although it would probably just have that syntax and be somewhat optimized behind the scenes, ideally anyway).

Quote:

{int x, int y, double d} foo() {  result<int> result_x(x);  result_x = 7;  result_x++;  return y(result_x+5), d(3.1415);}



Hmm, I can do that:

foo() : _int(7), _int, _double   # using capability-lists rather than expressions for the second two :P  foo[0] +: 1  return foo[0], foo[0]+5, 3.1415  # where perhaps the initial item could be implied by an 'empty' item?  # (Here, explicit casting of 3.1415 is not needed because the type has  # already been set.)


Quote:
Sometimes moving an object uses significantly different semantics than assignment.

And sometimes the programmer has to do the work for you, because you (the compiler) cannot figure it out automatically.

Of course, this might be simplified if you remove the possibility to have an actual instance of an object, and restrict objects to reference existence only. Not certain.


They'll be through references of a sort, but not all of them will do pointer-indirection. :) (Or at least, a pointer-indirection of a sort that could and should be optimized away by the C++ compiler.)

Anyway, I still think this one (move operator) falls in the YAGNI bin.

Quote:
Wasn't talking about a type union. I was talking about a function that can return either one of two different types.

The types need not ever exist in the same block of memory.

In ASM, imagine a function that returns either an INT in register R1, or a DOUBLE in register F1. When it returns, it actually returns to a different point of execution depending on which value it returns.

But, you know, done up pretty.


I have to think more about this. But as discussed below, I think it also still falls in the YAGNI bin:

Quote:
In effect, your dynamic cast code is a special case -- a function that can return more than 1 distinct type. Depending on what type your dynamic cast returns on, the function returns into different code -- which means your code is never executing with the wrong type in the wrong branch.

Why restrict this beautiful compile-time safety feature to your dynamic cast operator?


Because "the dynamic-cast operator" is an implementation detail - I was just describing how I think multiple dispatch needs to be implemented. I see your point, but it still seems like syntactic sugar of a sort that I personally can't imagine using much.

Quote:
If you are going to include the language feature, why not include the primitives required to write the language feature in your language, and then write it in your standard library?


True. This should often be a goal.

Quote:
Watch a useful usage:
{error OR special_case OR game_state} check_gamestate( game_state );branch evaluate (check_gamestate( game_state )) {  evaluate is game_state: {    // normal result  };  evaluate is special_case: {    // special case code  };  evaluate is error: {    // do error checking  };}


The interface for the check_gamestate indicates it could return an error, a special game state, or a game state. It requires the caller to pay attention, and unless it returns a game_state, the caller cannot examine the game_state it returned. So you can return semi-dangerous things (such as pointers), and arrange it so the caller cannot look at them unless they are valid.


The key point:

I still don't need it because of assuming-blocks (as long as game_state, special_case and error implement some common interface - which they do: in the worst case, it's called "" ;) Oh, and a cheap way to signal errors, when you don't want exceptions, would be to return an e-valued instance of the defn type.). Although I can see an argument for making 'assuming' behave more like a switch than an if.

Zahlman
Zahlman
This is a brief overview of how I think multiple dispatch needs to work. Although I should probably read the articles from this thread too...




"The intent is to generate the dispatch machinery at compile time, detecting such ambiguities and requiring the user to write overloads for them. Then, code can be generated at compile-time to select the appropriate overload."

The algorithm goes as follows:

For each pair of overloads with the same parameter count:  required_overload_specification <- empty list.  For each corresponding pair of parameters:    If either parameter specifies an exact type:      If the other specifies an interface: continue outer loop.      If the other specifies a different exact type: continue outer loop.      required_overload_specification.append(the exact type).    Otherwise:      required_overload_specification.append(union of the specified interfaces).  If required_overload_specification not in overloads:    report error (ambiguity: a set of parameters could satisfy both overloads,                  so it needs special handling).


Once a valid set of overloads is provided, it should be possible to sort them topologically from most to least specific, and unambiguously traverse the graph ("Let's see, (fooable barrable, bazzable). First arg isn't fooable? Okay, well I have (fooable, bazzable) and (barrable, bazzable) under that, and first arg isn't fooable, so let's try (barrable, bazzable). Is first arg barrable?" etc.).
_goat
_goat
Now you know how I felt when no one responded to my ideas. [grin] - Ah, the things I'd change if I could be bothered.

But, I have an exam in two days, so you'll have to wait 'till then for me to give the whole thread(s) the once-over.
NotAYakk
NotAYakk
Quote:
TBH, when I first learned C++, I had no difficulty with the idea at all, and being able to be implicit about 'self' seems like one of the key objective benefits of making member functions (instead of free functions with a foo* or foo& first parameter) in the first place. It's actually caused me a lot of slowdown in writing Python code! ("Oops, I forgot to pass 'self' when calling a helper function. Oops, I forgot to *accept* 'self' in the helper function.")


OTOH, I find member functions are useful for
1> object.member() syntax indicates they are "part of" the object in question.
2> virtual overrides allowing neat stuff to happen.

Saving the typing for an explicit self -- especially if you had, say, a this_type keyword -- never really was at the top of my list.

The advantage of having an explicit self is that member functions are no longer nearly as much of a special case.

Heck, I've started using this->member in C++. It distinguishes between class-level and local variables in my methods.

Quote:
I would like stricter scoping than Python offers, too. Once that's in, might as well take advantage of the lack of ambiguity.


Hmm?

Quote:

# within the implementation, names not resolvable as locals
# are tried to be resolved as members of _internal_name, and failing that,
# as globals.


Here is something insane -- I vote you should have to explicitly list your global dependancies, at least on variables.

On the other hand, if you leave the idea of "free, unbound variables" that have to be bound, you could have syntax to attempt to bind free variables within this function as members of the first arguement.

Ie, take python-style decorators one more step. A compile-time phase in which the programmer can have logic run that can bind variables properly.

Then, if you want the implicit binding of free variables to the first parameter, you'd do something like:

decorator method( function f )( parameters a ){  function decorated = f;  parameter first = front_of( a );  parameters tail = tail_of( a );  foreach free_var free_variables_in( f ) {    if (valid_member( first, free_var )) {      decorated = bind_variable( decorated, free_var, member( first, free_var ) );    }  }  decorated = bind_variable( decorated, 'this', first ); // edit: forgot this!  bind_parameters( decorated, tail );}


Ie, a compile-time language to make decisions about how free variables are bound and rerouting of parameters.

You'd do something like:
class foo {  int y;  @method bar( int x )    return x+y}


bar, all by itself, would have a free variable "y" unbound.

The @method would take the function bar, and return a function that takes 1 more parameter (the this pointer), and bind the free variable "y" to this->y.

Yes, I'm insane.

What such a fancy compile-time logic allows is the ability for the programmer to create really powerful descriptors.

Imagine:
@reader
@writer
@atomic
@logged
@debug_logged
@thread_unsafe

etc.

decorators that can bind free variables in your methods or functions to the correct symbol. Which can inject code before or after your method runs.

So your read locking method is not only locked, but the method has access to the variable "my_lock" and can query it, manipulate it, or pass it on to other functions.

And changing your read locking methods to log is a matter of changing your read locking descriptor.

For people who want unbound parameters to do a global lookup, they would just put in a:
@side_effects
descriptor. And you could "ship" with standard descriptors that do "what you think the user wants" -- @method, @function, @static_method, @operator, @pure_functional, etc.

Ideally, these standard descriptors would be written in the language. So if someone has a better idea, they could write it.

Once again, this comes from the idea of functions in formal logic.

The function doesn't have to be complete for it to be useful -- it can have "loose parameters" which are later bound. But a function that still has "loose parameters" isn't complete -- it can't be called until every parameter is bound.

Quote:

Hmm, I like it. :) Because my assignment syntax is restricted (you can only assign via a statement consisting of {variable-names}:{expressions} - just pluralized right now ;) - and possibly in loop initializers, although I'm still thinking about syntax there), there should be no problems with strange parses.


Steal from pascal -- what about := for assignment? I like the "=" when I move data around...

= would be invalid, and == be "test equality".

I agree that = for assignment and == for equality was a silly error on C's part.

Quote:

I think it's rather hard to explicitly return when you want to return some expression, and forget to type in the expression. If you just forget it at the end of the function - well, when I do that, it's almost always because I was accumulating my return in a result parameter and forgot to return the result parameter - which is exactly the problem I'm trying to fix. ;)


Can the compiler catch that? Personally, I'm less worried about easy-to-fix compilation errors "you didn't return anything from this function nimrod" than I am about run-time errors.

Easy to detect and bonk the head of programmer compile-time errors are next to free...

Quote:
But there is a simple workaround: in the grammar, just allow expression OR capability-list, and flag an error if (capability-list is specified AND (there exists a bald 'return' OR there exists a way to get past the end of the function)). But that's more work for me, and also one of my original hopes was that building the result-parameter paradigm into the language would make it easier to do (N?)RVO.


Give your functions access to their output parameters. In my opinion, every parameter should have a name, and that name should 'matter'. (in C++, parameter names when you forward decl a function don't matter)

Output parameter: a parameter that has no data that the function is allowed to read.
Input parameter: a parameter that can be read from, but no changes propogate out.
I/O parameter: a full reference/pointer to an external variable.

There are two kinds of Output parameters -- assignment and construction. You could hide the existance of the two kinds from the implementation of functions -- force output parameters to be constructed within each function before the function returns -- or actually distinguish between the two.

I don't think there is a need to force construction at the top of the function. Just force construction of your output parameters before the end of the function.

Quote:
Anyway, that last example isn't a problem because numeric literals would be of a type like 'scalar_numeric'; if you wanted _int or _float, you'd need either an explicit suffix ('i' for _int literals) or a constructor call (although it would probably just have that syntax and be somewhat optimized behind the scenes, ideally anyway).


Please tell me that doesn't mean you are using any kind of hungarian notation?

Quote:
Anyway, I still think this one (move operator) falls in the YAGNI bin.


C++ needs a move operator, which is why I want one in any language I poke at. :)

C++ operators assume that copy and move cost the same, because for primitive types this is true.

But with complex objects (like std::vectors), move is much cheaper than copy. And many of the operations of C++ don't need copy -- they don't need the source object to continue to be valid.

Returning from a function should use a move operator, if the data being returned is a local variable or a temporary.

When I need to manually move data, I either am forced to use pointers, or use "swap, then let source fall out of scope". But what I really want to do is move, it is just hard to do in C++.

...

Your overload machinery seems scary. :) I'll look at it later.
Zahlman
Zahlman
... You know, NAY, I think I now understand how GVR feels when he deals with the Lisp-heads on the Python devteam... :)

Quote:
Original post by NotAYakk
Quote:
TBH, when I first learned C++, I had no difficulty with the idea at all, and being able to be implicit about 'self' seems like one of the key objective benefits of making member functions (instead of free functions with a foo* or foo& first parameter) in the first place. It's actually caused me a lot of slowdown in writing Python code! ("Oops, I forgot to pass 'self' when calling a helper function. Oops, I forgot to *accept* 'self' in the helper function.")


OTOH, I find member functions are useful for
1> object.member() syntax indicates they are "part of" the object in question.


But now you want to destroy that by allowing equivalence of member and free functions (which I mostly agree with)?

Quote:

2> virtual overrides allowing neat stuff to happen.


But that hardly seems relevant when you have multimethods, which was the goal we started from, yeah?

Quote:
Saving the typing for an explicit self -- especially if you had, say, a this_type keyword -- never really was at the top of my list.


Is that supposed to be "'this'-type keyword", or "this_type keyword"? :) (And if the latter, what does it mean?)

Quote:

The advantage of having an explicit self is that member functions are no longer nearly as much of a special case.

Heck, I've started using this->member in C++. It distinguishes between class-level and local variables in my methods.

Quote:
I would like stricter scoping than Python offers, too. Once that's in, might as well take advantage of the lack of ambiguity.


Hmm?

Quote:

# within the implementation, names not resolvable as locals
# are tried to be resolved as members of _internal_name, and failing that,
# as globals.


Here is something insane -- I vote you should have to explicitly list your global dependancies, at least on variables.


Member function *implementations* would be a special case, in terms of the name lookup. After that, things look quite similar. Now there becomes a use again for writing things as members, in addition to just expressing some semantics.

Another thing I had in mind.

Now that I think about it, explicit listing of global variables seems like a useful safety feature, too.

In general, I am thinking of allowing "useful safety features" to be enabled or disabled by pragmas, a la "use|no strict refs" in Perl. These would apply for other things, too: for example, you could either explicitly state the indentation size with a pragma, or let the compiler infer it by assuming the first indented line in the file is indented one step.

As for your "hmm", the quoted comments are intended to elaborate on the idea. ;) Python only offers 'local' and 'global' scope. If there is additionally 'member' scope, we don't need explicit-self any more. The resolution order is: local; direct member of class; member of composed object (for methods only, recursively, left-to-right, only looking in those objects that are marked as delegates for an interface with that method); global (for methods only, unless the dependency is explicitly marked). Seems like what I want.

snipped: lots of "decorator" implementation and theory, which does sound interesting, but over my head. It seems to me that a lot of the features I wanted could be interpreted as specific kinds of decorators being built right in, rather than giving the user flexibility. This is why I mentioned GVR vs. Lisp: the situation seems analogous to me to GVR's desire to get rid of reduce() etc. (which I oppose!) because the common cases of sum() and product() are explicitly provided, and because GVR supposedly doesn't really understand all this FP stuff.

TL;DR: This is stuff that I will have to keep on the back burner. Hopefully, while implementing stuff, I will see a refactoring, which leads to a decorator "concept" in the language implementation code, which can then be reflected in the language syntax. But as it stands, I just can't fathom how to implement it.


Quote:
Quote:

Hmm, I like it. :) Because my assignment syntax is restricted (you can only assign via a statement consisting of {variable-names}:{expressions} - just pluralized right now ;) - and possibly in loop initializers, although I'm still thinking about syntax there), there should be no problems with strange parses.


Steal from pascal -- what about := for assignment? I like the "=" when I move data around...

= would be invalid, and == be "test equality".

I agree that = for assignment and == for equality was a silly error on C's part.


I currently have = for equality and : for assignment. I don't want to double up the equals sign because it's ugly and redundant (even at a character/token level that bothers me ;) ). I don't want Pascal's := exactly, because again, ':' conveys the meaning to me just fine, and also because it reads sort of Englishy to me ("mr_smith : man(72i)". "Mr. Smith: a man standing 6'0".").

Quote:
Quote:

I think it's rather hard to explicitly return when you want to return some expression, and forget to type in the expression. If you just forget it at the end of the function - well, when I do that, it's almost always because I was accumulating my return in a result parameter and forgot to return the result parameter - which is exactly the problem I'm trying to fix. ;)


Can the compiler catch that? Personally, I'm less worried about easy-to-fix compilation errors "you didn't return anything from this function nimrod" than I am about run-time errors.

Easy to detect and bonk the head of programmer compile-time errors are next to free...


The "problem" is solved (Java for example verifies that every exit path in a non-void function either returns a type-compatible value or throws an exception), so I have no worries there.

But part of my motivation in the language design is removing "annoyance factor". If you *want* to specify the return value on every path and let the compiler check you, there would be the capability-list version as suggested (I now favour this approach, in keeping with the idea of "turning things off if you don't want them"). But you can also easily use the "normal" form in order to save yourself effort and avoid recompiles for bonks-on-the-head, at the possible expense of safety (which I'm still not really convinced about).

Quote:
Quote:
But there is a simple workaround: in the grammar, just allow expression OR capability-list, and flag an error if (capability-list is specified AND (there exists a bald 'return' OR there exists a way to get past the end of the function)). But that's more work for me, and also one of my original hopes was that building the result-parameter paradigm into the language would make it easier to do (N?)RVO.


Give your functions access to their output parameters. In my opinion, every parameter should have a name, and that name should 'matter'. (in C++, parameter names when you forward decl a function don't matter)


The output parameter has a name: the name of the function. If you want to return a tuple, then being a tuple, each individual element has a name: the name of the function, subscripted with the element index. Although, I probably should implement "named tuple elements", since it would only be compile-time overhead. To that end, I can probably reuse code I'm planning on for different "storage models" for classes.

An "assignment output parameter" would be a return value or return-value-element specified by a capability-list, so I think I satisfy all the requirements this way - okay, 'parameters' are never output-only and we're still following a C++-style "function model", but with tuples that fill in the gaps.

Hmm. I wonder it will be difficult to deal with mixed constructed and not-constructed elements in a return-tuple. :
Quote:
Quote:
Anyway, that last example isn't a problem because numeric literals would be of a type like 'scalar_numeric'; if you wanted _int or _float, you'd need either an explicit suffix ('i' for _int literals) or a constructor call (although it would probably just have that syntax and be somewhat optimized behind the scenes, ideally anyway).


Please tell me that doesn't mean you are using any kind of hungarian notation?


Of course not; I said *suffix*, and this is only for literals (not to mention, there is plenty of precedent). '72i' is an _int. '72r' is a _real, for example (even if you don't specify a .0 or whatever. Maybe "72.0i" should be a warning; maybe an error.) '72j' is a _complex ;) '72' is just a numeric, though. No underscore.

The whole "numeric tower" will probably need a lot of careful thought and planning, though. I will probably want to look in to how typical LISP implementations handle things; I hear it's quite nice.

Quote:
Returning from a function should use a move operator, if the data being returned is a local variable or a temporary.


Agreed. I'm hoping that I can handle this with internally-generated transfer_handle's or something (auto_ptr-like). Naturally, I would want to design things in such a way that the language could take advantage of move constructors, etc. when they are added to C++. :)
BrianL
BrianL
Quote:
I currently have = for equality and : for assignment.


No no no! Everyone knows : is for slices. ;)

More seriously, the fewer conventions a language breaks, the more likely I will use it. Introducing use of non-conventional symbols for equality and assignment will just make your langauge harder for other people to pick up. Big differences (ie lisp vs C) are easy to keep in mind. Little syntactic differences rae much tougher.

If that isn't a concern, no problem at all! :) Its your language after all.
NotAYakk
NotAYakk
First, the N-ary dyanmic dispatch problem can't be solved at compile time.

You can determine if there are ambiguities at compile time, or just have rules/syntax that prevent the possibility of ambiguities... But, by definition, dynamic dispatch is a run-time thing.

Quote:
Original post by Zahlman
... You know, NAY, I think I now understand how GVR feels when he deals with the Lisp-heads on the Python devteam... :)


On the other hand, the functional parts of Python are some of it's most powerful innovations.

Without the functional parts, it is just an interprited object based language with non-mutable strings, run-time typing of variables, and white space that signifies...

Quote:

Quote:
Original post by NotAYakk
OTOH, I find member functions are useful for
1> object.member() syntax indicates they are "part of" the object in question.


But now you want to destroy that by allowing equivalence of member and free functions (which I mostly agree with)?


Naw. A member function is a function. But a function with a foo* as the first parameter maybe shouldn't work as a member function...

Sort of a namespace thing -- obj.func looks for func inside object.

This could be important just to prevent someone from doing a obj.size, and grabbing a completely wrong size function that some 3rd party wrote.

Quote:
Quote:

2> virtual overrides allowing neat stuff to happen.


But that hardly seems relevant when you have multimethods, which was the goal we started from, yeah?


*nod* :)

Quote:
Quote:
Saving the typing for an explicit self -- especially if you had, say, a this_type keyword -- never really was at the top of my list.


Is that supposed to be "'this'-type keyword", or "this_type keyword"? :) (And if the latter, what does it mean?)


class foo:  operator+( this_type self, this_type other )


Ie, "this_type" is implicitly typedef'd to the type of the class in the enclosing block. It wouldn't have to be hard-coded into the language if you go the "everything is a decorator" route.

Just an idea. I find it is something I end up doing a heck of alot in C++.

class foo {  typedef foo this_type;  this_type& operator+(this_type const& other) const;  ...};


Quote:
In general, I am thinking of allowing "useful safety features" to be enabled or disabled by pragmas, a la "use|no strict refs" in Perl. These would apply for other things, too: for example, you could either explicitly state the indentation size with a pragma, or let the compiler infer it by assuming the first indented line in the file is indented one step.


I view programming as a many step process.

Naively, writing code is "write code, see it run".

But most languages with a type system actually do "write code, compile-time checks that verify the code makes sense, then see it run".

C++ has gone another step. C++ is currently "write code that changes the language, write code that uses the changed language, compile-time checks that verify that the code makes sense, then see it run".

How does C++ change the language? Templates.

Well, and macros. :) But less macros.

C++'s templates aren't really run-time code. They are compile-time code that generates code.

This is what I like to call a brain-lever. Code that writes code, and code that checks that your code does what you think it does.

That is why I think the ability to static type check is powerful. And templates are even more powerful.

Each level of indirection gives you more leverage -- you can write code that more concely expresses what you mean, and the computer can do more of the work for you.

Now, some languages go off and try to predict what you want. Java, C#, and many other languages do this -- they add features that solve particular problems (garbage collection, iteration, etc).

When I look at languages like Ruby and Python, I see not "yet another interprited language". The interesting parts of Ruby and Python are the abilities to write code that writes code -- and not just as a bunch of print statements, but rather in a way that is inside the language.

...

Now, you can manage these insanely powerful indirect coding techniques without serious language support. Some of the coolest stuff I've seen is when someone wrote a graph-based graphics renderer language within C++. They wrote code that generates graphs, wrote code that runs the graph, and wrote UI code to make building graphs easier.

What they envisioned as 20 to 30 nodes connected together ended up being millions of nodes connected together. Graphs larger than they had any hope of grasping.

And we ended up with an engine capable of doing things thousands of times more complex than the programmer could write. The engine was an exponential multiplier on the programming ability of the programmer.

That leverage is what I want to see in a compiled language. Being able to generate exponential amounts of logic and complexity in linear code.

...

And you can pull of N^2 logic from N code in the C++ template pretty easily.

Quote:
snipped: lots of "decorator" implementation and theory, which does sound interesting, but over my head. It seems to me that a lot of the features I wanted could be interpreted as specific kinds of decorators being built right in, rather than giving the user flexibility. This is why I mentioned GVR vs. Lisp: the situation seems analogous to me to GVR's desire to get rid of reduce() etc. (which I oppose!) because the common cases of sum() and product() are explicitly provided, and because GVR supposedly doesn't really understand all this FP stuff.

TL;DR: This is stuff that I will have to keep on the back burner. Hopefully, while implementing stuff, I will see a refactoring, which leads to a decorator "concept" in the language implementation code, which can then be reflected in the language syntax. But as it stands, I just can't fathom how to implement it.


Decorators in Python are simpler than what I described. They are simply functions with take a function, and return a function. In Python, you can munge arguements at run-time. So a decorator could add a logger wrapper, thread safety, make the "self" pointer optional, run callbacks, or do a multitude of other things.

What I described was something a bit more. Rather than have all that busyness run at run-time, what if at compile time your language could manipulate, create, and reassign functions?

...

Quote:
Quote:


Steal from pascal -- what about := for assignment? I like the "=" when I move data around...

= would be invalid, and == be "test equality".

I agree that = for assignment and == for equality was a silly error on C's part.


I currently have = for equality and : for assignment. I don't want to double up the equals sign because it's ugly and redundant (even at a character/token level that bothers me ;) ). I don't want Pascal's := exactly, because again, ':' conveys the meaning to me just fine, and also because it reads sort of Englishy to me ("mr_smith : man(72i)". "Mr. Smith: a man standing 6'0".").


*nod*, but someone who isn't famililar with your language might get mighty confused.

Extra characters are not a problem. You will have >= <= anyhow, right?

Quote:
Quote:

Can the compiler catch that? Personally, I'm less worried about easy-to-fix compilation errors "you didn't return anything from this function nimrod" than I am about run-time errors.

Easy to detect and bonk the head of programmer compile-time errors are next to free...


The "problem" is solved (Java for example verifies that every exit path in a non-void function either returns a type-compatible value or throws an exception), so I have no worries there.

But part of my motivation in the language design is removing "annoyance factor". If you *want* to specify the return value on every path and let the compiler check you, there would be the capability-list version as suggested (I now favour this approach, in keeping with the idea of "turning things off if you don't want them"). But you can also easily use the "normal" form in order to save yourself effort and avoid recompiles for bonks-on-the-head, at the possible expense of safety (which I'm still not really convinced about).


*nod*. But failure to compile is not an annoyance. It can also be a useful "damnit, I did forget to assign a return value there!"

A language that compiles no matter what gibberish you feed it isn't a good language. You want your language to fail to compile. :)

Quote:
Quote:
Give your functions access to their output parameters. In my opinion, every parameter should have a name, and that name should 'matter'. (in C++, parameter names when you forward decl a function don't matter)


The output parameter has a name: the name of the function. If you want to return a tuple, then being a tuple, each individual element has a name: the name of the function, subscripted with the element index. Although, I probably should implement "named tuple elements", since it would only be compile-time overhead. To that end, I can probably reuse code I'm planning on for different "storage models" for classes.


Bah -- that's a boring name. And for tuples, not always that useful.

Quote:
An "assignment output parameter" would be a return value or return-value-element specified by a capability-list, so I think I satisfy all the requirements this way - okay, 'parameters' are never output-only and we're still following a C++-style "function model", but with tuples that fill in the gaps.

Hmm. I wonder it will be difficult to deal with mixed constructed and not-constructed elements in a return-tuple. :


Tuples exist in C++ -- they are nice, but they aren't the same as named parameters.

Being able to take a function, and wire up everything using names, can be powerfully self-documenting.

Quote:
Of course not; I said *suffix*, and this is only for literals (not to mention, there is plenty of precedent). '72i' is an _int. '72r' is a _real, for example (even if you don't specify a .0 or whatever. Maybe "72.0i" should be a warning; maybe an error.) '72j' is a _complex ;) '72' is just a numeric, though. No underscore.

The whole "numeric tower" will probably need a lot of careful thought and planning, though. I will probably want to look in to how typical LISP implementations handle things; I hear it's quite nice.


Bah. Ok.

Will you allow people to add their own suffixs?

Ruby: "Special cases aren't special enough."
Zahlman
Zahlman
Quote:
Original post by NotAYakk
First, the N-ary dyanmic dispatch problem can't be solved at compile time.

You can determine if there are ambiguities at compile time, or just have rules/syntax that prevent the possibility of ambiguities... But, by definition, dynamic dispatch is a run-time thing.


... Well yes, of course. I determine ambiguities at compile-time, and reject code with ambiguities. If there are none, then I generate code *that will do* the dispatch at run-time.

Quote:

Quote:

... You know, NAY, I think I now understand how GVR feels when he deals with the Lisp-heads on the Python devteam... :)


On the other hand, the functional parts of Python are some of it's most powerful innovations.

Without the functional parts, it is just an interprited object based language with non-mutable strings, run-time typing of variables, and white space that signifies...


You see, I agree to a large extent, which is why this discussion is often so difficult for me. :) Definitely, Python's strength (common to C++) is in the multi-paradigm support. Metaprogramming... well, Python has really impressive (and I mean bordering on positively insane) reflection capabilities, which let you effectively do your metaprogramming that way. But that only works in a fully dynamically typed environment, as best I can figure, and I want to avoid that. (The goal is static, simply-inferred, non-manifest typing, with an implicit bias towards dynamic polymorphism so as to instill "dynamic typing feel".)

I do want a certain amount of metaprogramming flexibility, but really not that much seems to be needed. I don't even really know where or how (or why?) to draw the line, which is the frustrating part, but I intuit when I get close to it. :
Quote:

class foo:  operator+( this_type self, this_type other )


Ie, "this_type" is implicitly typedef'd to the type of the class in the enclosing block. It wouldn't have to be hard-coded into the language if you go the "everything is a decorator" route.

Just an idea. I find it is something I end up doing a heck of alot in C++.


Suddenly you're the one pushing for another keyword and I'm resisting? :)

Seriously, though: you could just typedef, and I can't easily imagine why you're doing it "a heck of alot". It sounds like something primarily useful either if you're C&P'ing something (ouch) or doing some TMP magic (also ouch, but in a different way).

Quote:

But most languages with a type system actually do "write code, compile-time checks that verify the code makes sense, then see it run".

C++ has gone another step. C++ is currently "write code that changes the language, write code that uses the changed language, compile-time checks that verify that the code makes sense, then see it run".

...

That is why I think the ability to static type check is powerful. And templates are even more powerful.

Each level of indirection gives you more leverage -- you can write code that more concely expresses what you mean, and the computer can do more of the work for you.


Well, OTOH. ;)

Yes, I do want static typing. But:

Quote:
Now, some languages go off and try to predict what you want. Java, C#, and many other languages do this -- they add features that solve particular problems (garbage collection, iteration, etc).


To a large extent, this is the direction I'm headed - sorry if you don't want to go. However, instead of being guided by the compass of convenience, I am fleeing from boilerplate towards the promised land ;) That is, to say, things like: instead of solving memory management problems by GC, I want to solve them with automatically-generated wrappers that handle Rule of Three-like stuff. (You can get GC, but you have to ask for it; while it's a nice-to-have, there are things that are nice enough for most purposes, have advantages - deterministic destruction - and are simpler implementation-wise.) Instead of removing operator overloads, I offer a restricted set of them (e.g. +:, <=>), and infer the others (e.g. +, {<, >, =, >=, <=, !=}). (Some might be inferred but also overridable; e.g. scalar multiplication could be implemented by repeated addition in the absence of an explicit operator, but with appropriate performance warnings from the compiler. I don't know that I want that, though. Probably better to let the operator* require separate implementation...)

Quote:
Decorators in Python are simpler than what I described. They are simply functions with take a function, and return a function. In Python, you can munge arguements at run-time. So a decorator could add a logger wrapper, thread safety, make the "self" pointer optional, run callbacks, or do a multitude of other things.


That sounds redundant when functions are objects.

Quote:
What I described was something a bit more. Rather than have all that busyness run at run-time, what if at compile time your language could manipulate, create, and reassign functions?


That makes it seem useful (compile-time optimizations can then be applied to the wrapped version), but a bear to implement.

Quote:
[operators] Extra characters are not a problem. You will have >= <= anyhow, right?


Well, of course; single characters don't exist (in normal character sets :) ) that convey the meaning.

Again, something where pragmas could help - my brain-dead take on metaprogramming, I know. But with something like this, you have more control than just using preprocessor macros (it can only substitute tokens one-for-one; and you have expressive ways to either replace or just alias a symbol/keyword).

I did originally consider the "CS pseudocode" symbol of <- for assignment, too, BTW. :)

Quote:
*nod*. But failure to compile is not an annoyance. It can also be a useful "damnit, I did forget to assign a return value there!"

A language that compiles no matter what gibberish you feed it isn't a good language. You want your language to fail to compile. :)


Well, people presumably have a reason for using dynamically typed languages. :) I'm trying to make some compromises here.

Quote:
[More about function I/O and tuples]
Being able to take a function, and wire up everything using names, can be powerfully self-documenting.


Hmm, can I do it? Given named tuples, expression and construction representations for output parameters, and a little access to the type-inference engine, we could pray for something like (but see noted difficulties):

to foo(wibbler w) : _quux result, _int(0) # error count  # no implementationto magic(typeof<foo> func, wibbler w): func.bind_out(result, w.create_quux()).bind_in(w, w)  # typeof<foo> gets expanded to _lambda<_output<unbound<_quux>, _int>, wibbler>  # the first parameters for bind_out and bind_in would have to be looked up  # in a symbol table associated with that type , or something.   # Note that the 'result' parameter could still be referred to as parameter 0.  # The error count can *only* be referred to as parameter 1 (right now).  # XXX: if this is to work, then it seems like we can't also have  # to bar(wibbler x) : _quux output, _int(42) be the same type as foo.  # (and conversely, if typeof<foo> == typeof<bar>, then where do we look up  # the names?  # XXX: Can we name constructed parameters? The obvious syntax is just  # '_int(0) error_count', but can that create any syntactical ambiguities?  # inferred return type: _lambda<_output<_quux, _int>>a, b: magic(foo, _wibbler())() # a is _quux(_wibbler().create_quux()); b is _int(0)


Quote:
Will you allow people to add their own suffixs?

Ruby: "Special cases aren't special enough."


Python also has this idea (sum/product notwithstanding I guess ;) Maybe it's intended more for programmers than language implementors). But with these suffixes, there are problems:

1) For user-defined suffixes to be meaningful, first we have to make *literals of user-defined types* meaningful. What are those? Anonymous instances created from another literal, is my best guess. But (a) we already have syntax for those (anonymous constructor call), and (b),

2) adjacent suffixes get very ambiguous very quickly. I am already thinking along the lines of numbers on the complex integer lattice being specified like 23i + 42ij. (This is of course technically two separate literals; the idea is that the optimizing compiler would constant-fold them into one.) The i vs j usage is confusing enough, but now the 'ij' pair is two stacked suffixes. If someone wants an 'ijk' suffix to indicate "3x3 identity matrix times given scalar", and someone else wants a 'k' suffix to indicate "Kelvin temperature object"... well, you get the picture.

... You know, I am starting to question the idea of the 'numeric interface' after all; would it ever make sense to make decisions like, say, whether or not to truncate division at runtime? There are a lot of possible sub-interfaces here for all sorts of weird number-like things that mathematicians keep inventing :D Math is too hard. ;) The 'numeric tower' will need a lot of work no matter what.

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.