3

No idea how to write a good title for this question.

I'm thinking about introducing an operator in a DSL that makes hidden identifiers (like variables) accessible. Think this.foo in Java or C# to access a member that was hidden by a local foo. Or base.bar when bar is a member in the current class. Let's call this "explicitly accessing an enclosing scope".

Target audience of the DSL are non-programmers / novices, which should not be required to understand (in detail) the scopes they're working in. So the idea is to instead introduce an operator that skips one occurrence of an identifier and can be applied multiple times. The compiler will do his usual thing when resolving identifiers, starting in the local scope and moving outward. When he finds something and the operator is used, he ignores the result and keeps on looking.

Consider this pseudo-code example:

foo = 1 // global
object bar {
  foo = "member foo" 
  function bla() {
    let foo = 2
    // _ we are here
  }
}

In the target location, foo refers to the local variable with value 2.
@foo will be the member of type string (skip one occurrence)
@@foo will be the global variable with value 1 (skip two)

The alternatives in languages like Java or C# would be this.foo for the member and some imaginary global::foo for the imaginary global. The problem with that is a) users must learn several keywords and b) users must understand the exact structure of scopes (what does this refer to etc.)

The actual question: is this a bad idea, are there some severe problems I'm not seeing?
Or are there perhaps any languages that employ such an operator that I could look into?

A bit more context that may be relevant:

  • this is not a general-purpose programming language
  • it's a mix of textual and visual elements (think Excel), so it's clear visually what is contained in what (and where skipping occurrences will take us)
  • static typing, no dynamic scope (evaluation at compile time)
  • The idea is that people "get" that their thing has the same name as a different thing, but they don't know the magic words to explicitly refer to that thing's container.
  • writing @foo where there is only one foo will result in an error

Update: A few observations:

  • Lexical Scopes: I failed to mention this, but the operator would escape all lexical scopes when used first. If there are 3 variables in 3 nested blocks, like loops or functions, you cannot use @ to address them. It is only intended to access non-lexical scopes (objects, global scope). I believe lexical scopes are even more difficult to understand for novices and thus they wouldn't understand how many @ they'd need to address a member.
  • Brittleness: As John R. Strohm mentions, introducing a new foo or removing one from an object will break all references to existing foo resp. the number of @ required to address them will change. That is indeed a problem, but isn't that the case in e.g. C# as well? Consider this example:

.

namespace foo {
    class X {
        public static int Value;
    }

    namespace bar {
        class foo { }
        class X {}

        class Main {
            public static int Value = global::foo.X.Value + 1;
        }
    }
}

By creating a new class users can hide other types and namespaces in outer scopes. Existing code will break and must be updated, either by prefixing namespace names or global:: if namespaces were hidden.

The point is: this problem exists in languages like C# and doesn't cause problems. The problem is more severe with the @ operator because removing things will also cause it to fail, whereas explicitly addressing scopes fixes the problem once and for all. My argument to that is this: when you have to fix your code when adding stuff, it can be expected that you also have to fix it when removing stuff.


Update: I accepted John's answer because he points out a problem that I believe is worse than the one most others mentioned (copying code to a different location may alter which symbols are being addressed).
The problem is that if a user creates a new symbol foo in one layer he alters which foo symbols are resolved in nested layers. In other words an invisible change to code that is not currently being edited. In my opinion this is worse than the copy-paste problem, as that can at least be dealt with right then and there and while it still my go unnoticed, the user has a greater chance of discovering it.

