lyng/docs/tutorial.md

34 KiB

Table of Contents

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:

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

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

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.

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

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, this is doe using mp_stools kotlin multiplatform library. 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. 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