1256 lines
34 KiB
Markdown
1256 lines
34 KiB
Markdown
---
|
|
gitea: none
|
|
include_toc: true
|
|
---
|
|
|
|
# Lyng tutorial
|
|
|
|
Lyng is a very simple language, where we take only most important and popular features from
|
|
other scripts and languages. In particular, we adopt _principle of minimal confusion_[^1].
|
|
In other word, the code usually works as expected when you see it. So, nothing unusual.
|
|
|
|
__Other documents to read__ maybe after this one:
|
|
|
|
- [Advanced topics](advanced_topics.md), [declaring arguments](declaring_arguments.md)
|
|
- [OOP notes](OOP.md), [exception handling](exceptions_handling.md)
|
|
- [math in Lyng](math.md)
|
|
- Some class references: [List], [Set], [Map], [Real], [Range], [Iterable], [Iterator]
|
|
- Some samples: [combinatorics](samples/combinatorics.lyng.md), national vars and loops: [сумма ряда](samples/сумма_ряда.lyng.md). More at [samples folder](samples)
|
|
|
|
# Expressions
|
|
|
|
Everything is an expression in Lyng. Even an empty block:
|
|
|
|
// empty block
|
|
>>> void
|
|
|
|
any block also returns it's last expression:
|
|
|
|
if( true ) {
|
|
2 + 2
|
|
3 + 3
|
|
}
|
|
>>> 6
|
|
|
|
If you don't want block to return anything, use `void`:
|
|
|
|
fn voidFunction() {
|
|
3 + 4 // this will be ignored
|
|
void
|
|
}
|
|
voidFunction()
|
|
>>> void
|
|
|
|
otherwise, last expression will be returned:
|
|
|
|
fn normalize(value, minValue, maxValue) {
|
|
(value - minValue) / (maxValue-minValue)
|
|
}
|
|
normalize( 4, 0.0, 10.0)
|
|
>>> 0.4
|
|
|
|
Every construction is an expression that returns something (or `void`):
|
|
|
|
val x = 111 // or autotest will fail!
|
|
val limited = if( x > 100 ) 100 else x
|
|
limited
|
|
>>> 100
|
|
|
|
You can use blocks in if statement, as expected:
|
|
|
|
val x = 200
|
|
val limited = if( x > 100 ) {
|
|
100 + x * 0.1
|
|
}
|
|
else
|
|
x
|
|
limited
|
|
>>> 120.0
|
|
|
|
When putting multiple statments in the same line it is convenient and recommended to use `;`:
|
|
|
|
var from; var to
|
|
from = 0; to = 100
|
|
>>> 100
|
|
|
|
Notice: returned value is `100` as assignment operator returns its assigned value.
|
|
Most often you can omit `;`, but improves readability and prevent some hardly seen bugs.
|
|
|
|
## Assignments
|
|
|
|
Assignemnt is an expression that changes its lvalue and return assigned value:
|
|
|
|
var x = 100
|
|
x = 20
|
|
println(5 + (x=6)) // 11: x changes its value!
|
|
x
|
|
>>> 11
|
|
>>> 6
|
|
|
|
As the assignment itself is an expression, you can use it in strange ways. Just remember
|
|
to use parentheses as assignment operation insofar is left-associated and will not
|
|
allow chained assignments (we might fix it later). Use parentheses insofar:
|
|
|
|
var x = 0
|
|
var y = 0
|
|
x = (y = 5)
|
|
assert(x==5)
|
|
assert(y==5)
|
|
>>> void
|
|
|
|
Note that assignment operator returns rvalue, it can't be assigned.
|
|
|
|
## Modifying arithmetics
|
|
|
|
There is a set of assigning operations: `+=`, `-=`, `*=`, `/=` and even `%=`.
|
|
|
|
var x = 5
|
|
assert( 25 == (x*=5) )
|
|
assert( 25 == x)
|
|
assert( 24 == (x-=1) )
|
|
assert( 12 == (x/=2) )
|
|
x
|
|
>>> 12
|
|
|
|
Notice the parentheses here: the assignment has low priority!
|
|
|
|
These operators return rvalue, unmodifiable.
|
|
|
|
## Assignment return r-value!
|
|
|
|
Naturally, assignment returns its value:
|
|
|
|
var x
|
|
x = 11
|
|
>>> 11
|
|
|
|
rvalue means you cant assign the result if the assignment
|
|
|
|
var x
|
|
assertThrows { (x = 11) = 5 }
|
|
void
|
|
>>> void
|
|
|
|
This also prevents chain assignments so use parentheses:
|
|
|
|
var x
|
|
var y
|
|
x = (y = 1)
|
|
>>> 1
|
|
|
|
## Nullability
|
|
|
|
When the value is `null`, it might throws `NullReferenceException`, the name is somewhat a tradition. To avoid it
|
|
one can check it against null or use _null coalescing_. The null coalescing means, if the operand (left) is null,
|
|
the operation won't be performed and the result will be null. Here is the difference:
|
|
|
|
val ref = null
|
|
assertThrows { ref.field }
|
|
assertThrows { ref.method() }
|
|
assertThrows { ref.array[1] }
|
|
assertThrows { ref[1] }
|
|
assertThrows { ref() }
|
|
|
|
assert( ref?.field == null )
|
|
assert( ref?.method() == null )
|
|
assert( ref?.array?[1] == null )
|
|
assert( ref?[1] == null )
|
|
assert( ref?() == null )
|
|
>>> void
|
|
|
|
There is also "elvis operator", null-coalesce infix operator '?:' that returns rvalue if lvalue is `null`:
|
|
|
|
null ?: "nothing"
|
|
>>> "nothing"
|
|
|
|
## Utility functions
|
|
|
|
The following functions simplify nullable values processing and
|
|
allow to improve code look and readability. There are borrowed from Kotlin:
|
|
|
|
### let
|
|
|
|
`value.let {}` passes to the block value as the single parameter (by default it is assigned to `it`) and return block's returned value. It is useful dealing with null or to
|
|
get a snapshot of some externally varying value, or with `?.` to process nullable value in a safe manner:
|
|
|
|
// this state is changed from parallel processes
|
|
class GlobalState(nullableParam)
|
|
|
|
val state = GlobalState(null)
|
|
|
|
fun sample() {
|
|
state.nullableParam?.let { "it's not null: "+it} ?: "it's null"
|
|
}
|
|
assertEquals(sample(), "it's null")
|
|
state.nullableParam = 5
|
|
assertEquals(sample(), "it's not null: 5")
|
|
>>> void
|
|
|
|
This is the same as:
|
|
|
|
fun sample() {
|
|
val it = state.nullableParam
|
|
if( it != null ) "it's not null: "+it else "it's null"
|
|
}
|
|
|
|
The important is that nullableParam got a local copy that can't be changed from any
|
|
parallel thread/coroutine. Remember: Lyng _is __not__ a single-threaded language_.
|
|
|
|
## Also
|
|
|
|
Much like let, but it does not alter returned value:
|
|
|
|
assert( "test".also { println( it + "!") } == "test" )
|
|
>>> test!
|
|
>>> void
|
|
|
|
While it is not altering return value, the source object could be changed:
|
|
|
|
class Point(x,y)
|
|
val p = Point(1,2).also { it.x++ }
|
|
assertEquals(p.x, 2)
|
|
>>> void
|
|
|
|
## apply
|
|
|
|
It works much like `also`, but is executed in the context of the source object:
|
|
|
|
class Point(x,y)
|
|
// see the difference: apply changes this to newly created Point:
|
|
val p = Point(1,2).apply { x++; y++ }
|
|
assertEquals(p, Point(2,3))
|
|
>>> void
|
|
|
|
## Math
|
|
|
|
It is rather simple, like everywhere else:
|
|
|
|
val x = 2.0
|
|
sin(x * π/4) / 2.0
|
|
>>> 0.5
|
|
|
|
See [math](math.md) for more on it. Notice using Greek as identifier, all languages are allowed.
|
|
|
|
Logical operation could be used the same
|
|
|
|
var x = 10
|
|
++x >= 11
|
|
>>> true
|
|
|
|
## Supported operators
|
|
|
|
| op | ass | args | comments |
|
|
|:--------:|-----|-------------------|----------|
|
|
| + | += | Int or Real | |
|
|
| - | -= | Int or Real | infix |
|
|
| * | *= | Int or Real | |
|
|
| / | /= | Int or Real | |
|
|
| % | %= | Int or Real | |
|
|
| && | | Bool | |
|
|
| \|\| | | Bool | |
|
|
| !x | | Bool | |
|
|
| < | | String, Int, Real | (1) |
|
|
| <= | | String, Int, Real | (1) |
|
|
| >= | | String, Int, Real | (1) |
|
|
| > | | String, Int, Real | (1) |
|
|
| == | | Any | (1) |
|
|
| === | | Any | (2) |
|
|
| !== | | Any | (2) |
|
|
| != | | Any | (1) |
|
|
| ++a, a++ | | Int | |
|
|
| --a, a-- | | Int | |
|
|
|
|
(1)
|
|
: comparison are based on comparison operator which can be overloaded
|
|
|
|
(2)
|
|
: referential equality means left and right operands references exactly same instance of some object. Note that all
|
|
singleton object, like `null`, are referentially equal too, while string different literals even being equal are most
|
|
likely referentially not equal
|
|
|
|
Reference quality and object equality example:
|
|
|
|
assert( null == null) // singletons
|
|
assert( null === null)
|
|
// but, for non-singletons:
|
|
assert( 5 == 5)
|
|
assert( 5 !== 5)
|
|
assert( "foo" !== "foo" )
|
|
>>> void
|
|
|
|
# Variables
|
|
|
|
Much like in kotlin, there are _variables_:
|
|
|
|
var name = "Sergey"
|
|
|
|
Variables can be not initialized at declaration, in which case they must be assigned before use, or an exception
|
|
will be thrown:
|
|
|
|
var foo
|
|
// WRONG! Exception will be thrown at next line:
|
|
foo + "bar"
|
|
|
|
Correct pattern is:
|
|
|
|
foo = "foo"
|
|
// now is OK:
|
|
foo + bar
|
|
|
|
This is though a rare case when you need uninitialized variables, most often you can use conditional operators
|
|
and even loops to assign results (see below).
|
|
|
|
# Constants
|
|
|
|
Almost the same, using `val`:
|
|
|
|
val foo = 1
|
|
foo += 1 // this will throw exception
|
|
|
|
# Constants
|
|
|
|
Same as in kotlin:
|
|
|
|
val HalfPi = π / 2
|
|
|
|
Note using greek characters in identifiers! All letters allowed, but remember who might try to read your script, most
|
|
likely will know some English, the rest is the pure uncertainty.
|
|
|
|
# Defining functions
|
|
|
|
fun check(amount) {
|
|
if( amount > 100 )
|
|
"enough"
|
|
else
|
|
"more"
|
|
}
|
|
>>> Callable@...
|
|
|
|
Notice how function definition return a value, instance of `Callable`.
|
|
|
|
You can use both `fn` and `fun`. Note that function declaration _is an expression returning callable_,
|
|
but Lyng syntax requires using the __lambda syntax__ to create such.
|
|
|
|
val check = {
|
|
it > 0 && it < 100
|
|
}
|
|
assert( check(1) )
|
|
assert( !check(101) )
|
|
>>> void
|
|
|
|
See lambdas section below.
|
|
|
|
## Declaring arguments
|
|
|
|
There are default parameters in Lyng:
|
|
|
|
fn check(amount, prefix = "answer: ") {
|
|
prefix + if( amount > 100 )
|
|
"enough"
|
|
else
|
|
"more"
|
|
}
|
|
assert( "do: more" == check(10, "do: ") )
|
|
check(120)
|
|
>>> "answer: enough"
|
|
|
|
It is possible to define also vararg using ellipsis:
|
|
|
|
fun sum(args...) {
|
|
var result = args[0]
|
|
for( i in 1 ..< args.size ) result += args[i]
|
|
}
|
|
sum(10,20,30)
|
|
>>> 60
|
|
|
|
See the [arguments reference](declaring_arguments.md) for more details.
|
|
|
|
## Closures
|
|
|
|
Each __block has an isolated context that can be accessed from closures__. For example:
|
|
|
|
var counter = 1
|
|
|
|
// this is ok: counter is incremented
|
|
fun increment(amount=1) {
|
|
// use counter from a closure:
|
|
counter = counter + amount
|
|
}
|
|
|
|
increment(10)
|
|
assert( counter == 11 )
|
|
|
|
val callable = {
|
|
// this obscures global outer var with a local one
|
|
var counter = 0
|
|
// ...
|
|
counter = 1
|
|
// ...
|
|
counter
|
|
}
|
|
|
|
assert(callable() == 1)
|
|
// but the global counter is not changed:
|
|
assert(counter == 11)
|
|
>>> void
|
|
|
|
## Lambda functions
|
|
|
|
Lambda expression is a block with optional argument list ending with `->`. If argument list is omitted,
|
|
the call arguments will be assigned to `it`:
|
|
|
|
lambda = {
|
|
it + "!"
|
|
}
|
|
assert( lambda is Callable)
|
|
assert( lambda("hello") == "hello!" )
|
|
void
|
|
|
|
### `it` assignment rules
|
|
|
|
When lambda is called with:
|
|
|
|
- no arguments: `it == void`
|
|
- exactly one argument: `it` will be assigned to it
|
|
- more than 1 argument: `it` will be a `List` with these arguments:
|
|
|
|
Here is an example:
|
|
|
|
val lambda = { it }
|
|
assert( lambda() == void )
|
|
assert( lambda("one") == "one")
|
|
assert( lambda("one", "two") == ["one", "two"])
|
|
>>> void
|
|
|
|
If you need to create _unnamed_ function, use alternative syntax (TBD, like { -> } ?)
|
|
|
|
### Declaring parameters
|
|
|
|
Parameter is a list of comma-separated names, with optional default value; last
|
|
one could be with ellipsis that means "the rest pf arguments as List":
|
|
|
|
assert( { a -> a }(10) == 10 )
|
|
assert( { a, b -> [a,b] }(1,2) == [1,2])
|
|
assert( { a, b=-1 -> [a,b] }(1) == [1,-1])
|
|
assert( { a, b...-> [a,...b] }(100) == [100])
|
|
// notice that splat syntax in array literal unrills
|
|
// ellipsis-caught arguments back:
|
|
assert( { a, b...-> [a,...b] }(100, 1, 2, 3) == [100, 1, 2, 3])
|
|
void
|
|
|
|
### Using lambda as the parameter
|
|
|
|
// note that fun returns its last calculated value,
|
|
// in our case, result after in-place addition:
|
|
fun mapValues(iterable, transform) {
|
|
var result = []
|
|
for( x in iterable ) result += transform(x)
|
|
}
|
|
assert( [11, 21, 31] == mapValues( [1,2,3], { it*10+1 }))
|
|
>>> void
|
|
|
|
### Auto last parameter
|
|
|
|
When the function call is follower by the `{` in the same line, e.g. lambda immediately
|
|
after function call, it is treated as a last argument to the call, e.g.:
|
|
|
|
fun mapValues(iterable, transform) {
|
|
var result = []
|
|
for( x in iterable ) result += transform(x)
|
|
}
|
|
val mapped = mapValues( [1,2,3]) {
|
|
it*10+1
|
|
}
|
|
assert( [11, 21, 31] == mapped)
|
|
>>> void
|
|
|
|
|
|
# Lists (aka arrays)
|
|
|
|
Lyng has built-in mutable array class `List` with simple literals:
|
|
|
|
[1, "two", 3.33].size
|
|
>>> 3
|
|
|
|
[List] is an implementation of the type `Array`, and through it `Collection` and [Iterable].
|
|
|
|
Lists can contain any type of objects, lists too:
|
|
|
|
val list = [1, [2, 3], 4]
|
|
assert( list is List ) // concrete implementatino
|
|
assert( list is Array ) // general interface
|
|
assert(list.size == 3)
|
|
// second element is a list too:
|
|
assert(list[1].size == 2)
|
|
>>> void
|
|
|
|
Notice usage of indexing. You can use negative indexes to offset from the end of the list; see more in [Lists](List.md).
|
|
|
|
When you want to "flatten" it to single array, you can use splat syntax:
|
|
|
|
[1, ...[2,3], 4]
|
|
>>> [1, 2, 3, 4]
|
|
|
|
Of course, you can splat from anything that is List (or list-like, but it will be defined later):
|
|
|
|
val a = ["one", "two"]
|
|
val b = [10.1, 20.2]
|
|
["start", ...b, ...a, "end"]
|
|
>>> ["start", 10.1, 20.2, "one", "two", "end"]
|
|
|
|
Of course, you can set any list element:
|
|
|
|
val a = [1, 2, 3]
|
|
a[1] = 200
|
|
a
|
|
>>> [1, 200, 3]
|
|
|
|
Lists are comparable, and it works well as long as their respective elements are:
|
|
|
|
assert( [1,2,3] == [1,2,3])
|
|
|
|
// but they are _different_ objects:
|
|
assert( [1,2,3] !== [1,2,3])
|
|
|
|
// when sizes are different, but common part is equal,
|
|
// longer is greater
|
|
assert( [1,2,3] > [1,2] )
|
|
|
|
// otherwise, where the common part is greater, the list is greater:
|
|
assert( [1,2,3] < [1,3] )
|
|
>>> void
|
|
|
|
The simplest way to concatenate lists is using `+` and `+=`:
|
|
|
|
// + works to concatenate iterables:
|
|
assert( [5, 4] + ["foo", 2] == [5, 4, "foo", 2])
|
|
var list = [1, 2]
|
|
|
|
// append allow adding iterables: all elements of it:
|
|
list += [2, 1]
|
|
// or you can append a single element:
|
|
list += "end"
|
|
assert( list == [1, 2, 2, 1, "end"])
|
|
>>> void
|
|
|
|
***Important note***: the pitfall of using `+=` is that you can't append in [Iterable] instance as an object: it will always add all its contents. Use `list.add` to add a single iterable instance:
|
|
|
|
var list = [1, 2]
|
|
val other = [3, 4]
|
|
|
|
// appending lists is clear:
|
|
list += other
|
|
assert( list == [1, 2, 3, 4] )
|
|
|
|
// but appending other Iterables could be confusing:
|
|
list += (10..12)
|
|
assert( list == [1, 2, 3, 4, 10, 11, 12])
|
|
>>> void
|
|
|
|
Use `list.add` to avoid confusion:
|
|
|
|
var list = [1, 2]
|
|
val other = [3, 4]
|
|
|
|
// appending lists is clear:
|
|
list.add(other)
|
|
assert( list == [1, 2, [3, 4]] )
|
|
|
|
// but appending other Iterables could be confusing:
|
|
list.add(10..12)
|
|
assert( list == [1, 2, [3, 4], (10..12)])
|
|
>>> void
|
|
|
|
|
|
To add elements to the list:
|
|
|
|
val x = [1,2]
|
|
x.add(3)
|
|
assert( x == [1,2,3])
|
|
// same as x += ["the", "end"] but faster:
|
|
x.add("the", "end")
|
|
assert( x == [1, 2, 3, "the", "end"])
|
|
>>> void
|
|
|
|
Self-modifying concatenation by `+=` also works (also with single elements):
|
|
|
|
val x = [1, 2]
|
|
x += [3, 4]
|
|
x += 5
|
|
assert( x == [1, 2, 3, 4, 5])
|
|
>>> void
|
|
|
|
You can insert elements at any position using `insertAt`:
|
|
|
|
val x = [1,2,3]
|
|
x.insertAt(1, "foo", "bar")
|
|
assert( x == [1, "foo", "bar", 2, 3])
|
|
>>> void
|
|
|
|
Using splat arguments can simplify inserting list in list:
|
|
|
|
val x = [1, 2, 3]
|
|
x.insertAt( 1, ...[0,100,0])
|
|
x
|
|
>>> [1, 0, 100, 0, 2, 3]
|
|
|
|
|
|
Note that to add to the end you still need to use `add` or positive index of the after-last element:
|
|
|
|
val x = [1,2,3]
|
|
x.insertAt(3, 10)
|
|
x
|
|
>>> [1, 2, 3, 10]
|
|
|
|
but it is much simpler, and we recommend to use '+='
|
|
|
|
val x = [1,2,3]
|
|
x += 10
|
|
>>> [1, 2, 3, 10]
|
|
|
|
## Removing list items
|
|
|
|
val x = [1, 2, 3, 4, 5]
|
|
x.removeAt(2)
|
|
assert( x == [1, 2, 4, 5])
|
|
// or remove range (start inclusive, end exclusive):
|
|
x.removeRange(1..2)
|
|
assert( x == [1, 5])
|
|
>>> void
|
|
|
|
There is a shortcut to remove the last elements:
|
|
|
|
val x = [1, 2, 3, 4, 5]
|
|
|
|
// remove last:
|
|
x.removeLast()
|
|
assert( x == [1, 2, 3, 4])
|
|
|
|
// remove 2 last:
|
|
x.removeLast(2)
|
|
assertEquals( [1, 2], x)
|
|
>>> void
|
|
|
|
You can get ranges to extract a portion from a list:
|
|
|
|
val list = [1, 2, 3, 4, 5]
|
|
assertEquals( [1,2,3], list[..2])
|
|
assertEquals( [1,2,], list[..<2])
|
|
assertEquals( [4,5], list[3..])
|
|
assertEquals( [2,3], list[1..2])
|
|
assertEquals( [2,3], list[1..<3])
|
|
>>> void
|
|
|
|
# Sets
|
|
|
|
Set are unordered collection of unique elements, see [Set]. Sets are [Iterable] but have no indexing access.
|
|
|
|
assertEquals( Set(3, 2, 1), Set( 1, 2, 3))
|
|
assert( 5 !in Set(1, 2, 6) )
|
|
>>> void
|
|
|
|
Please see [Set] for detailed description.
|
|
|
|
# Maps
|
|
|
|
Maps are unordered collection of key-value pairs, where keys are unique. See [Map] for details. Map also
|
|
are [Iterable]:
|
|
|
|
val m = Map( "foo" => 77, "bar" => "buzz" )
|
|
assertEquals( m["foo"], 77 )
|
|
>>> void
|
|
|
|
Please see [Map] reference for detailed description on using Maps.
|
|
|
|
|
|
# Flow control operators
|
|
|
|
## if-then-else
|
|
|
|
As everywhere else, and as expression:
|
|
|
|
val count = 11
|
|
if( count > 10 )
|
|
println("too much")
|
|
else {
|
|
// do something else
|
|
println("just "+count)
|
|
}
|
|
>>> too much
|
|
>>> void
|
|
|
|
Notice returned value `void`: it is because of `println` have no return value, e.g., `void`.
|
|
|
|
Or, more neat:
|
|
|
|
var count = 3
|
|
println( if( count > 10 ) "too much" else "just " + count )
|
|
>>> just 3
|
|
>>> void
|
|
|
|
## When
|
|
|
|
It is very much like the kotlin's:
|
|
|
|
fun type(x) {
|
|
when(x) {
|
|
in 'a'..'z', in 'A'..'Z' -> "letter"
|
|
in '0'..'9' -> "digit"
|
|
'$' -> "dollar"
|
|
"EUR" -> "crap"
|
|
in ['@', '#', '^'] -> "punctuation1"
|
|
in "*&.," -> "punctuation2"
|
|
else -> "unknown"
|
|
}
|
|
}
|
|
assertEquals("digit", type('3'))
|
|
assertEquals("dollar", type('$'))
|
|
assertEquals("crap", type("EUR"))
|
|
>>> void
|
|
|
|
Notice, several conditions can be grouped with a comma.
|
|
Also, you can check the type too:
|
|
|
|
fun type(x) {
|
|
when(x) {
|
|
"42", 42 -> "answer to the great question"
|
|
is Real, is Int -> "number"
|
|
is String -> {
|
|
for( d in x ) {
|
|
if( d !in '0'..'9' )
|
|
break "unknown"
|
|
}
|
|
else "number"
|
|
}
|
|
}
|
|
}
|
|
assertEquals("number", type(5))
|
|
assertEquals("number", type("153"))
|
|
assertEquals("number", type(π/2))
|
|
assertEquals("unknown", type("12%"))
|
|
assertEquals("answer to the great question", type(42))
|
|
assertEquals("answer to the great question", type("42"))
|
|
>>> void
|
|
|
|
### supported when conditions:
|
|
|
|
#### Contains:
|
|
|
|
You can thest that _when expression_ is _contained_, or not contained, in some object using `in container` and `!in container`. The container is any object that provides `contains` method, otherwise the runtime exception will be thrown.
|
|
|
|
Typical builtin types that are containers (e.g. support `conain`):
|
|
|
|
| class | notes |
|
|
|------------|--------------------------------------------|
|
|
| Collection | contains an element (1) |
|
|
| Array | faster maybe that Collection's |
|
|
| List | faster than Array's |
|
|
| String | character in string or substring in string |
|
|
| Range | object is included in the range (2) |
|
|
|
|
(1)
|
|
: Iterable is not the container as it can be infinite
|
|
|
|
(2)
|
|
: Depending on the inclusivity and open/closed range parameters. BE careful here: String range is allowed, but it is usually not what you expect of it:
|
|
|
|
assert( "more" in "a".."z") // string range ok
|
|
assert( 'x' !in "a".."z") // char in string range: probably error
|
|
assert( 'x' in 'a'..'z') // character range: ok
|
|
assert( "x" !in 'a'..'z') // string in character range: could be error
|
|
>>> void
|
|
|
|
So we recommend not to mix characters and string ranges; use `ch in str` that works
|
|
as expected:
|
|
|
|
"foo" in "foobar"
|
|
>>> true
|
|
|
|
and also character inclusion:
|
|
|
|
'o' in "foobar"
|
|
>>> true
|
|
|
|
## while
|
|
|
|
Regular pre-condition while loop, as expression, loop returns the last expression as everything else:
|
|
|
|
var count = 0
|
|
val result = while( count < 5 ) count++
|
|
result
|
|
>>> 4
|
|
|
|
Notice it _is 4 because when count became 5, the loop body was not executed!_.
|
|
|
|
We can break while as usual:
|
|
|
|
var count = 0
|
|
while( count < 5 ) {
|
|
if( count < 5 ) break
|
|
count = ++count * 10
|
|
}
|
|
>>> void
|
|
|
|
Why `void`? Because `break` drops out without the chute, not providing anything to return. Indeed, we should provide
|
|
exit value in the case:
|
|
|
|
var count = 0
|
|
while( count < 50 ) {
|
|
if( count > 3 ) break "too much"
|
|
count = ++count * 10
|
|
"wrong "+count
|
|
}
|
|
>>> "too much"
|
|
|
|
### Breaking nested loops
|
|
|
|
If you have several loops and want to exit not the inner one, use labels:
|
|
|
|
var count = 0
|
|
// notice the label:
|
|
outerLoop@ while( count < 5 ) {
|
|
var innerCount = 0
|
|
while( innerCount < 100 ) {
|
|
innerCount = innerCount + 1
|
|
|
|
if( innerCount == 5 && count == 2 )
|
|
// and here we break the labelled loop:
|
|
break@outerLoop "5/2 situation"
|
|
}
|
|
count = count + 1
|
|
count * 10
|
|
}
|
|
>>> "5/2 situation"
|
|
|
|
### and continue
|
|
|
|
We can skip the rest of the loop and restart it, as usual, with `continue` operator.
|
|
|
|
var count = 0
|
|
var countEven = 0
|
|
while( count < 10 ) {
|
|
count = count + 1
|
|
if( count % 2 == 1) continue
|
|
countEven = countEven + 1
|
|
}
|
|
"found even numbers: " + countEven
|
|
>>> "found even numbers: 5"
|
|
|
|
`continue` can't "return" anything: it just restarts the loop. It can use labeled loops to restart outer ones (we intentionally avoid using for loops here):
|
|
|
|
var count = 0
|
|
var total = 0
|
|
// notice the label:
|
|
outerLoop@ while( count++ < 5 ) {
|
|
var innerCount = 0
|
|
while( innerCount < 10 ) {
|
|
if( ++innerCount == 10 )
|
|
continue@outerLoop
|
|
}
|
|
// we don't reach it because continue above restarts our loop
|
|
total = total + 1
|
|
}
|
|
total
|
|
>>> 0
|
|
|
|
Notice that `total` remains 0 as the end of the outerLoop@ is not reachable: `continue` is always called and always make
|
|
Lyng to skip it.
|
|
|
|
## else statement
|
|
|
|
The while and for loops can be followed by the else block, which is executed when the loop
|
|
ends normally, without breaks. It allows override loop result value, for example,
|
|
to not calculate it in every iteration. For example, consider this naive prime number
|
|
test function (remember function return it's last expression result):
|
|
|
|
fun naive_is_prime(candidate) {
|
|
val x = if( candidate !is Int) candidate.toInt() else candidate
|
|
var divisor = 1
|
|
while( ++divisor < x/2 || divisor == 2 ) {
|
|
if( x % divisor == 0 ) break false
|
|
}
|
|
else true
|
|
}
|
|
assert( !naive_is_prime(16) )
|
|
assert( naive_is_prime(17) )
|
|
assert( naive_is_prime(3) )
|
|
assert( !naive_is_prime(4) )
|
|
>>> void
|
|
|
|
## Loop return value diagram
|
|
|
|
```mermaid
|
|
flowchart TD
|
|
S((start)) --> Cond{check}
|
|
Cond--false, no else--->V((void))
|
|
Cond--true-->E(["last = loop_body()" ])
|
|
E--break value---->BV((value))
|
|
E--> Check2{check}
|
|
E--break---->V
|
|
Check2 --false-->E
|
|
Check2 --true, no else--->L((last))
|
|
Check2 --true, else-->Else(["else_clause()"])
|
|
Cond--false, else--->Else
|
|
Else --> Ele4$nr((else))
|
|
```
|
|
|
|
So the returned value, as seen from diagram could be one of:
|
|
|
|
- `void`, if the loop was not executed, e.g. `condition` was initially false, and there was no `else` clause, or if the empty break was executed.
|
|
- value returned from `break value' statement
|
|
- value returned from the `else` clause, of the loop was not broken
|
|
- value returned from the last execution of loop body, if there was no `break` and no `else` clause.
|
|
|
|
## do-while loops
|
|
|
|
There works exactly as while loops but the body is executed prior to checking the while condition:
|
|
|
|
var i = 0
|
|
do { i++ } while( i < 1 )
|
|
i
|
|
>>> 1
|
|
|
|
The important feature of the do-while loop is that the condition expression is
|
|
evaluated on the body scope, e.g., variables, intruduced in the loop body are
|
|
available in the condition:
|
|
|
|
do {
|
|
var continueLoop = false
|
|
"OK"
|
|
} while( continueLoop )
|
|
>>> "OK"
|
|
|
|
This is sometimes convenient when condition is complex and has to be calculated inside the loop body. Notice the value returning by the loop:
|
|
|
|
fun readLine() { "done: result" }
|
|
val result = do {
|
|
val line = readLine()
|
|
} while( !line.startsWith("done:") )
|
|
result.drop(6)
|
|
>>> "result"
|
|
|
|
Suppose readLine() here reads some stream of lines.
|
|
|
|
|
|
## For loops
|
|
|
|
For loop are intended to traverse collections, and all other objects that supports
|
|
size and index access, like lists:
|
|
|
|
var letters = 0
|
|
for( w in ["hello", "wolrd"]) {
|
|
letters += w.length
|
|
}
|
|
"total letters: "+letters
|
|
>>> "total letters: 10"
|
|
|
|
For loop support breaks the same as while loops above:
|
|
|
|
fun search(haystack, needle) {
|
|
for(ch in haystack) {
|
|
if( ch == needle)
|
|
break "found"
|
|
}
|
|
else null
|
|
}
|
|
assert( search("hello", 'l') == "found")
|
|
assert( search("hello", 'z') == null)
|
|
>>> void
|
|
|
|
We can use labels too:
|
|
|
|
fun search(haystacks, needle) {
|
|
exit@ for( hs in haystacks ) {
|
|
for(ch in hs ) {
|
|
if( ch == needle)
|
|
break@exit "found"
|
|
}
|
|
}
|
|
else null
|
|
}
|
|
assert( search(["hello", "world"], 'l') == "found")
|
|
assert( search(["hello", "world"], 'z') == null)
|
|
>>> void
|
|
|
|
# Exception handling
|
|
|
|
Very much like in Kotlin. Try block returns its body block result, if no exception was cauht, or the result from the catch block that caught the exception:
|
|
|
|
var error = "not caught"
|
|
var finallyCaught = false
|
|
val result = try {
|
|
throw IllegalArgumentException()
|
|
"invalid"
|
|
}
|
|
catch(nd: SymbolNotDefinedException) {
|
|
error = "bad catch"
|
|
}
|
|
catch(x: IllegalArgumentException) {
|
|
error = "no error"
|
|
"OK"
|
|
}
|
|
finally {
|
|
// finally does not affect returned value
|
|
"too bad"
|
|
}
|
|
assertEquals( "no error", error)
|
|
assertEquals( "OK", result)
|
|
>>> void
|
|
|
|
There is shorter form of catch block when you want to catch any exception:
|
|
|
|
var caught = null
|
|
try {
|
|
throw IllegalArgumentException()
|
|
}
|
|
catch(t) { // same as catch(t: Exception), but simpler
|
|
caught = t
|
|
}
|
|
assert( caught is IllegalArgumentException )
|
|
>>> void
|
|
|
|
And even shortest, for the Lying lang tradition, missing var is `it`:
|
|
|
|
var caught = null
|
|
try {
|
|
throw IllegalArgumentException()
|
|
}
|
|
catch { // same as catch(it: Exception), but simpler
|
|
caught = it
|
|
}
|
|
assert( caught is IllegalArgumentException )
|
|
>>> void
|
|
|
|
It is possible to catch several exceptions in the same block too, use
|
|
`catch( varName: ExceptionClass1, ExceptionClass2)`, etc, use short form of throw and
|
|
many more.
|
|
|
|
- see [exception handling](exceptions_handling.md) for detailed exceptions tutorial and reference.
|
|
|
|
|
|
# Self-assignments in expression
|
|
|
|
There are auto-increments and auto-decrements:
|
|
|
|
var counter = 0
|
|
assert(counter++ * 100 == 0)
|
|
assert(counter == 1)
|
|
>>> void
|
|
|
|
but:
|
|
|
|
var counter = 0
|
|
assert( ++counter * 100 == 100)
|
|
assert(counter == 1)
|
|
>>> void
|
|
|
|
The same with `--`:
|
|
|
|
var count = 100
|
|
var sum = 0
|
|
while( count > 0 ) sum = sum + count--
|
|
sum
|
|
>>> 5050
|
|
|
|
There are self-assigning version for operators too:
|
|
|
|
var count = 100
|
|
var sum = 0
|
|
while( count > 0 ) sum += count--
|
|
sum
|
|
>>> 5050
|
|
|
|
# Ranges
|
|
|
|
Ranges are convenient to represent the interval between two values:
|
|
|
|
5 in (0..100)
|
|
>>> true
|
|
|
|
It could be open and closed:
|
|
|
|
assert( 5 in (1..5) )
|
|
assert( 5 !in (1..<5) )
|
|
>>> void
|
|
|
|
Ranges could be inside other ranges:
|
|
|
|
assert( (2..3) in (1..10) )
|
|
>>> void
|
|
|
|
There are character ranges too:
|
|
|
|
'd' in 'a'..'e'
|
|
>>> true
|
|
|
|
and you can use ranges in for-loops:
|
|
|
|
for( x in 'a' ..< 'c' ) println(x)
|
|
>>> a
|
|
>>> b
|
|
>>> void
|
|
|
|
See [Ranges](Range.md) for detailed documentation on it.
|
|
|
|
# Comments
|
|
|
|
// single line comment
|
|
var result = null // here we will store the result
|
|
>>> null
|
|
|
|
# Integral data types
|
|
|
|
| type | description | literal samples |
|
|
|--------|---------------------------------|---------------------|
|
|
| Int | 64 bit signed | `1` `-22` `0x1FF` |
|
|
| Real | 64 bit double | `1.0`, `2e-11` |
|
|
| Bool | boolean | `true` `false` |
|
|
| Char | single unicode character | `'S'`, `'\n'` |
|
|
| String | unicode string, no limits | "hello" (see below) |
|
|
| List | mutable list | [1, "two", 3] |
|
|
| Void | no value could exist, singleton | void |
|
|
| Null | missing value, singleton | null |
|
|
| Fn | callable type | |
|
|
|
|
See also [math operations](math.md)
|
|
|
|
## Character details
|
|
|
|
The type for the character objects is `Char`.
|
|
|
|
### Char literal escapes
|
|
|
|
Are the same as in string literals with little difference:
|
|
|
|
| escape | ASCII value |
|
|
|--------|-------------------|
|
|
| \n | 0x10, newline |
|
|
| \t | 0x07, tabulation |
|
|
| \\ | \ slash character |
|
|
| \' | ' apostrophe |
|
|
|
|
### Char instance members
|
|
|
|
assert( 'a'.code == 0x61 )
|
|
>>> void
|
|
|
|
| member | type | meaning |
|
|
|--------|------|--------------------------------|
|
|
| code | Int | Unicode code for the character |
|
|
| | | |
|
|
|
|
|
|
## String details
|
|
|
|
Strings are arrays of Unicode characters. It can be indexed, and indexing will
|
|
return a valid Unicode character at position. No utf hassle:
|
|
|
|
"Парашют"[5]
|
|
>>> 'ю'
|
|
|
|
Its length is, of course, in characters:
|
|
|
|
"разум".length
|
|
>>> 5
|
|
|
|
And supports growing set of kotlin-borrowed operations, see below, for example:
|
|
|
|
assertEquals("Hell", "Hello".dropLast(1))
|
|
>>> void
|
|
|
|
To format a string use sprintf-style modifiers like:
|
|
|
|
val a = "hello"
|
|
val b = 11
|
|
assertEquals( "hello:11", "%s:%d"(a, b) )
|
|
assertEquals( " hello: 11", "%6s:%6d"(a, b) )
|
|
assertEquals( "hello :11 ", "%-6s:%-6d"(a, b) )
|
|
>>> void
|
|
|
|
List of format specifiers closely resembles C sprintf() one. See [format specifiers](https://github.com/sergeych/mp_stools?tab=readme-ov-file#sprintf-syntax-summary), this is doe using [mp_stools kotlin multiplatform library](https://github.com/sergeych/mp_stools). Currently supported Lyng types are `String`, `Int`, `Real`, `Bool`, the rest are displayed using their `toString()` representation.
|
|
|
|
This list will be extended.
|
|
|
|
To get the substring use:
|
|
|
|
assertEquals("pult", "catapult".takeLast(4))
|
|
assertEquals("cat", "catapult".take(3))
|
|
assertEquals("cat", "catapult".dropLast(5))
|
|
assertEquals("pult", "catapult".drop(4))
|
|
>>> void
|
|
|
|
And to get a portion you can slice it with range as the index:
|
|
|
|
assertEquals( "tap", "catapult"[ 2 .. 4 ])
|
|
assertEquals( "tap", "catapult"[ 2 ..< 5 ])
|
|
>>> void
|
|
|
|
Open-ended ranges could be used to get start and end too:
|
|
|
|
assertEquals( "cat", "catapult"[ ..< 3 ])
|
|
assertEquals( "cat", "catapult"[ .. 2 ])
|
|
assertEquals( "pult", "catapult"[ 4.. ])
|
|
>>> void
|
|
|
|
### String operations
|
|
|
|
Concatenation is a `+`: `"hello " + name` works as expected. No confusion.
|
|
|
|
Typical set of String functions includes:
|
|
|
|
| fun/prop | description / notes |
|
|
|--------------------|------------------------------------------------------------|
|
|
| lower() | change case to unicode upper |
|
|
| upper() | change case to unicode lower |
|
|
| startsWith(prefix) | true if starts with a prefix |
|
|
| endsWith(prefix) | true if ends with a prefix |
|
|
| take(n) | get a new string from up to n first characters |
|
|
| takeLast(n) | get a new string from up to n last characters |
|
|
| drop(n) | get a new string dropping n first chars, or empty string |
|
|
| dropLast(n) | get a new string dropping n last chars, or empty string |
|
|
| size | size in characters like `length` because String is [Array] |
|
|
| (args...) | sprintf-like formatting, see [string formatting] |
|
|
| [index] | character at index |
|
|
| [Range] | substring at range |
|
|
| s1 + s2 | concatenation |
|
|
| s1 += s2 | self-modifying concatenation |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### Literals
|
|
|
|
String literal could be multiline:
|
|
|
|
"Hello
|
|
World"
|
|
|
|
though multiline literals is yet work in progress.
|
|
|
|
# Built-in functions
|
|
|
|
See [math functions](math.md). Other general purpose functions are:
|
|
|
|
| name | description |
|
|
|----------------------------------------------|----------------------------------------------------------|
|
|
| assert(condition,message="assertion failed") | runtime code check. There will be an option to skip them |
|
|
| println(args...) | Open for overriding, it prints to stdout. |
|
|
|
|
# Built-in constants
|
|
|
|
| name | description |
|
|
|-------------------------------------|------------------------------|
|
|
| Real, Int, List, String, List, Bool | Class types for real numbers |
|
|
| π | See [math](math.md) |
|
|
|
|
[List]: List.md
|
|
[Iterable]: Iterable.md
|
|
[Iterator]: Iterator.md
|
|
[Real]: Real.md
|
|
[Range]: Range.md
|
|
[String]: String.md
|
|
[string formatting]: https://github.com/sergeych/mp_stools?tab=readme-ov-file#sprintf-syntax-summary
|
|
[Set]: Set.md
|
|
[Map]: Map.md |