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>()
Simple Music Notation Processor
SMNP Language Reference
- About SMNP Language
- Import statement
- Supported data types
- Variables
- Operators
- Functions and methods
- Condition statement
- Loop statement-expression
- Error handling
Modules and standard library: