whitespace matters?
I'm sure there'll be plenty of people who hate the very idea of exploring this. But what if we took a page out of Python and made whitespace matter?
There will numerous problems with this but let's at least give it a try.
a add-with: b | a: int, b: int -> r: int
r <- a + b
The absolute basics. We divide the method definition between signature and types but the body is indented. When the indentation drops back to the previous level we have finished the method - unless it's a completely empty line in which case we keep going.
It's very minimalistic. It's easy to consume and to look at. The [] brackets aren't making a big blocky mess on the screen. But let's get fancier and try a block:
tall-people := people select | each: person -> r: bool
r <- each height > 6 foot
That's... kind of interesting. We still needed a place to put the type information and on the line about to deploy the block seemed as good a place as any. It does require | to be very special syntax though. We'd never be able to use it to represent 'or'. It's debatable if | is even a worthy replacement for the word 'or'.
There is a limitation here that you can only pass in one block at a time. You could assign a block to a variable and pass that as an argument; but it becomes a clunkier API when we do that.
In a previous example I wrote the following:
jane? then: [ jane | stdout print: jane ],
else: [ stdout print: 'Jane missing' ]
This is not possible with the Python-esque approach to blocks.
We could reserve this indenting logic for the top level, for methods only, but inconsistencies aren't very fun. Still may be there's something we can adapt from this.
Currently we write blocks inside [] to make it look block-like. That's lovely. Methods just aren't blocks. So let's take a step sideways. What if the top-level methods don't need to be wrapped inside square brackets? only the source code part of it.
// a method:
a add-with: b | a: int, b: int -> r: int [ r <- a + b ]
1 add-with: 2
// a block:
a-block := [ a: int, b: int -> r: int | r <- a + b ]
a-block evaluate: (1, 2)
// an inline method:
a-named-block := [ a add-with: b | a: int, b: int -> r: int | r <- a + b ]
a-named-block evaluate: (1, 2)
1 add-with: 2
Blocks can be aliased with a signature and it lives in the same scope of where it would otherwise have been assigned. It can still be assigned to a variable too in case you want to pass it as a parameter somewhere. Assigning it to a variable, if it's got a signature, is not required.
do-database-stuff: db | db: database-sesion -> ø [
[ a-person print-to: output | a-person, output |
a-person then: [ resolved-person | output print: resolved-person ]
else: [ output print: 'person not found' ] ]
jane := db select: "select * from people where name = 'Jane'"
jane print-to: io stdout ]
Some type inferencing expected here. Just to make the example look as simple as possible. It's contrived, of course, but it combines a top level method with a named inline-method and blocks.
You could also write methods in a 'long' form if you wanted, but the | needs to be on the same line as the signature otherwise the compiler will think you're actually doing a message send at compile time:
do-database-stuff: db |
db: database-session
-> ø
[
...
]
We're back to whitespace not mattering, which is what I personally prefer. It would be nice to allow the | to be on the next line too. The parser could be smart enough to recognise the next line starts with a | and treat it as a single line instead:
do-database-stuff: db
| db: database-session -> ø
[
...
]
This is nice enough I'm going to sit on the idea for a bit and perhaps adopt it as the new approach.