Open creationix opened 8 years ago
Continuing on this thought, suppose we had lexical closure like JS has. This would mean that our objects would inherit from their parent scope.
name = "Bob"
tim = {
name = "Tim"
age = 33
isProgrammer = true
}
bob = {}
print(tim.name, bob.name) // Tim Bob
We can inherit functions as well for shared methods.
function greet() {
print("Hello from " + this.name)
}
tim.greet() // Hello from Tim
Now one of the cool features of ruby is the ability to enter a scope after it's been created. Let's explore that here as well by stealing the with
keyword to have new meaning.
// These braces don't create a new scope, but reuse an existing one.
with tim {
print(name) // Tim
hasFun = true
}
print(tim.hasFun) // true
Let's also add first-class blocks. These will look like scope literals, but have an escape character at the front that means to keep them as code and to not execute yet.
myblock = #{
print("run me!")
}
This can make for some really pretty DSLs.
require('tap')(#{
test("this should work", #{
assert(1 == 1, "it works")
})
})
Let's add a new with
infix operator that calls the block in the left hand side with the value in the right hand side as the scope.
// Define a block (like a function, but has no formal parameters)
greet = #{
print("Hello from " + name)
}
// Create a scope object
tim = {
name = "Tim"
age = 33
}
// Call the block passing in `tim` as the scope. using new `with` operator.
greet with tim
At this point, you're probably starting to have panic attacks thinking of all the ways these proposed changes can go wrong. Everything is public and mutable, everything is inherited, local scope changes at runtime. But let's continue on reducing features till something elegant comes out. We can always bolt on safety and best-practices at the end right?
We now have two systems for defining and calling functions. What if we drop the old JS syntax and semantics in favor of the new. That means no more function (...) { ...}
or expr(...)
. We'll assume a global print
block instead that accepts a message
variable.
print with { message = "Hello World" }
Look at that, we have named arguments! Named is strictly safer and more explicit than positional arguments right? Maybe this rabbit hole isn't so crazy after all.
Now many functions have exactly one input and it would be a pain to name that one input all the time. How about we also allow non-objects to be used with with
. We'll create a new pseudo variable self
that refers to the current scope. Our print
will use this instead.
print with "Hello World"
While we're changing things, how about pulling in some proposed ES7 features. I used to be a linux admin and love the pipe operator in bash. Let's add it! scope | block
is the same as block with scope
except the order is reversed.
"Hello World" | print
We can chain these as well. (note we've added implicit return)
doubleSay = #{ self + ', ' + self }
// head returns the first character in a string, tail returns the rest
capitalize = #{ (self | head | toUpperCase) + (self | tail) }
exclaim = #{ self + '!' }
"hello" | doubleSay | capitalize | exclaim | print // "Hello, hello!"
Now we hit our first design conflict. The implicit return added in the last example breaks the first examples where we had an ideal configuration language.
{
name = "Tim"
}
This now evaluates to the string "Tim"
since the last expression in the block was the assignment. We could say that assignments return the scope they were assigned to. This works well and only breaks the case where people use the (often considered bad) practice of using an assignment in the middle of an expression.
But if we want to keep the C semantics of assignment evaluating to the RHS, we just need to add a self
at the end to return the scope.
{
name = "Tim"
age = 33
self
}
So the tradeoff is to make config style declarative code less pleasant or to change well-known (but considered harmful) C semantics around assignment. I vote for changing the assignment semantics!
Remember:
A language that doesn't affect the way you think about programming, is not worth knowing. -- Alan Perlis
Also my primary target audience is people learning programming for the first time, so they won't be infected by learned habits from other languages.
So back to making this language safe to use. I'm not going to go so far as to add compile-time type checks. This is a highly dynamic scripting language after all. But we should add some run-time tools to help developers quickly and safely assert certain conditions. Making this part of the language opens up opportunities for future engines to optimize as well ad provides a better opportunity for good syntax.
The first problem is scope objects inherit everything. This fine as long as you don't use the stuff you're not supposed to use and aren't iterating on the objects, but some visibility control would be nice.
If we stick to public by default, then the config-file use case stays the same, but if we use private by default there are no surprises about what is exported. There is also the global or local by default question, but I think we'll stick to local-by-default. Otherwise nothing makes sense.
name = "Bob"
tim = { name = "Tim" }
bob = {}
bob.tim.name | print
Yes, this is possible. I doubt we wanted bob
to have a tim
property. We just wanted it to inherit name
as a default.
While we're listing problems, there is another to ponder. Since we dropped normal lambda style function calls and instead use with
or the pipe operator to call blocks, there is no way to mix closure variables with passed in variables. The with
scope will replace the inherited local variables.
Should our scopes have multiple inheritance? Should we be able to declare certain variables that we wish to preserve from the outer scopes?
greet = {
// A private variable
greeting = "Hello "
// return a block
#{ greeting + name | print }
}
{ name = "Tim" } | greet
This example will not work since greeting
will be undefined. Though we did find a clever way to create private variables, if only the closure worked.
One solution to the broken closures is to go back to a more JS-like system where self
is the only injected variable (from with
or pipe) and all local variables are lexical scope only. We'll even rename it to this
to make the JS highlighter happy.
greet = {
greeting = "Hello "
#{ greeting + this.name | print }
}
{ name = "Tim" } | greet
This works, but we've reduced our blocks to JS functions with zero arguments and using this
for all external inputs. This won't end well, there must be another way.
Actually on second thought, this isn't that bad. Will consider more...
With the self
renamed to this
and lexical scope preserved, the pipe examples look nicer:
doubleSay = #{ this + ', ' + this }
capitalize = #{ (this | head | toUpperCase) + (this | tail) }
exclaim = #{ this + '!' }
"hello" | doubleSay | capitalize | exclaim | print // "Hello, hello!"
Hmm, the lack of positional arguments makes the tap example quite interesting.
'tap' | require | #{
"this should work" | this.test | #{
1 == 1 | this.assert with "it works"
}
}
I'm not sure about mixing block 'with' value
with value '|' block
.
So let's summarize the changes to the language so far.
First we made a nice config-file language with near ideal syntax for defining nested properties (JSON-like data)
let
and var
, assume all new assignments are local.a = { ... }
that execute right away.Then we completely redid how functions and function calls worked.
#{ ... }
that are like functions with no positional arguments, only one input that maps to this
.newThis | block
.block 'with' newThis
Maybe we should have (>
and <
) instead of (|
and with
) since their semantics are exactly symmetrical?
So with >
and <
we could write the assert like:
1 == 1 > this.assert < "it works"
// or
"it works" > this.assert < 1 == 1
depending on how we want to define order of operations.
I think a scripting language with first-class scope objects would be neat. This would combine the idea of scope in closures with objects into a single construct. For example, the following in JS:
Could be rewritten if JS has first-class scopes that evaluated to themselves.
Now combine this with some language tweaks (remove semicolons, assume local for new vars):
There is now zero difference between creating local variables and creating properties on objects. This also makes for a very nice configuration language.