6
  • 3
    It would be simpler to just prohibit hiding at all. Then the user will never accidentally refer to an inner name while attempting to refer to a hidden name, and they will never have to use any special syntax to refer to a hidden name (since hidden names can't exist). Jul 18, 2017 at 16:45
  • That's right, but it's also very restrictive. Every name you use must be unique (in relation to what's already there in enclosing scopes). I've tried that initially and users were very upset when greeted with a "this name is already in use!" error message. Also, there are many legitimate use cases for hiding. When people never need to use the hidden thing, there's no problem at all. It's actually rare that hidden stuff must be accessed, which is why I'm looking for a simple general-purpose solution instead of introducing several magic words.
    – enzi
    Jul 18, 2017 at 16:52
  • The standard comment applies: Does the world really need Yet Another special-purpose ad hoc scripting language? It has been proven that non-programmers have no trouble at all writing LISP (!), as long as you DON'T TELL THEM that they are programming, or that this is a programming language. Jul 18, 2017 at 17:24
  • 2
    @enzi: It wasn't a formal study. Xerox delivered an advanced multi-user word processing system, I don't recall the name any more. The extension language was LISP. Nobody told the secretaries who used it that LISP was a programming language, and they started hacking up extensions and modifications as they needed them. Jul 18, 2017 at 18:45
  • 1
    @john There will never be enough languages.
    – svidgen
    Jul 19, 2017 at 0:16

6 Answers 6

1

You've stated that you intend lexical scoping as opposed to dynamic scoping. While this does solve some problems, it creates others. (GNU Emacs LISP is dynamically scoped, so as to allow users to write extensions that modify the behavior of other extensions.)

You appear to intend to allow multiple levels of scoping, potentially requiring the user to do something like "@@@@@@@foo", to go up half-a-dozen levels to get to something, if "foo" turns out to be a really popular identifier.

The obvious troublemaker is this: If you have a "foo" three layers up, that is not hidden anywhere in between there and here, and you can get to it just by saying "foo", then there are at least three places where someone else can break your code just by defining a new "foo".

This seems to me to be a recipe for disaster.

I would suggest that you instead consider abandoning the nested procedure model, and go to a "flat" model instead. You have "local" variables, and then you have imported objects, referred to by name, say "foo", and entities inside those objects use a qualified syntax, say "foo.waldo". (Yes, you could use C/C++ pointer syntax, "foo->waldo", or even PASCAL pointer syntax "foo^.waldo".)

3
  • John, I updated my question with a few thoughts to your points. Considering the C# example and taking into account that lexical scopes are not affected, what do you think? As to your suggestion: addressing entities by name is already a possibility, but what if that name is hidden by a local variable? Should there be an alternative to address an enclosing object, or would you consider it the user's fault and require him to rename his locals?
    – enzi
    Jul 19, 2017 at 9:47
  • @enzi: By "local variable", I assume you mean "variable local to the immediate procedure". If I know I need to access a global object that has a certain name, it seems kind of obvious to me that I should not define a local variable with the same name. Jul 19, 2017 at 14:14
  • It's pretty commonplace for constructor parameters to be named exactly like the members they will be assigned to. To be able to do that, this. must be prefixed. Without this, programmers must call their parameters differently or can't assign them to their members. Not relevant to my DSL, just a real-life example because I'm unsure how often people actually hide things with parameters or variables.
    – enzi
    Jul 19, 2017 at 14:52
4

As I understand it, you're suggesting a construct: @ prefixed to an identifier, e.g. @foo, which goes to the next (2nd) visible foo starting from the current scope going to outer scopes.

Thus, writing @foo requires the existence of a 2 foos in the scope (one hiding the other).

I don't care for the design, because it means the code written using that construct, e.g. @foo, captures with it acknowledgement of the presence of another foo (that it isn't interested in) making the code more brittle, because — to one way of looking at it — it is a reference of both foos.

Code written using @@foo implicitly captures the existence of two other foos that it is not interested in.

This suggests to me that simple code will be prone to more copy & paste errors.

(Further, a foo and bar at the same scope may require @ for one but not the other — I'd rather qualify both or none, as in the examples you provide, re: this. and global::.)

1
  • Thank you for your thoughts! Your last point I used to think is the same in Java/C#, where you may have to use this. for one but not the other – the difference to @ is that you can use this. for both, whether or not it is necessary, but you cannot use @ on both. Good point.
    – enzi
    Jul 19, 2017 at 10:10
3

Think how easy it is to make an mistake. For example if someone writes

@@foo = @foo

instead of

@foo = @@foo

Now how long do you think it would take your novice programmers to spot this error.

What i think would make more sense is to make it easy to find which foo you one.

For example

  • @global.foo (ok i understand this one)
  • bar.foo (this is the foo defined in the bar object)
  • bla.foo (this is the foo defined in the bla function)

But that's assuming you would really need to access the outerlying scopes at all. in my opionion a DSL should be about minimizing complexity. So introduce tooling that avoids the need of something like this at all. A DSL should be about writing business rules in the way they understand it. If you have nesting your scopes 5 deep then you are on the wrong path.

Update: An other way of dealing with this issue is to never allow 'regular' global variables. If a global variable is always defined and called (explicitly) as "global.foo" then you also avoid the clash in naming between 2 foo variables. A user would have to define a variable like this

let global.foo = 5

if he wanted a global variable

2
  • bar.foo as you describe it is available. The problem is that bar can also be hidden by a local variable. Bad idea obviously if you want to access members of your bar object. I feel compelled to provide a "way out" of this situation, but maybe it's not even necessary. I'm interested in your opinion: how would you feel about a language where bar.foo is the only way to access a member of bar and if you create a local variable bar you close the door on that access? Meaning, you need to rename your local variable.
    – enzi
    Jul 19, 2017 at 7:32
  • 1
    My point is that instead of allowing local variables/a this operator you always give some other named operator. I think allowing local variables to be named the same as global variables is a source of errors that people with less experience will miss too often. So if you always name them bar.foo and bla.foo (and never just foo) then you have an alternative way of dealing with this short of 'magic' @ tokens
    – Batavia
    Jul 19, 2017 at 16:42
1

Such language constructs are not really necessary: If a user wants to access a symbol in a surrounding scope, they could first create an alias for the outer symbol in the outer scope with a different name:

let foo = "outer"
let outer_foo = foo
{
  let foo = "inner"
  // can use foo and outer_foo
}

Most mainstream programming languages therefore have no mechanism to access outer scopes from a nested scope. Related but completely different:

  • accessing instance members like this.foo in Java, C#; this->foo in C++; @foo in Ruby.
  • looking up a symbol in a (global) namespace (ike name.space.Symbol in Java, C#, Python; name::space::Symbol in C++, Perl.

A notable exception is Python, where the nonlocal and global keywords can be used to bring a variable into scope. But this is necessary because you cannot declare a Python variable: whenever you assign to a variable, it is created in the innermost scope.

In less common languages, operators to access outer scopes do exist. One example is the Handlebars template language, which uses a filepath-like syntax: {{foo}} accesses the variable in the current scope, {{../foo}} looks at the parent scope, {{../../foo}} at the second-parent scope and so on. In Perl6, the OUTER pseudo-namespace lets you look outwards, as in OUTER::Symbol, OUTER::OUTER::Symbol and so on. It also has lots of other pseudo-namespaces, such as OUTERS:: which means “any outer scope, you don't have to count”.

Is this a good idea? Possibly. Although not strictly necessary or a good idea, accessing outer scopes is probably convenient. Especially if you don't want to explain the concept of “lexical scope” to your users. I think, but have no evidence, that Perl6's pseudo-namespace solution is the most elegant approach here for block scopes. Using an explicit name like outer is probably better than using a scope-walking symbol like @ unless you are already using Perl- or Ruby-like Sigils in your language.

However, these explicit outer-scopes do have an universal problem: you generally have to carefully count the required levels, which makes the code harder to refactor and copy-paste: if put the code into a different context, the scope level counts will probably be off.

For object-oriented code, a classic keyword like this, self, my or currentObject might be more sensible, in which symbol access happens just like with any other object.

1
  • Thanks for the examples in other languages! The brittleness when copy/pasting code around is my primary concern with this idea.
    – enzi
    Jul 19, 2017 at 9:54
1

Some problems

  1. As a matter of principle-- control of scope is a very important concern for programmers. Making it easier to play fast and loose with scope is likely to introduce more complexity, not less, because poor control of scope makes dependency tracing more difficult.

  2. Indicating scope via a relative number (the number of @ symbols) is prone to error since scope leveling may change if, say, you move the code, or if you remove a layer. I am not sure how your language will be structured, but do if and while blocks have their own scope? Can you have nested classes? Any sort of nesting would result in utter confusion, especially if you were to move code from inside a block to the outside of that block. You'd have to go through and change every @@ to a @.

  3. You can end up with two symbols representing the same variable, e.g. @foo and @@foo might refer to the same memory location if they are used in different layers of the code. That to me is utterly confusing.

  4. If your programmers don't understand what global scope is, there isn't a language construct in the universe that will help them out of that.

1

Perl can do something similar to what you suggest but you have to set it up to do so. This technique will work with both lexical and dynamic variables.

Using lexical variables:

my $foo = 'begin';
my $foo_ref = \$foo;
print "foo=$foo foo_ref=$$foo_ref # both the same at the beginning\n";

{
    my $foo = 'inner';
    print "foo=$foo foo_ref=$$foo_ref # foo changes but foo_ref remains the same\n";

    # magic
    $$foo_ref = 'magic';
    print "foo=$foo foo_ref=$$foo_ref # foo_ref changes but foo is still inner\n";
}
print "foo=$foo foo_ref=$$foo_ref # outer foo changed when foo_ref did\n";

Using dynamic variables:

our $foo = 'begin';
our $foo_ref = \$foo;
print "foo=$foo foo_ref=$$foo_ref # both the same at the beginning\n";

{
    local $foo = 'inner';
    print "foo=$foo foo_ref=$$foo_ref # foo changes but foo_ref remains the same\n";

    # magic
    $$foo_ref = 'magic';
    print "foo=$foo foo_ref=$$foo_ref # foo_ref changes but foo is still inner\n";
}
print "foo=$foo foo_ref=$$foo_ref # outer foo changed when foo_ref did\n";

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.