Open jamiebuilds opened 5 years ago
- If Ghost didn't have implicit returns, you'd effectively be getting rid of expression statements
let value = 42 return -num
They’d still be useful, for cases like your examples in “The Problem With The Solution.”
How about adding a new syntax for functions?
let fn = fn (numbers: Iter<Number>) async {
Iter.reduce(numbers, 0, fn (total, number) => total + number)
# or (if you prefer)
Iter.reduce(numbers, 0, fn (total, number) -> total + number)
}
Like in JS, this would allow exactly one expression, which would be implicitly returned.
let deploy = fn (tag, message) async {
let _ = await updateYaml(tag: tag)
let _ = await gitAddAll()
let _ = await gitCommit(message: message)
let _ = await gitTagAdd(tag: tag)
let _ = await gitPush(remote: 'origin', branch: 'master')
let _ = await triggerCi(workflow: 'deploy', tag: tag, message: message)
}
This actually reminds me quite a bit of what you do in ocaml when you need a side effect executed but don't care about the value:
let add_with_side_effect = fun x y ->
let () = do_a_side_effect () in
x + y
;;
While they do have semicolons, I think they specifically do this for the type system (in ocaml ()
is considered their "null" value), because then it can validate that do_a_side_effect
has a return type of null. I noticed you didn't mention type considerations in the OP, but if you take the implementation of _
even further than described, assigning to _
could actually provide real value! I thought I'd pitch in yet another positive of this proposed solution; it's a pretty small thing but it makes it feel really natural in ocaml and encourages the programmer to not have methods with side effects that also return values (a general anti-pattern as described by Robert Martin's Clean Code)
it's a pretty small thing but it makes it feel really natural in ocaml and encourages the programmer to not have methods with side effects that also return values (a general anti-pattern as described by Robert Martin's Clean Code)
I don't know that I agree that returning a value from an side-effectful function is an anti-pattern. But interesting
There's a couple aspects of the language that when considered together create a design problem:
The Features
Implicit Returns - Automatically returning the "last" value in a function
Expression Statements - Allow expressions in "statement" positions
Whitespace Insignificance - Whitespace (spaces, tabs, & newlines) should be allowed in between "parts" of language constructs
Unary/Binary Operators - There are generally two types of operators "binary" operators and "unary" operators"
No Semicolons - Lots of languages have separators between statements in order to make sure you know exactly when one ends, generally that is a semicolon:
The Problem
Combining all these things you end up with:
let value = 42 - num
let val = 42
...-num
It's ambiguous, and it needs to be resolved.
It's not an option to just look at the whitespace and see they are two separate statements. That would greatly complicate the language grammar.
The Possibilities
In order to solve this, Ghost has two options:
If we were to get rid of any one of the above language features, suddenly this wouldn't be a problem anymore.
The Solution
Out of all those options, the one I like the best is getting rid of expression statements:
If naming variables is too annoying, I would say to use the Junk Binding (the value should still be returned, it just wont create a binding):
The Problem With The Solution
Through most of this I've been focusing on expression statements in "return" positions. But in order to be a good language for writing scripts, you want to write lots of expression statements that have effects.
Using the above solution, it would look something like this:
Personally, I actually quite like this, it makes it very obvious that you're doing side-effectful shit. But others might find it annoying.
I might even take it further by copying something from Rust, and saying:
_
, you must reference it afterwards._
, it must not be referenced afterwards.This would allow you to name everything without creating tons of bindings, and while making it clear you're running functions for their side-effects instead of their return value.
The Other Problem With The Solution
With the simplest version of this proposal, it's adding quite a bit of typing:
For convenience, I'm tempted to say that you can have an expression statement if there are no other statements above it (including other expression statements).
So you could still get:
But as soon as you add statements, you need to use
let
I'm 50/50 on this. It's very convenient (saving a minimum of 5 keystrokes at a time adds up fast), but its adding a syntax to the language that you can't "cut and paste" and put anywhere.
It also means that if you add a statement to a function, you might need to rewrite a function below it in order for it to be syntactically valid.
Like when I wanted to add that
let numbers = doubleAll(numbers)
to the above function, I also needed to addlet total =
(or at leastlet _ =
). It's not terrible, but it's something I try to avoid in Ghost, because it's annoying, and it's the sort of thing that will confuse beginners.The Alternative Solutions
If expression statements had a syntax of their own, it could also solve the problem. The most obvious answer would a prefix. Sadly, my favorite prefixes would be
!
or~
, but I can't use those or any of these:Funny enough.... semicolons could work:
There's lots of other things you could do, but it should be much easier to write than
let _ =
or it's not really worth it. Personally I preferlet