Method
Methods are recipes that can take arguments to "fill in the blanks".
in FatScript, we refer to all functions as Methods, irrespective of their definition context
Definition
A method is anonymously defined with a thin arrow ->
, like so:
<parameters> -> <recipe>
Parameters can be omitted if none are needed:
-> <recipe> # arity zero
To register a method to the scope, assign it to an identifier:
<identifier> = <parameters> -> <recipe>
Parameters within a method's execution scope are immutable, ensuring that the method's operations do not alter their original state. For mutable behavior, consider passing a scope or utilizing a custom type capable of encapsulating multiple values and states.
Optional parameters
While method signatures typically require a fixed number of mandatory parameters, FatScript supports optional parameters through default values:
greet = (name: Text = 'World') -> {
'Hello, {name}'
}
greet() # 'Hello, World'
In this example, the name
parameter is optional, defaulting to 'World' if no argument is provided. This feature allows for more flexible method invocations.
Argument handling
Method calls in FatScript are designed to accept more arguments than required; extra arguments are simply ignored. This behavior is part of the language's design to enhance flexibility and performance.
Auto-return
FatScript uses auto-return, meaning the last standing value is returned:
answer: Method = (theGreatQuestion) -> {
# TODO: explain Life, the Universe and Everything
42
}
answer('6 x 7 = ?') # outputs: 42
Return type safety
In FatScript, one peculiarity is that even when you declare a method with a specific return type, the language allows for null
values, like in:
fn = (arg: Text): Text -> ... ? ... : null
This means that while the method is declared to return Text
, the return value is, in a sense, optional because the method can also return Void
. The only strict guarantee is that if the method tries to return an incompatible type, such as a Number
or Boolean
, a TypeError
will be raised. This design choice introduces implicit flexibility while still maintaining a degree of type safety.
If you need to ensure a non-null outcome, you can wrap your call with Option like this:
Option(fn(myArg)).getOrElse('fallbackVal')
Automatic calls
FatScript introduces a unique feature that simplifies method calls, when no arguments are involved. This feature is known as the "automatic call trick" and it offers several key benefits:
Reduced Boilerplate: Reduces the need for parentheses, making code cleaner and more concise, for zero-parameter methods that act like properties or procedures.
Dynamic Computation: Allows for dynamic computation with outputs that can change based on the object's internal or global state.
Deferred Execution: Enables deferred execution, useful in asynchronous programming and complex initialization patterns.
Old implementation (deprecated)
In FatScript, a method defined without parameters executes "automagically" when referenced:
foo = {
bar = -> 'Hello!'
}
# Both lines below output 'Hello!'
foo.bar() # explicit call
foo.bar # automatic call
Procedures (new)
<identifier> = <type> <> <recipe>
Note: for procedure syntax the Type needs to be a single word; if you need a compound type, declare it as an alias beforehand and use the alias.
Starting in version
3.4.0
The <>
symbol explicitly declares a method as a procedure, an argument-free function that executes automatically when referenced:
meth = (): Text -> 'yo' // classic method syntax
proc = Text <> 'yo' // new procedure syntax
Starting in version
4.0.0
Classic methods will no longer auto-call and will require ()
parentheses to execute. Only procedures will support automatic execution without parentheses. Passing arguments to a procedure will raise an error, as procedures do not accept arguments.
This is a breaking change, but it is intended to make code safer and easier to distinguish, while reducing the confusion caused by passing methods as arguments.
Referencing
To reference a procedure without triggering the automatic calling feature, you can use the the get syntax:
foo('bar') # yields a reference to foo.bar, without calling it
FatScript also offers self
and root
keywords to reference procedures at the local and global levels, respectively:
self('myLocalProcedure')
root('myGlobalProcedure')
Avoiding an automatic call
The tilde ~
operator allows you to bypass the automatic call feature, providing flexibility in procedure handling:
# Both lines below fetch the procedure reference, without calling it
foo.~bar
~ myProcedure
Or you can simply wrap the procedure call into yet another procedure:
<> foo.bar
Warning: passing methods as arguments
There's an important exception when it comes to passing methods as arguments, specifically in the case of a local method:
another(bar) # passes `bar` as a reference, without executing it
however, this does not apply with chaining:
another(foo.bar)
passes the result ofbar
, not the reference
In this case, to pass the value resulting of the local method bar
, an explicit call must be made:
another(bar())
this behavior might seem counterintuitive, but it is extremely useful in various use cases, such as when passing methods to reduce, to an asynchronous task, to a mapping operation etc.
Implicit argument
A convenience offered by FatScript is the ability to reference a value passed to the method without explicitly specifying a name for it. In this case, the implicit argument is represented by the underscore _
.
Here's an example that illustrates the use of implicit argument:
double = -> _ * 2
double(3) # output: 6
You can use an implicit argument whenever you need to perform a simple operation on a single parameter without assigning a specific name to it, but note that the method must have arity zero to trigger it.
Tail Recursion Optimization
starting with version
3.2.0
FatScript supports Tail Recursion Optimization (TRO) to enhance performance by conserving stack space. To benefit from this optimization, several conditions must be satisfied:
Explicit parameters: Methods must explicitly declare parameters; the implicit argument feature is not supported for TRO.
Flow control: TRO is only compatible with
If-Else
,Cases
, andSwitch
constructs for branching.Call structure: Nested method calls, such as
x(a)(b)(c)
, are not supported for TRO.Recursive calls: The method must call itself recursively by name as the final operation in its execution path.
For example, a function correctly set up for TRO might look like this:
tailRec = (n: Number, m: Number): Void -> {
n > m => console.log('done')
_ => {
console.log(n)
tailRec(n + 1, m)
}
}
In this example, tailRec
recursively calls itself as the final operation in one of the branches, making it eligible for optimization.
You can check if TRO has been enabled for your method using static analysis with the fry --probe
option.
TRO can be disabled by wrapping the recursive call within parentheses, as shown below:
...
(tailRec(n + 1, m)) # no TRO