2
Functions and methods
Bartłomiej Przemysław Pluta edited this page 2020-04-01 21:12:22 +02:00

Function is a code snippet that takes some arguments and produces result. Both arguments and result can be optional. Method in turn is a special function that is called in behalf of some object. Function (and method of course) can be invoked using its name and optional arguments separated with commas (,) bounded on both sides with parentheses (( and )). Even if no arguments are passed you have to put empty arguments list: ().

Some functions are available out-of-the-box as they are implemented to SMNP tool. Set of such functions is called standard library and is provided in the form of so-called modules (you can read more about standard library and modules in later section). You are able also to create custom functions and methods (it'll be covered in next section).

As you can see on below examples implemented functions and methods mechanism is really similar to other popular languages, like Java or Python.

Examples:

# Invoking function 'println' without any arguments
println();

# Invoking function 'println' with 1 argument
println("Hello, world!");            # Hello, world

# Invoking function 'println' with 2 argument
println(1, 2);                       # 12

# Invoking function 'println' with 2 argument
# Invoked function produces also result which
# is being assigned to variable 'newNote1' 
newNote1 = transpose(2, @c);

# Method equivalent of function above
newNote2 = @c.transpose(2);

# Another example of method
firstElementOfList = [14, 3, 20, -4].get(0);


println(newNote1 == newNote2);       # true

Note, that some functions or methods don't return anything. In this case expecting any returned value can throw an exception.

# 'println' is example function, that doesn't return anything
# in this case following instruction will raise an error
println(println());                  # error

# other examples
x = println();                       # error

println([1, 2, 3, println(), 5, 6]); # error

Function/method signature

Signature is feature of both functions and methods that makes them unique. Signature consists of function/method name, function/method arguments and applicable type in case of method (however, technically the signature in the SMNP source code is referred only to arguments list).

Custom methods and functions

SMNP language introduces possibility to define custom functions and methods. They can be invoked later in the same way as regular functions/methods provided by SMNP standard library and other modules.

Functions and methods can be defined only at top-level of the code, which means they cannot be nested in any blocks.

Function definition

Functions can be defined with function keyword, like it is shown on following example:

function multipleBy2(number) {
    return 2*number;
}

# Correct invocations
x = multipleBy2(2);      # x = 4
x = multipleBy2(14);     # x = 28
x = multipleBy2("hey");  # in spite of correctness it will cause an error
                        # because string argument doesn't support '*' operator 

# Incorrect invocations
x = multipleBy2(1, 2);   # it'll cause Invocation Error because of signatures mismatch
x = multipleBy2();       # as above

Thanks to return keyword you can produce an output value basing on e.g. passed arguments. Example above takes one arbitrary argument, multiplies it by 2 and then returns a result.

Explicit argument types

Because arbitrary argument can be passed, the multipleBy2 function can throw an error in case of trying to multiple e.g. note or string, that don't support * operator. In this case you can put a constraint to the argument which accepts only chosen types (using Pascal/Kotlin-like syntax):

function multipleBy2(number: int) {
    return 2*number;
}

# Correct invocations
x = multipleBy2(2);      # x = 4
x = multipleBy2(14);     # x = 28

# Incorrect invocations 
x = multipleBy2(1, 2);   # it'll cause Invocation Error because of signatures mismatch
x = multipleBy2();       # as above
x = multipleBy2("hey");  # as above

Function multipleBy2 will work only with int argument now. Any attempt to invoke it with no-int values will cause Invocation Error related to mismatched signatures.

Of course, you are still able to create functions without any arguments or mix their types:

function noArgs() {
    println("Hello, I don't accept any arguments.");
    println("Also see that I don't return anything!");
}

function mixedArguments(a1: int, a2: note, a3) {
    println("See, " + a1 + " is an int!");
    println("And " + a2 + " is a note.");
    println("Type of third argument is " + typeOf(a3));
}

# Correct invocations
mixedArgument(1, @c, "hey");
mixedArguments(14, @Gb:4d, true);

# Incorrect invocations
mixedArgument(@c, @c, @c);
mixedArgument(1, 1, 1);

Functions returning nothing

Notice also that functions presented in previous example don't return anything. Technically they do actually return a void type and it is transparent to SMNP user. You should remember, that you can expect value produced by function only and only if return statement is declared in invoked function and the statement has been called. Otherwise function being invoked will return void type which cannot act as expression.

Ambiguous arguments

SMNP language allows you also to declare more than one argument that functions can accept. Accepted types can be declared using union construction which consists of types separated with commas (,) and bounded on both sides with < and > characters.

Example:

function foo(x: <string, bool, int, float>, y: note, z) {
    # 'x' can be either string, bool, int or float
    # whereas 'y' can be only a note 
    # and 'z' can be of any available type
}

# Correct invocations
foo("hey", @c, [1, 2, true, [@c], { a -> 1, b -> 2 }]);
foo(true, @f#:4d, "hello");
foo(14, @B3:2d, @c#:16d);
foo(1.4, @h5, 3.14);

# Incorrect invocations
foo("abc", @cb, 10);
foo("hey", 20, 10);
foo([1, 2, 3], @f, 10);

Specified lists

SMNP language allows you to specify what objects can be contained in lists. It can be done with construction similar to union construction. Accepted types are separated with commas (,), bounded on both sides with < and > characters and placed next to list keyword:

function foo(x: list<int>) {
    # 'x' can be only list of ints
}

# Correct invocations
foo([1, -2, 3]);
foo([0]);
foo([]);

# Incorrect invocations
foo(1);
foo(2, 3);
foo([ 1, 2, @c ]);

You are still able to use multiple type as constraints:

function foo(x: list<int, note>) {
    # 'x' can be only list of ints and notes, nothing else
}

# Correct invocations
foo([1, @c, 4]);
foo([]);
foo([0]);
foo([@d#]);

# Incorrect invocations
foo([1, @c, true]);
foo(@c, 1);
foo({ @c -> 1 });

List specifier can be empty or even be absent. In this case list can contain any available types:

function foo(x: list<>) {
    # 'x' can be only a list containing arbitrary objects
}

# the same function can be implemented as

function foo(x: list) {
    # ...
}

# Correct invocations
foo([1, @c, true]);
foo([]);
foo(["abc", 38, @g#:32d, false, [[[]]], { a -> 1, @d -> 2, true -> {} }]);

# Incorrect invocations
foo({});
foo([1, @c, true], 1);
foo(true);

Specifiers can be nested - you are able to create e.g. list of lists of lists of ints etc.

function foo(x: list<list<list<int>>>) {
}

# Correct invocations
foo([[[1, 2], [3, 4]], [[5, 6], [7, 8]]]);

# Incorrect invocations
foo([[[1, 2], [3, 4]], [[5, 6], [7, 8]], 9]);

Of course ambiguous arguments can include specified lists:

function foo(x: <int, note, list<int, note>) {
    # 'x' can be int, note or list containing only ints and notes
}

# Correct invocations
foo(@c);
foo(1);
foo([]);
foo([2, @G]);
foo([@h]);

# Incorrect invocations
foo(1.0);
foo([true]);

Specified maps

Maps can be also specified. All rules related to specified lists are also applicable for maps. The only difference is map supports 2 specifiers: first one for keys and second one for values.

Example:

function foo(x: map<string><note>) {
    # 'x' can be only a map with strings as keys and notes as values
}

function bar(x: map<string><>) {
    # 'x' can be only a map with strings as keys and arbitrary values
}

function xyz(x: map<string, bool><int, note>) {
    # 'x' can be only a map with strings and booleans as keys and ints and notes as values
}

function abc(x: map<><int, bool>) {
    # 'x' can be only a map with arbitrary keys and ints with booleans as values
}

# Correct invocations
foo({ c -> @c, d -> @d });
bar({ a -> @c, b -> 1, c -> true, d -> map, e -> { x -> [] });
xyz({ a -> 1, true -> @c, false -> 2, b -> @d });
abc({ a -> true, 1 -> false, @c -> 10, true -> 14 });

# Incorrect invocations
foo({ c -> @c, @d -> @d, @c -> "hey" });
bar({ a -> @c, @d -> @c });
xyz({ 1 -> @c, 2 -> 3, 3 -> bool });
abc({ a -> true, false -> @c });

Optional arguments

SMNP language allows you also to use default arguments. Optional arguments are special kind of arguments that can have default value and are totally optional to pass during invocation. Syntax of optional arguments is similar to variable assignment syntax.

Example:

function foo(x = 10) {
    # 'x' can be of any available type.
    # You can override default value but you don't have to.
    # In this case default value will be used.
    
    return x;
}

# Correct invocations
y = foo();       # y = 10
y = foo(10);     # y = 10
y = foo(true);   # y = true

All kinds of arguments mentioned so far can be optional:

function foo(x = 1, y: int = 14, z: <note, list<list<int, note>> = [[1, @c], [@d]]) {
}

# Correct invocations
foo();
foo(2);
foo(10, 11);
foo(-2, 33, @c);
foo(-2, 33, [[1, @d, @f#], [4, 3], [@c, @d, @e]);
foo(0, 0, []);
foo(0, 0, [[]]);

# Incorrect invocations
foo(true);
foo(1, 0.5);
foo(10, 2, 13);
foo(0, 0, [], 3);

As you can see, you can have as many optional arguments as you want. Just remember, that after first declared optional argument you can't use regular arguments in the same signature anymore.

# Correct signatures
function foo(a, b, c = 0) {}
function bar(a, b = "hello", c = true) {}
function xyz(a = 14, b = [12.5], c = @c) {}

# Incorrect signatures (will throw an error)
function abc(a = 0, b) {}
function def(a, b, c = 0, d = true, e, f = @G#) {}

In other words, optional arguments should be declared at the tail of function signature.

Varargs

SMNP language also supports functions' signatures with arbitrary length. This is exactly how println or print functions work. You can pass as many arguments as you want and functions are still able to handle it. SMNP language provides a special operator ... that enables you to have function with variable number of arguments.

Remember that:

  • you can have only one vararg at the signature
  • vararg should be placed at the very end of signature
  • you can't mix varargs with optional arguments in one signature.

After invocation, all matched arguments are collected into one list which is passed as vararg.

Example:

function foo(a, b, ...c) {       
    # 'c' is a list of arguments passed after 'a' and 'b'

    return c;
}

# Correct invocations:
x = foo(0, 1);                                       # x = []
x = foo(1, 2, 3, 4);                                 # x = [3, 4]
x = foo(true, false, @c, [3.14, 5, "abc"], 2); # x = [@c, [3.14, 5, "abc"], 2]

# Incorrect invocations:
foo();
foo(true);

Same as optional arguments, vararg can work with any previously mentioned kinds of arguments (except optional arguments of course).

function foo(a, b: note, ...c: map<string><list<int, note>>) {
    # after two arguments you can pass any number of
    # maps with strings as keys and list of ints and notes as values
}

# Correct invocations:
foo(1, @c, { a -> [@d], b -> [@c, @g] }, { a -> [], b -> [@e] });
foo(1, @c, {}, {}, {}, {}, {}, {}, {}, {});

# Incorrect invocations:
foo();
foo(1, @c, { a -> [@d], b -> [@c, @g] }, { a -> [], b -> @e }); 

Method definition

All rules related to defining custom functions are applicable to methods either. The difference is method can be invoked in a context of some object. That's why methods can be defined only as a part of some type.

extend statement

SMNP language introduces extend statement which allows you to add custom methods to existing types. All you need is to precise desired type and define function which will be working as new method. In the body of declared method, the extending object is available through this special identifier. Following listing presents a syntax of extend statement:

extend int with function multipleBy(x: int) {
    return this*x;
}

# From now on you can use new method as follows:
x = 10.multipleBy(2);        # x = 20
y = x.multipleBy(4);         # y = 80

As mentioned before, all rules related to functions are applicable here, so there is no need to cover function multipleBy(...) {...} statement again.

Note, that type being extended must be declared only as a simple data type or map/list with optional

Examples:

# Correct
extend int with function a() {}
extend list with function b() {}
extend list<> with function c() {}
extend list<list<note, int>> with function d() {}
extend map<string><note> with function e() {}
extend map<><> with function f() {}
extend map<><list<map<><note, string, list<int>>>> with function g() {}

# Incorrect
extend <string, int> with function h() {}
extend <int> with function i() {}
extend <> with function j() {}
extend int with function k() {}

Because extending statement supports list/map specifiers, you can define methods that are applicable only for list/map with specified content:

# Extends all lists, no matter of content
extend list with function foo() {
    println("foo()");
}

# Extends only list<int>
extend list<int> with function bar() {
    println("bar()");
}

l1 = [1, 2, 3, 4];
l2 = [1, 2, @c, 4];
l3 = ["a", "b", "c", "d"];

l1.foo();        # foo()
l2.foo();        # foo()
l3.foo();        # foo()

l1.bar();        # bar()
l2.bar();        # error!
l3.bar();        # error!

Because SMNP doesn't allow you to extend multiple types in one instruction, if you want extend for example list<int> and list<note> (but not list<int, note>) you have to do it separately:

# This won't work at all
extend <list<int>, list<note>> as l with function foo() {
    # ...
}

# Instead of that you can do:
extend list<int> as l with function foo() {
    # ...
}

extend list<note> as l with function foo() {
    # ...
}

extend block

In case of extending type with more than one function you can use block of methods.

Example:

extend int {
    function multipleBy2() {
        return 2*this;
    }

    function multipleBy4() {
        return 4*this;
    }
}

# Construction above is equivalent of following code:

extend int with function multipleBy2() {
    return 2*this;
}

extend int with function multipleBy4() {
    return 4*this;
}

Overloading function/methods

Unfortunately functions/methods overloading is not supported in custom functions/methods so far. Both functions and methods can be overloaded. It means, you are able to declare multiple functions and methods applicable to the same objects as long as you enforce them to have different signatures.

Note, that potential signatures' collision is evaluated only at runtime, when method is being called.

Example:

function display(x: int) {
  println("int: ", x);
}

function display(x: float) {
  println("float: ", x);
}

display(14);     # int: 14
display(14.0);   # float: 14.0

However:

function display(x) {
  println("any: ", x);
}

function display(x: float) {
  println("float: ", x);
}

display(14);     # any: 14
display(14.0);   # error!

The code will end with following error:

Function invocation error
Source: /.../.../scratchpad.mus
Position: line 16, column 1

Found 2 functions with name of display, that matched provided arguments: [<root>, <root>]

Stack trace:
[0] <root>::<entrypoint>()