Files
fabric-carpet/docs/scarpet/language/FunctionsAndControlFlow.md
T
2021-02-21 01:52:55 +01:00

14 KiB

User-defined functions and program control flow

Writing programs with more than 1 line

Operator ;

To effectively write programs that have more than one line, a programmer needs way to specify a sequence of commands that execute one after another. In scarpet this can be achieved with ;. Its an operator, and by separating statements with semicolons. And since whitespaces and $ (commandline visible newline separator) sign are all treats as whitespaces, how you layout your code doesn't matter, as long as it is readable to everyone involved.

expr;
expr;
expr;
expr

Notice that the last expression is not followed by a semicolon. Since instruction separation is functional in scarpet, and not barely an instruction delimiter, terminating the code with a dangling operator wouldn't be valid. Having said that, since many programming languages don't care about the number of op terminators programmers use, carpet preprocessor will remove all unnecessary semicolons from scripts when compiled.

In general expr; expr; expr; expr is equivalent to (((expr ; expr) ; expr) ; expr).

Result of the evaluated expression is the same as the result of the second expression, but first expression is also evaluated for side-effects

expr1 ; expr2 => expr2  // with expr1 as a side-effect

Global variables

All defined functions are compiled, stored persistently, and available globally within the app. Functions can only be undefined via call to undef('fun'), which would erase global entry for function fun. Since all variables have local scope inside each function, or each command script, global variables is a way to share the global state.

Any variable that is used with a name that starts with 'global_' will be stored and accessible globally, not only inside the current scope. If used directly in the chat window with the default app, it will persist across calls to /script function. Like functions, which are global, global variables can only be undefined via undef.

For apps running in 'global' scope - all players will share the same global variables and defined functions, and with player scope, each player hosts its own state for each app, so function and global_variables are distinct.

/script run a() -> global_list+=1; global_list = l(1,2,3); a(); a(); global_list  // => [1, 2, 3, 1, 1]
/script run a(); a(); global_list  // => [1, 2, 3, 1, 1, 1, 1]

Operator ->

-> operator has two uses - as a function definition operator and key-value initializer for maps.

To organize code better than a flat sequence of operations, one can define functions. Definition is correct if has the following form

fun(args, ...) -> expr

Where fun(args, ...) is a function signature indicating function name, number of arguments, and their names, and expr is an expression (can be complex) that is evaluated when fun is called. Names in the signature don't need to be used anywhere else, other occurrences of these names will be masked in this function scope. Function call creates new scope for variables inside expr, so all non-global variables are not visible from the caller scope. All parameters are passed by value to the new scope, including lists and other containers, however their copy will be shallow.

The function returns itself as a first class object, which means it can be used to call it later with the call function

Using _ as the function name creates anonymous function, so each time _ function is defined, it will be given a unique name, which you can pass somewhere else to get this function called. Anonymous functions can only be called by their value and call method.

a(lst) -> lst+=1; list = l(1,2,3); a(list); a(list); list  // => [1,2,3]

In case the inner function wants to operate and modify larger objects, lists from the outer scope, but not global, it needs to use outer function in function signature.

in map construction context (directly in m() or {}), the -> operator has a different function by converting its arguments to a tuple which is used by map constructor as a key-value pair:

{ 'foo' -> 'bar' } => {l('foo', 'bar')}

This means that it is not possible to define literally a set of inline function, however a set of functions can still be created by adding elements to an empty set, and building it this way. That's a tradeoff for having a cool map initializer.

outer(arg)

outer function can only be used in the function signature, and it will cause an error everywhere else. It saves the value of that variable from the outer scope and allows its use in the inner scope. This is a similar behaviour to using outer variables in lambda function definitions from Java, except here you have to specify which variables you want to use, and borrow

This mechanism can be used to use static mutable objects without the need of using global_... variables

list = l(1,2,3); a(outer(list)) -> list+=1;  a(); a(); list  // => [1,2,3,1,1]

The return value of a function is the value of the last expression. This as the same effect as using outer or global lists, but is more expensive

a(lst) -> lst+=1; list = l(1,2,3); list=a(list); list=a(list); list  // => [1,2,3,1,1]

Ability to combine more statements into one expression, with functions, passing parameters, and global and outer scoping allow to organize even larger scripts

Operator ...

Defines a function argument to represent a variable length argument list of whatever arguments are left from the argument call list, also known as a varargs. There can be only one defined vararg argument in a function signature. Technically, it doesn't matter where is it, but it looks best at the butt side of it.

foo(a, b, c) -> ...  # fixed argument function, call foo(1, 2, 3)
foo(a, b, ... c) -> ... # c is now representing the variable argument part
    foo(1, 2)  # a:1, b:2, c:[]
    foo(1, 2, 3)  # a:1, b:2, c:[3]
    foo(1, 2, 3, 4)  # a:1, b:2, c:[3, 4] 
foo(... x) -> ...  # all arguments for foo are included in the list

    

import(module_name, symbols ...)

Imports symbols from other apps and libraries into the current one: global variables or functions, allowing to use them in the current app. This includes other symbols imported by these modules. Scarpet supports circular dependencies, but if symbols are used directly in the module body rather than functions, it may not be able to retrieve them. Returns full list of available symbols that could be imported from this module, which can be used to debug import issues, and list contents of libraries.

call(function, args.....)

calls a user defined function with specified arguments. It is equivalent to calling function(args...) directly except you can use it with function value, or name instead. This means you can pass functions to other user defined functions as arguments and call them with call internally. Since function definitions return the defined function, they can be defined in place as anonymous functions.

Passing function references to other modules of your application

In case a function is defined by its name, Scarpet will attempt to resolve its definition in the given module and its imports, meaning if the call is in a imported library, and not in the main module of your app, and that function is not visible from the library perspective, but in the app, it won't be call-able. In case you pass a function name to a separate module in your app, it should import back that method from the main module for visibility.

