In Ferret, all named functions and methods are implemented transparently as events. This means that for every function or method name, any number of responders may exist, each of which can accept its own arguments and spit out its own return values.
The result? Ferret libraries and programs are like super dynamimcally extensible through modules and other whatnot.
Technically, a Function (with a capital F) is the same as in many other languages - a group of statements which accepts zero or more values as input and returns zero or more values as output.
An Event, on the other hand, is a collection of Functions hidden behind a single name.
However, because all named functions (func NAME
) and methods
(method NAME
, op OP
, hook NAME
, etc.) are implemented as events, the
word function is usually used to describe events. The only way you can
create a true Function is with an inline or anonymous function
(func {}
without a name), and the docs will hopefully explicitly denote
such as anonymous functions.
For cases where anything callable is an acceptable value, the Code type represents something callable. Events as well as anonymous functions conform to it.
Take a look at this example.
func checkID {
want $name: Str = "sir"
need $age: Num
# not old enough
if $age < 21 {
underage -> true : Bool
say("Sorry, $name. Come back later.")
->
}
say("Welcome, $name.")
}
checkID("John Doe", 43) # "Welcome, John Doe."
checkID(age: 12) # "Sorry, sir. Come back later."
checkID("Jane Roe") # no output (unsatisfied 'need')
The first thing you likely noticed is these weird wants and needs.
- A
want
is an argument the function is hopeful for but does not require. When it's missing, the code still executes, and in this case, the variable falls back to a specified default value. - A
need
is an argument the function simply cannot function(!) without. When the value is omitted or does not conform to the specified type, the call fails with an invalid argument error.
Argument variable names are not arbitrary, nor is their order. Both are used in determining the function's signature. These names can be used when passing named arguments in any order, while the signature is used in determining the argument order when names are omitted from a call.
The simplest form of an argument declaration is want
or need
followed by
a variable name. Optionally, as shown in the above example, you can specify
accepted types and fallback values.
need $x
need $x: Num
want $verbose: Bool = true
By the way, you can use the comma to split minimize this a little:
need $name: Str, $age: Num
want $x = 0, $y = 0
Functions can return any number of values. Yeah.
That might sound confusing at first, but um, it's kind of a lie too, because really functions just return a single normal-ish object with normally-ish named properties.
Like arguments, returns are named in the function's signature.
Each return value is associated with a name, and these become the properties of the return object yielded by the function. Execution of the function continues beyond each return pair.
Here's what a return pair statement looks like.
name -> $value
For simple functions, one value may be returned in the usual way. This, of course, stops the execution of the function at that point. The function then yields the returned value instead of the return object.
-> $value
A return with no value stops the execution of the function, yielding the
return object as-is (NOT a void value). This is demonstrated
in the checkID()
example.
->
Functions with no return value do not need an explicit return statement.
A return object with no values associated is false in boolean context, which allows functions returning nothing to yield false as expected.
This is a line-by-line breakdown of the same checkID()
example from above.
# func checkID is actually an event!
func checkID {
# optional argument $name
# if not provided OR not a Str, fallback to default value "sir"
want $name: Str = "sir"
# required argument $age
# if not provided OR not a Num, the function is unsatisfied
# and will fail with an invalid argument error
need $age: Num
# not old enough
if $age < 21 {
# return pair 'underage'
# this will be a property of the return object,
# such as in checkID("John Doe", 43).underage
#
# the Bool at the end is specifying the type of the
# return pair 'underage' and will appear in the signature
#
# this is optional, but when present, guarantees that if
# the 'underage' pair is present, it will be of type Bool
#
underage -> true : Bool
say("Sorry, $name. Come back later.")
# return statement with no value means stop the
# execution of the function here. the function
# yields the return object (with 'underage' set),
# NOT void!
#
# if there were other callbacks for this event,
# they would continue execution--
# return does NOT stop event propagation.
#
->
}
say("Welcome, $name.")
}
checkID("John Doe", 43) # "Welcome, John Doe."
checkID(age: 12) # "Sorry, sir. Come back later."
checkID("Jane Roe") # no output (unsatisfied 'need')
These features apply to Functions and Events alike.
A signature describes the arguments and returns of a function. When passing unnamed arguments to a function call, the signature is used in determining which names the ordered arguments are associated with.
This is the human-readable signature of the checkID()
function:
?$name:Str $age:Num -> $underage:Bool
For each function call, a return object is created. The return values of the function are assigned to properties of this object.
Calls always yield the return object except when the return value is overridden
by an explicit return statement. In such a case, the return object can still be
accessed with detail
.
Remember: A return object with no values associated is false in boolean context, which allows functions returning nothing to yield false as expected.
If a function has an explicit return statement, its value overrides the
return object. The detail
keyword
allows you to access the return object when it would have otherwise been masked
by the override value.
The idea behind this is that, since Ferret is extensible through modules, dynamically added callbacks can still contribute return values to events that would normally yield only a single value.
fail
is an alternative to throw
for
indicating the failure of a function call.
fail Error(:Unimplemented, "This function is not yet implemented")
fail
does NOT throw an exception, nor does it stop the execution of the
thread. It only stops the execution of the current function and stores the
error. You should use fail
in place of ->
any time the function did not
achieve what it intended to or in place of throw
when non-fatal errors occur
but execution of the thread can continue.
In events, however, fail
serves another purpose. All callbacks dependent on
the one which failed (due to after
priority hints) are
canceled with failure status.
An event call only ever fails if every callback fails. The error message is set to the first error which occurred (usually that of the default callback).
Functions may accept a variable number of arguments.
func getSum {
need $nums: Num...
-> $nums.sum
}
getSum(1, 2, 3, 4, 5) # 15
You can also pass a list as a named argument.
$list = [6, 7, 8, 9]
getSum(nums: $list)
See the ellipsis for more.
These features are available to Events only.
Callback names allow callbacks to be identified by each other. This is useful for priority hints, cancellation, and tracebacks.
Callback names follow the func
,
method
, or on
declaration and are
identified by symbols.
You should make a habit of specifying a callback name any time it might be useful for other modules to tack on additional functionality.
on $myObj.someEvent, :doSomething {
statements
}
When the callback name is omitted from a function or method declaration,
:default
is implicit. For this reason it is not useful to specify a
callback name for every function or method declaration, only those which
are extensions of the event rather than the default responder.
The priority hints before
and
after
allow you to specify that a callback should be
executed at some point relative to another callback's execution time.
func enterBar {
need $name: Str
say("Welcome $name.")
}
func enterBar before :default, :checkID {
want $age: Int = 0
want $name: Str = "stranger"
if $age >= 21
->
stop # cancel all remaining callbacks
fail Error(:UnderageError, "Sorry $name...")
}
enterBar("John", age: 39) # "Welcome John."
enterBar("Chip", age: 12) # :UnderageError "Sorry Chip..."
enterBar("Mark") # :UnderageError "Sorry Mark..."
enterBar(age: 15) # :UnderageError "Sorry stranger..."
stop
cancels any remaining callbacks for the particular
call but does NOT stop the execution of the current callback function.
Remember: The ->
statement stops the execution
of the current callback function but does NOT stop the propagation of the event.
Use them in conjunction to achieve both.
The cancel
statement cancels the execution of a named
callback. If no such callback exists by the specified name, or if the callback
was already executed (due to a higher priority than the current one), cancel
quietly does nothing.
on say before :default, :censor {
need $message: Str
if $message == "shit"
cancel :default
}
Like named functions, all class instance methods are implemented as events.
All of the same rules mentioned above apply to methods. They are written
with the method
keyword rather than func
:
method orthogonalTo {
need $ehs: Vector
-> @dot($ehs) == 0
}
As long as there is at least one want
or need
in the method body,
you can use this shorthand syntax:
.orthogonalTo {
need $ehs: Vector
-> @dot($ehs) == 0
}
If there are no arguments specified, it is interpreted as a computed property implementation.
Hooks are literally exactly the same as methods, but using the hook
keyword makes it clear what your intention is and produces different
documentation.
Usually hooks have no body since they exist only for other modules to
latch onto. But, there's nothing stopping you from using the hook
keyword in place of method
an implementation with a body
just to indicate that it's a good place to hook onto.
hook connected
Operator implementations are also just methods, but they're written
with the more appropriate op
keyword.
#> addition of two vectors
op + {
need $ehs: Vector
if $ehs.dimension != @dimension
throw Error(:DimensionError, "Dimension mismatch $ehs.dimensionHR != @dimensionHR")
$items = gather for $i in [email protected] {
take *self[$i] + $ehs[$i]
}
-> Vector(items: $items)
}
Computed properties are methods also, except their return values are evaluated at the time the property is accessed.
prop direction {
if @dimension != 2
throw Error(:DimensionError, "Direction only exists in 2D")
-> Math.atan2(@y, @x)
}
Method short-hand syntax is allowed for computed properties.
# same as
.direction {
if @dimension != 2
throw Error(:DimensionError, "Direction only exists in 2D")
-> Math.atan2(@y, @x)
}
If the property name is suffixed by a question mark, its value will
only be computed once. After the first evaluation, the returned value will be
stored as the semi-permanent property value. This works similarly to lazy
assignment with the
lazy assignment operator ?=
.
.odd? -> *self % 2 != 0
Functions with only a single return instruction can be condensed to one line with curly brackets omitted. It also works for methods and computed properties.
func scare -> "Boo!"
method copy -> Point(@x, @y)
# these are the same
prop description -> "Point(@x, @y)"
.description -> "Point(@x, @y)"
Functions, methods, and properties with no body can exist as well. This is useful for hooks with no default callback. It's also used for documenting normal non-computed properties.
hook connected
# these are the same
prop m #< the line's slope
.m #< the line's slope