feat: allow empty argument list methods in calls, definitions, and docs
- Compile empty `()` as a valid method signature distinct from no parentheses - Update `call()`, `clear()`, `run()`, `try()`, and `yield()` to use empty argument lists - Revise documentation across multiple files to reflect new signature semantics
This commit is contained in:
@@ -39,12 +39,12 @@ add a parenthesized parameter list after the method's name:
|
||||
}
|
||||
}
|
||||
|
||||
### Arity
|
||||
### Signature
|
||||
|
||||
Unlike most other dynamically-typed languages, in Wren you can have multiple
|
||||
methods in a class with the same name, as long as they take a different number
|
||||
of parameters. In technical terms, you can *overload by arity*. So this class
|
||||
is fine:
|
||||
methods in a class with the same name, as long as they have a different
|
||||
parameter *signature*. In technical terms, you can *overload by arity*. So this
|
||||
class is fine:
|
||||
|
||||
:::dart
|
||||
class Unicorn {
|
||||
@@ -77,6 +77,44 @@ sets of arguments. In other languages, you'd define a single method for the
|
||||
operation and have to check for "undefined" or missing arguments. Wren just
|
||||
treats them as different methods that you can implement separately.
|
||||
|
||||
Signature is a bit more than just arity. It also lets you distinguish between a
|
||||
method that takes an *empty* argument list (`()`) and no argument list at all:
|
||||
|
||||
:::dart
|
||||
class Confusing {
|
||||
method { "no argument list" }
|
||||
method() { "empty argument list" }
|
||||
}
|
||||
|
||||
var confusing = new Confusing
|
||||
confusing.method // "no argument list".
|
||||
confusing.method() // "empty argument list".
|
||||
|
||||
Like the example says, having two methods that differ just by an empty set of
|
||||
parentheses is pretty confusing. That's not what this is for. It's mainly so
|
||||
you can define methods that don't take any arguments but look "method-like".
|
||||
|
||||
Methods that don't need arguments and don't modify the underlying object tend
|
||||
to omit the parentheses. These are "getters" and usually access a property of
|
||||
an object, or produce a new object from it:
|
||||
|
||||
:::dart
|
||||
"string".count
|
||||
(1..3).min
|
||||
0.123.sin
|
||||
|
||||
Other methods do change the object, and it's helpful to draw attention to that:
|
||||
|
||||
:::dart
|
||||
list.clear()
|
||||
|
||||
Since the parentheses are part of the method's signature, the callsite and
|
||||
definition have to agree. These don't work:
|
||||
|
||||
:::
|
||||
"string".count()
|
||||
list.clear
|
||||
|
||||
### Operators
|
||||
|
||||
Operators are just special syntax for a method call on the left hand operand
|
||||
@@ -153,7 +191,7 @@ Values are passed to the constructor like so:
|
||||
:::dart
|
||||
new Unicorn("Flicker", "purple")
|
||||
|
||||
Like other methods, you can overload constructors by [arity](#arity).
|
||||
Like other methods, you can overload constructors by [arity](#signature).
|
||||
|
||||
## Fields
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ fiber is run. Does not immediately start running the fiber.
|
||||
IO.print("I won't get printed")
|
||||
}
|
||||
|
||||
### Fiber.**yield**
|
||||
### Fiber.**yield**()
|
||||
|
||||
Pauses the current fiber and transfers control to the parent fiber. "Parent"
|
||||
here means the last fiber that was started using `call` and not `run`.
|
||||
@@ -21,37 +21,38 @@ here means the last fiber that was started using `call` and not `run`.
|
||||
:::dart
|
||||
var fiber = new Fiber {
|
||||
IO.print("Before yield")
|
||||
Fiber.yield
|
||||
Fiber.yield()
|
||||
IO.print("After yield")
|
||||
}
|
||||
|
||||
fiber.call // "Before yield"
|
||||
fiber.call() // "Before yield"
|
||||
IO.print("After call") // "After call"
|
||||
fiber.call // "After yield"
|
||||
fiber.call() // "After yield"
|
||||
|
||||
When resumed, the parent fiber's `call` method returns `null`.
|
||||
When resumed, the parent fiber's `call()` method returns `null`.
|
||||
|
||||
If a yielded fiber is resumed by calling `call()` or `run()` with an argument,
|
||||
`yield` returns that value.
|
||||
`yield()` returns that value.
|
||||
|
||||
:::dart
|
||||
var fiber = new Fiber {
|
||||
IO.print(Fiber.yield) // "value"
|
||||
IO.print(Fiber.yield()) // "value"
|
||||
}
|
||||
|
||||
fiber.call // Run until the first yield.
|
||||
fiber.call() // Run until the first yield.
|
||||
fiber.call("value") // Resume the fiber.
|
||||
|
||||
If it was resumed by calling `call` or `run` with no argument, returns `null`.
|
||||
If it was resumed by calling `call()` or `run()` with no argument, it returns
|
||||
`null`.
|
||||
|
||||
It is a runtime error to call this when there is no parent fiber to return to.
|
||||
|
||||
:::dart
|
||||
Fiber.yield // ERROR
|
||||
Fiber.yield() // ERROR
|
||||
|
||||
new Fiber {
|
||||
Fiber.yield // ERROR
|
||||
}.run
|
||||
Fiber.yield() // ERROR
|
||||
}.run()
|
||||
|
||||
### Fiber.**yield**(value)
|
||||
|
||||
@@ -63,9 +64,9 @@ Similar to `Fiber.yield` but provides a value to return to the parent fiber's
|
||||
Fiber.yield("value")
|
||||
}
|
||||
|
||||
IO.print(fiber.call) // "value"
|
||||
IO.print(fiber.call()) // "value"
|
||||
|
||||
### **call**
|
||||
### **call**()
|
||||
|
||||
**TODO**
|
||||
|
||||
@@ -78,7 +79,7 @@ Similar to `Fiber.yield` but provides a value to return to the parent fiber's
|
||||
Whether the fiber's main function has completed and the fiber can no longer be
|
||||
run. This returns `false` if the fiber is currently running or has yielded.
|
||||
|
||||
### **run**
|
||||
### **run**()
|
||||
|
||||
**TODO**
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ An indexable contiguous collection of elements. More details [here](../lists.htm
|
||||
|
||||
Appends `item` to the end of the list.
|
||||
|
||||
### **clear**
|
||||
### **clear**()
|
||||
|
||||
Removes all items from the list.
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
An associative collection that maps keys to values. More details [here](../maps.html).
|
||||
|
||||
### **clear**
|
||||
### **clear**()
|
||||
|
||||
Removes all entries from the map.
|
||||
|
||||
|
||||
@@ -107,7 +107,7 @@ For example, if you run this program:
|
||||
123.badMethod
|
||||
}
|
||||
|
||||
var error = fiber.try
|
||||
var error = fiber.try()
|
||||
IO.print("Caught error: ", error)
|
||||
|
||||
It prints:
|
||||
@@ -156,8 +156,9 @@ before its parsed, but validating that a string is a number is pretty much the
|
||||
same thing as parsing it.
|
||||
|
||||
For cases like this where failure can occur and the program *will* want to
|
||||
handle it, fibers and `try` are too coarse-grained to work with. Instead, these
|
||||
operations will indicate failure by *returning* some sort of error indication.
|
||||
handle it, fibers and `try()` are too coarse-grained to work with. Instead,
|
||||
these operations will indicate failure by *returning* some sort of error
|
||||
indication.
|
||||
|
||||
For example, a method for parsing a number could return a number on success and
|
||||
`null` to indicate parsing failed. Since Wren is dynamically typed, it's easy
|
||||
|
||||
@@ -35,13 +35,25 @@ look like so:
|
||||
|
||||
You have a *receiver* expression followed by a `.`, then a name and an argument
|
||||
list in parentheses. Arguments are separated by commas. Methods that do not
|
||||
take any arguments omit the `()`:
|
||||
take any arguments can omit the `()`:
|
||||
|
||||
:::dart
|
||||
text.length
|
||||
|
||||
These are special "getters" or "accessors" in other languages. In Wren, they're
|
||||
just method calls.
|
||||
just method calls. You can also define methods that take an empty argument list:
|
||||
|
||||
:::dart
|
||||
list.clear()
|
||||
|
||||
An empty argument list is *not* the same as omitting the parentheses
|
||||
completely. Wren lets you overload methods by their call signature. This mainly
|
||||
means [*arity*](classes.html#signature)—number of parameters—but
|
||||
also distinguishes between "empty parentheses" and "no parentheses at all".
|
||||
|
||||
You can have a class that defines both `foo` and `foo()` as separate methods.
|
||||
Think of it like the parentheses and commas between arguments are part of the
|
||||
method's *name*.
|
||||
|
||||
If the last (or only) argument to a method call is a
|
||||
[function](functions.html), it may be passed as a [block
|
||||
|
||||
+18
-18
@@ -38,10 +38,10 @@ code sitting there waiting to be activated, a bit like a
|
||||
## Invoking fibers
|
||||
|
||||
Once you've created a fiber, you can invoke it (which suspends the current
|
||||
fiber) by calling its `call` method:
|
||||
fiber) by calling its `call()` method:
|
||||
|
||||
:::dart
|
||||
fiber.call
|
||||
fiber.call()
|
||||
|
||||
The called fiber will execute its code until it reaches the end of its body or
|
||||
until it passes control to another fiber. If it reaches the end of its body,
|
||||
@@ -50,7 +50,7 @@ it's considered *done*:
|
||||
:::dart
|
||||
var fiber = new Fiber { IO.print("Hi") }
|
||||
fiber.isDone // false
|
||||
fiber.call
|
||||
fiber.call()
|
||||
fiber.isDone // true
|
||||
|
||||
When it finishes, it automatically resumes the fiber that called it. It's a
|
||||
@@ -67,19 +67,19 @@ Things get interesting when a fiber *yields*. A yielded fiber passes control
|
||||
*back* to the fiber that ran it, but *remembers where it is*. The next time the
|
||||
fiber is called, it picks up right where it left off and keeps going.
|
||||
|
||||
You can make a fiber yield by calling the static `yield` method on `Fiber`:
|
||||
You can make a fiber yield by calling the static `yield()` method on `Fiber`:
|
||||
|
||||
:::dart
|
||||
var fiber = new Fiber {
|
||||
IO.print("fiber 1")
|
||||
Fiber.yield
|
||||
Fiber.yield()
|
||||
IO.print("fiber 2")
|
||||
}
|
||||
|
||||
IO.print("main 1")
|
||||
fiber.call
|
||||
fiber.call()
|
||||
IO.print("main 2")
|
||||
fiber.call
|
||||
fiber.call()
|
||||
IO.print("main 3")
|
||||
|
||||
This program prints:
|
||||
@@ -100,11 +100,11 @@ the mercy of a thread scheduler playing Russian roulette with your code.
|
||||
Calling and yielding fibers is used for passing control, but it can also pass
|
||||
*data*. When you call a fiber, you can optionally pass a value to it. If the
|
||||
fiber has yielded and is waiting to resume, the value becomes the return value
|
||||
of the `yield` call:
|
||||
of the `yield()` call:
|
||||
|
||||
:::dart
|
||||
var fiber = new Fiber {
|
||||
var result = Fiber.yield
|
||||
var result = Fiber.yield()
|
||||
IO.print(result)
|
||||
}
|
||||
|
||||
@@ -112,11 +112,11 @@ of the `yield` call:
|
||||
fiber.call("sent")
|
||||
|
||||
This prints "sent". Note that the first value sent to the fiber through call is
|
||||
ignored. That's because the fiber isn't waiting on a `yield` call, so there's
|
||||
ignored. That's because the fiber isn't waiting on a `yield()` call, so there's
|
||||
no where for the sent value to go.
|
||||
|
||||
Fibers can also pass values *back* when they yield. If you pass an argument to
|
||||
`yield`, that will become the return value of the `call` that was used to
|
||||
`yield()`, that will become the return value of the `call` that was used to
|
||||
invoke the fiber:
|
||||
|
||||
:::dart
|
||||
@@ -124,7 +124,7 @@ invoke the fiber:
|
||||
Fiber.yield("sent")
|
||||
}
|
||||
|
||||
IO.print(fiber.call)
|
||||
IO.print(fiber.call())
|
||||
|
||||
This also prints "sent".
|
||||
|
||||
@@ -146,15 +146,15 @@ example:
|
||||
}
|
||||
}
|
||||
|
||||
Here, we're calling `yield` from within a [function](functions.html) being
|
||||
passed to the `map` method. This works fine in Wren because that inner `yield`
|
||||
call will suspend the call to `map` and the function passed to it as a
|
||||
callback.
|
||||
Here, we're calling `yield()` from within a [function](functions.html) being
|
||||
passed to the `map()` method. This works fine in Wren because that inner
|
||||
`yield()` call will suspend the call to `map()` and the function passed to it
|
||||
as a callback.
|
||||
|
||||
## Transferring control
|
||||
|
||||
Fibers have one more trick up their sleeves. When you execute a fiber using
|
||||
`call`, the fiber tracks which fiber it will return to when it yields. This
|
||||
`call()`, the fiber tracks which fiber it will return to when it yields. This
|
||||
lets you build up a chain of fiber calls that will eventually unwind back to
|
||||
the main fiber when all of the called ones yield or finish.
|
||||
|
||||
@@ -168,7 +168,7 @@ entirely. (This is analogous to [tail call
|
||||
elimination](http://en.wikipedia.org/wiki/Tail_call) for regular function
|
||||
calls.)
|
||||
|
||||
To enable this, fibers also have a `run` method. This begins executing that
|
||||
To enable this, fibers also have a `run()` method. This begins executing that
|
||||
fiber, and "forgets" the previous one. If the running fiber yields or ends, it
|
||||
will transfer control back to the last *called* one. (If there are no called
|
||||
fibers, it will end execution.)
|
||||
|
||||
@@ -76,17 +76,17 @@ so by calling a method on it:
|
||||
:::dart
|
||||
class Blondie {
|
||||
callMe(fn) {
|
||||
fn.call
|
||||
fn.call()
|
||||
}
|
||||
}
|
||||
|
||||
Functions expose a `call` method that executes the body of the function. This
|
||||
Functions expose a `call()` method that executes the body of the function. This
|
||||
method is dynamically-dispatched like any other, so you can define your own
|
||||
"function-like" classes and pass them to methods that expect "real" functions.
|
||||
|
||||
:::dart
|
||||
class FakeFn {
|
||||
call {
|
||||
call() {
|
||||
IO.print("I'm feeling functional!")
|
||||
}
|
||||
}
|
||||
@@ -157,6 +157,6 @@ to`i`:
|
||||
|
||||
:::dart
|
||||
var counter = Counter.create
|
||||
IO.print(counter.call) // Prints "1".
|
||||
IO.print(counter.call) // Prints "2".
|
||||
IO.print(counter.call) // Prints "3".
|
||||
IO.print(counter.call()) // Prints "1".
|
||||
IO.print(counter.call()) // Prints "2".
|
||||
IO.print(counter.call()) // Prints "3".
|
||||
|
||||
@@ -18,7 +18,7 @@ a familiar, modern [syntax][].
|
||||
["small", "clean", "fast"].map {|word| Fiber.yield(word) }
|
||||
}
|
||||
|
||||
while (!adjectives.isDone) IO.print(adjectives.call)
|
||||
while (!adjectives.isDone) IO.print(adjectives.call())
|
||||
|
||||
* **Wren is small.** The codebase is about [5,000 lines][src]. You can
|
||||
skim the whole thing in an afternoon. It's *small*, but not *dense*. It
|
||||
|
||||
@@ -106,5 +106,5 @@ The `removeAt` method returns the removed item:
|
||||
If you want to remove everything from the list, you can clear it:
|
||||
|
||||
:::dart
|
||||
hirsute.clear
|
||||
hirsute.clear()
|
||||
IO.print(hirsute) // []
|
||||
|
||||
@@ -81,10 +81,10 @@ If the key was found, this returns the value that was associated with it:
|
||||
If the key wasn't in the map to begin with, `remove()` just returns `null`.
|
||||
|
||||
If you want to remove *everything* from the map, just like with [lists][], you
|
||||
can just call `clear`:
|
||||
can just call `clear()`:
|
||||
|
||||
:::dart
|
||||
capitals.clear
|
||||
capitals.clear()
|
||||
IO.print(capitals.count) // "0".
|
||||
|
||||
[lists]: lists.html
|
||||
|
||||
Reference in New Issue
Block a user