Allow empty argument list methods.

- Compile them as calls and definitions.
- Use them for call(), clear(), run(), try(), and yield().
- Update the docs.
This commit is contained in:
Bob Nystrom
2015-02-26 23:08:36 -08:00
parent 1aaa8cff52
commit 96ceaa528b
94 changed files with 291 additions and 224 deletions
+43 -5
View File
@@ -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
+16 -15
View File
@@ -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**
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -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.
+4 -3
View File
@@ -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
+14 -2
View File
@@ -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
View File
@@ -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.)
+6 -6
View File
@@ -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".
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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) // []
+2 -2
View File
@@ -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