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 of bar, 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:

  1. Explicit parameters: Methods must explicitly declare parameters; the implicit argument feature is not supported for TRO.

  2. Flow control: TRO is only compatible with If-Else, Cases, and Switch constructs for branching.

  3. Call structure: Nested method calls, such as x(a)(b)(c), are not supported for TRO.

  4. 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

See also

results matching ""

    No results matching ""