I'm designing a programming language for beginners/kids and am trying to keep the syntax for basic usage as simple as possible, while still providing useful features for more advanced users such as first-class functions and lambda expressions.
In particular I'm keen that it should be possible to call functions without parentheses:
print "hello world"
This turns out to be quite complicated when the language is dynamically typed and supports first-class functions. Given the following:
-- Function definition
-- This is immutable (i.e. any other assignment of foo in this scope
-- is an error)
def foo(a, b, c)
...
end
-- This is a lambda
bar := .[ a,b,c| ... ]
When making a call with arguments the same forms are valid for both static functions and lambdas:
foo 1, 2, 3 -- call, without parens
foo(1, 2, 3) -- call, with parens
bar 1, 2, 3 -- call, without parens
bar(1, 2, 3) -- call, with parens
The situation is more complicated with no-args because both static functions and lambdas are first-class (i.e. you can reference them without calling).
For static functions, call always wins. Rationale: using functions as first-class values is a relatively advanced feature for beginners so just use call semantics. Bonus: static analysis means this can be identified at compile time so we can just spit out a CALL opcode.
foo -- call
foo() -- call
For lambdas:
bar -- reference
bar() -- call
Rationale: when dealing with first-class functions, it's common to pass them around, assign etc.
Now, for nested calls, there's a bit of ambiguity. Generally nested calls require parens, so:
foo a(1,2), b(2,3)
But what using no-arg functions:
def baz()
...
end
-- are these calls?
foo baz, baz
-- or should this be required?
foo baz(), baz()
2 rules are in conflict here so not sure what to do... for now I think making them calls is the best thing to do.
The only elephant in the room is the situation where you want to take a first-class reference to a statically defined function. e.g.:
def sort(lambda, ary)
...
end
def reverse_compare(l, r)
r - l
end
-- because reverse_compare is a static def'd function this
-- will actually call it, rather than pass it in...
sort(reverse_compare, [1, 2, 3])
To counter this, we introduce the no-call unary operator (&
) which
forces a reference to the function:
sort(&reverse_compare, [1, 2, 3])
This is a bit messy but I feel it's justified because it's unusual to use static functions as callbacks - much more usual to use lambdas.
Thoughts?