Check an example of a problematic code of a library that expects a function value as a passed argument and how it is called in the parent app:

//app.sc
import('lib', 'callme');
foo(x) -> x*x;
test() -> callme('foo' , 5);
//lib.scl
callme(fun, arg) -> call(fun, arg);

In this case 'foo' will fail to dereference in lib as it is not visible by name. In tightly coupled modules, where lib is just a component of your app you can use circular import to acknowledge the symbol from the other module (pretty much like imports in Java classes), and that solves the issue but makes the library dependent on the main app:

//lib.scl
import('app','foo');
callme(fun, arg) -> call(fun, arg);

You can circumvent that issue by explicitly dereferencing the local function where it is used as a lambda argument created in the module in which the requested function is visible:

//app.sc
import('lib', 'callme');
foo(x) -> x*x;
test() -> callme(_(x) -> foo(x), 5);
//lib.scl
callme(fun, arg) -> call(fun, arg);

Or by passing an explicit reference to the function, instead of calling it by name:

//app.sc
import('lib', 'callme');
global_foohandler = (foo(x) -> x*x);
test() -> callme(global_foohandler, 5);

Little technical note: the use of _ in expression passed to built in functions is much more efficient due to not creating new call stacks for each invoked function, but anonymous functions is the only mechanism available for programmers with their own lambda arguments

my_map(list, function) -> map(list, call(function, _));
my_map(l(1,2,3), _(x) -> x*x);    // => [1,4,9]
profile_expr(my_map(l(1,2,3), _(x) -> x*x));   // => ~32000
sq(x) -> x*x; profile_expr(my_map(l(1,2,3), 'sq'));   // => ~36000
sq = (_(x) -> x*x); profile_expr(my_map(l(1,2,3), sq));   // => ~36000
profile_expr(map(l(1,2,3), _*_));   // => ~80000

Control flow

return(expr?)

Sometimes its convenient to break the organized control flow, or it is not practical to pass the final result value of a function to the last statement, in this case a return statement can be used

If no argument is provided - returns null value.

def() -> (
   expr1;
   expr2;
   return(expr3); // function terminates returning expr3
   expr4;     // skipped
   expr5      // skipped
)

In general its cheaper to leave the last expression as a return value, rather than calling returns everywhere, but it would often lead to a messy code.

exit(expr?)

It terminates entire program passing expr as the result of the program execution, or null if omitted.

try(expr, id_filter?, catch_expr(_, _msg)?, id_filter2?, catch_expr2(_, _msg), ...)

try function evaluates expression, and continues further unless an exception is thrown anywhere inside expr, it being produced by throw or a catchable exception being thrown by Carpet. In that case the catch_expr is evaluated with _ set to the id of the exception and _msg set to the exception message. If an id_filter is present, only exceptions with an id equal to that value will be caught, and any other exception will continue up the stack. You can specify multiple filters and their respective catch_expr to be able to catch different exceptions within the same code. When an exception occurs, the script will try to find an equality with every provided filter in order. Therefore, even if the caught exception matches multiple filters, only the first matching block will be executed. It is recommended to filter the exceptions to catch to be able to distinguish from an exception that is known that may happen from one that shouldn't.

This mechanic accepts skipping catch expression - then try returns null. This mechanism allows to terminate large portion of a convoluted call stack and continue program execution.

A try block will not only catch any exception whose id match the id_filter, but also any exception that has a parent with their id being that one, recursively.

The try function allows you to catch some Scarpet exceptions for those cases when trying to get things like items, blocks, biomes or dimensions from registries, that may have been modified by datapacks, resourcepacks or other mods, or when an error that is out of the scope of the programmer occurs, such as problems when reading files.

In this documentation, those are documented in at least most of the functions that throw them, and the current exception hierarchy is the following:

  • exception: This is the base exception. Every exception has this as its top-level parent
    • value_exception: This is the parent for any exception that occurs due to a provided value for something not being present in the game
      • unknown_item: Happens when a specified item doesn't exist
      • unknown_block: Happens when a specified block doesn't exist
      • unknown_biome: Happens when a specified biome doesn't exist
      • unknown_sound: Happens when a specified sound doesn't exist
      • unknown_particle: Happens when a specified particle doesn't exist
      • unknown_poi_type: Happens when a specified POI type doesn't exist
      • unknown_dimension: Happens when a specified dimension doesn't exist
      • unknown_structure: Happens when a specified structure doesn't exist
      • unknown_criterion: Happens when a specified scoreboard criterion doesn't exist
    • file_read_exception: This is the parent for any exception that occurs due to an error while reading a file, including appdata load
      • nbt_read_exception: Happens when there is an exception while reading an NBT file
      • json_read_exception: Happens when there is an exception while reading a JSON file
    • user_defined_exception: This is the parent for any exception thrown by the below throw function, except for those that explicitly declare their parent to be one of the above

throw(id?, message?, parent?)

Throws an exception that can be caught in a try block (see above). If ran without arguments, it will pass null as the value to the catch_expr. message is the message to show to chat or console in case the throw is unhandled.

By default, the parent of any exception thrown by this function is user_defined_exception. You can specify the parent to be either an already existing exception from the list above, in which case your exception will not inherit from user_defined_exception, or specify a custom one, in which case your exception will inherit from exception > user_defined_exception > parent > id

if(cond, expr, cond?, expr?, ..., default?)

If statement is a function that takes a number of conditions that are evaluated one after another and if any of them turns out true, its expr gets returned, otherwise, if all conditions fail, the return value is default expression, or null if default is skipped

if function is equivalent to if (cond) expr; else if (cond) expr; else default; from Java, just in a functional form