Skip to content

Samples

K Lange edited this page Feb 9, 2021 · 1 revision

Code Examples

NOTE: Due to limitations with Github's markdown renderer, these snippets will be highlighted as Python code.

These examples are also available in an interactive tutorial.

Hello World

print("Hello, world!")
# → Hello, world!

Multiple expressions can be supplied to print and will be concatenated with spaces:

print("Hello", 42, "!")
# → Hello 42 !

The string used to combine arguments can be changed with sep=.

print("Hello", 42, "!", sep="...")
# → Hello...42...!

The string printed after all arguments have been printed can be changed with end=:

print("Hello",end=" ")
print("World")
# → Hello World

Note: When using the REPL with the rich line editor enabled, the line editor will automatically add a line feed when displaying the prompt if the previous output did not include one, and will display a gray left-facing triangle to indicate this.

Basic Types

Kuroko's basic types are integers (which use the platform long type), double-precision floats, booleans (True and False), and None.

print(1 + 2 + 3)
# → 6

When integer values are used in arithmetic operations, such as division, the result will be an integer as well:

print(1 / 2)
# → 0

To get floating-point results, one of the arguments should be explicitly typed or converted:

print(1 / 2.0)
# → 0.5

Implicit type conversion occurs late in evaluation, so be careful of integer overflows:

# Probably not what you want:
print(1000000000 * 1000000000 * 1000000000 * 3.0)
# → -2.07927e+19
# Try something like this instead:
print(1000000000.0 * 1000000000 * 1000000000 * 3.0)
# → 3e+27

Objects

Objects are values which live on the heap. Basic objects include strings, functions, classes, and instances.

Objects are passed by reference, though strings are immutable so this property is only relevant for other object types.

Strings

Strings can be defined with single, double, or Python-style triple quotes, the latter of which allows for unescaped line feeds to appear in strings.

The following escape sequences can be embedded in string literals:

  • \a: bell
  • \b: backspace
  • \f: formfeed
  • \n: linefeed
  • \r: carriage return
  • \t: horizontal tab
  • \v: vertical tab
  • \[: ANSI escape value (decimal value 27)
  • \x: A two-character hexadecimal sequence; represents a byte value in a bytes object and a codepoint below U+0100 in strings.
  • \u: A four-character hexadecimal sequence; represents a codepoint below U+10000 in strings.

Strings in Kuroko are immutable; they can not be modified in-place.

String concatenate uses the + operator and produces a new string. Other values are converted to strings when added to a string:

print("Hello, " + 42 + "!")
# → Hello, 42!

Strings can also be subscripted to extra individual units:

print("Hello"[1])
# → e

Much like in Python 3, strings in Kuroko represent sequences of non-normalized Unicode codepoints. Both source files and the terminal in which Kuroko is running are expected to be UTF-8.

This means that when subscrpting into a Unicode string, individual codepoints should be expected:

print("日本語"[1])
# → 本
print("日本語"[2])
# → 語

The length of a Unicode string is also represented in codepoints:

print(len("日本語"))
# → 3

The ord() function on a string representing a single codepoint will return the integer representation of that codepoint:

print(ord("本"))
# → 26412
print(ord("t"))
# → 116

Invalid UTF-8 sequences will most likely result in a ValueError during decoding or parsing.

Implementation Note: Generally, the internal representation of strings is their UTF-8 encoded form. When a subscript or slicing operation happens in which a codepoint index needs to be converted to an offset in the string, the most appropriate 'canonical' format will be generated and remain with the interned string until it is garbage collected. For strings containing only ASCII characters, no conversion is done and no additional copy is created. For all other strings, the smallest possible size for representing the largest codepoint is used, among the options of 1, 2, or 4. This approach is similar to CPython post-3.9.

Strings can also be encoded to bytes objects to get their UTF-8 representation:

print('テスト'.encode())
# → b'\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88'

Bytes objects can also be written as literals in the same format. Note that strings and bytes are not generally compatible with each other, so comparisons, concatenation, and so on will typically fail or raise exceptions.

print(b'test')
# → b'test'

When indexing into a bytes object, values are returned as positive integers.

print(b'test'[0])
# → 116

A Note on Unicode

Kuroko strings are sequences of Unicode codepoints without normalization applied. This means that while é and é may look the same in your terminal or text editor, they are different strings in Kuroko - the latter of the two is constructed from the codepoint for the letter "e" and the combining character for the acute accent.

print('é'.encode())
# → b'\xc3/\xa9'
print('é'.encode())
# → b'e\xcc\x81'

Additionally, it means that the latter is two codepoints and thus also a string of length 2.

print(len('é'))
# → 1
print(len('é'))
# → 2

Unicode normalization and combining characters are complicated topics requiring library support and Kuroko does not bake them into the language semantics.

Note: A module to provide access to information from ICU, including string normalization and character types, is planned.

Variables

In a departure from Python, Kuroko has explicit variable declaration and traditional scoping rules. Variables are declared with the let keyword and take the value None if not defined at declaration time:

let foo
print(foo)
# → None
foo = 1
print(foo)
# → 1

You may declare and define multiple variables on a single line as follows:

let a, b, c = 1, "test", object()
print(a,b,c)
# → 1 test <instance of object at ...>

The let statement can also be used to unpack some sequence types:

let t = (1, 2, 3)
let a, b, c = t
print(a,b,c)
# → 1 2 3

Note: Identifier names, including for variables, functions, and classes, can be Unicode sequences. All non-ASCII codepoints are accepted as identifier characters.

Assignments

After a variable is declared, assignments to it are valid as both expressions and statements. Kuroko provides assignment shortcuts like += and -= as well as C-style postfix increment (++) and decrement (--).

let x = 1
print(x++)
# → 2
print(x -= 7)
# → -5
print((x = 42))
# → 42

Note: var= is used for keyword arguments, so be careful if you are trying to pass an assignment as a value to a function - wrap it in parentheses first or you may not get what you wanted.

Functions

Function syntax is essentially the same as in Python:

def greet(name):
    print("Hello, " + name + "!")
greet("user")
# → Hello, user!

Default arguments can be specified as follows:

def greet(name="world"):
    print("Hello, " + name + "!")
greet()
greet("user")
# → Hello, world!
#   Hello, user!

If a default argument value is not provided, the expression assigned to it will be evaluated as if it were at the top of the body of the function. Note that this behavior intentionally differs from Python, where default values are calculated once when the function is defined; assigning a mutable object, such as a list, as a default value will create a new list with each invocation of the function in Kuroko, rather than re-using the same list. If you would like to have behavior like in Python, define the value outside of the function:

let l = []
def pythonishDefaultList(arg, values=l):
    l.append(arg)
    print("I've seen the following:",values)
pythonishDefaultList("hello")
pythonishDefaultList("world")
# → I've seen the following: ['hello']
#   I've seen the following: ['hello', 'world']

Blocks, including function def blocks and control flow structures like if and for, must be indented with spaces to a level greater than the enclosing block.

You may indent blocks to whatever level you desire, so long as ordering remains consistent, though the recommended indentation size is 4 spaces.

It is recommended that you use an editor which provides a clear visual distinction between tabs and spaces, such as Bim.

Blocks can also accept a single inline statement:

if True: print("The first rule of Tautology Club is the first rule of Tautology Club.")
# → The first rule of Tautology Club is the first rule of Tautology Club.

Closures

Functions in Kuroko are inherently closures and capture local variables from their enclosing scopes.

When a function references a local from another function in which its definition is nested (or variables declared within a block), the referenced variables will continue to "live" in the heap beyond the execution of their original scope context.

If we define a function which declares a local variable and then define an inner function which references that variable, such as in the example below, each call to the other function will create a new instance of the variable and a new instance of the inner function. When the inner function is returned, it will take with it the variable it captured from the outer function and further calls to this instance of the inner function will use that variable.

def foo():
    let i = 1 # Local to this call to foo()
    def bar():
        print(i) # Reference to outer variable
        i = i + 1
    return bar # Produces a closure
let a = foo() # Each copy of `bar` gets its own `i`
let b = foo()
let c = foo()
a() # So these all print "1" as the first call,
b() # but each one also increments its own copy of i
c()
a() # So further calls will reference that copy
a()
a()
# → 1
#   1
#   1
#   2
#   3
#   4

Lambda Functions

Lambda functions allow for the creation of simple functions anonymously. Note that the body of a lambda is an expression, not a list of statements.

let myLambda = lambda x: (x * 5)
print(myLambda(1))
print(myLambda(2))
print(myLambda(3))
# → 5
#   10
#   15

Creating a lambda and assigning it immediately to a name is not all that useful, but lambdas can be used where-ever an expression is expected.

Basic Objects and Classes

Objects and classes in Kuroko work a lot like Python or similar languages in that they have an arbitrary and mutable set of fields, which may be methods or other values.

To create a basic object without methods, the object class is provided:

let o = object()
o.foo = "bar"
print(o.foo)
# → bar

To supply methods, define a class:

class Foo():
    def printFoo():
        print(self.foo)
let o = Foo()
o.foo = "bar"
o.printFoo()
# → bar

The self keyword is implicit in all methods and does not need to be supplied in the argument list. You may optionally include it in the method declaration anyway, for compatibility with Python:

class Foo():
    def printFoo(self):
        print(self.foo)
let o = Foo()
o.foo = "bar"
o.printFoo()
# → bar

Note: As self is implicit, it can not be renamed; other argument names listed in a method signature will refer to additional arguments.

Classes can also define fields, which can be accessed from the class or through an instance.

class Foo():
    bar = "baz"
    def printBar(self):
        print(self.bar)
let o = Foo()
o.printBar()
# → baz
print(Foo.bar)
# → baz

Note: Instances receive a copy of their class's fields upon creation. If a class field is mutable, the instance's copy will refer to the same underlying object. Assignments to the instance's copy of a field will refer to a new object. If a new field is added to a class after instances have been created, the existing instances will not be able to reference the new field.

When a class is instantiated, if it has an __init__ method it will be called automatically. __init__ may take arguments as well.

class Foo():
    def __init__(bar):
        self.foo = bar
    def printFoo(self):
        print(self.foo)
let o = Foo("bar")
o.printFoo()
# → bar

Some other special method names include __get__, __set__, and __str__, which will be explained later.

Note: As in Python, all values are objects, but internally within the Kuroko VM not all values are instances. The difference is not very relevant to user code, but if you are embedding Kuroko it is important to understand.

Inheritance

Classes may inherit from a single super class:

class Foo():
    def __init__():
        self.type = "foo"
    def printType():
        print(self.type)

class Bar(Foo):
    def __init__():
        self.type = "bar"

let bar = Bar()
bar.printType()
# → bar

Methods can refer to the super class with the super keyword, which should be called as a function with no arguments, as in new-style Python code:

class Foo():
    def __init__():
        self.type = "foo"
    def printType():
        print(self.type)

class Bar(Foo):
    def __init__():
        self.type = "bar"
    def printType():
        super().printType()
        print("Also, I enjoy long walks on the beach.")

let bar = Bar()
bar.printType()
# → bar
#   Also, I enjoy long walks on the beach.

You can determine the type of an object at runtime with the type function:

class Foo():
let foo = Foo()
print(type(foo))
# → <type 'Foo'>

You can also determine if an object is an instance of a given type, either directly or through its inheritance chain, with the isinstance function:

class Foo:
class Bar:
class Baz(Bar):
let b = Baz()
print(isinstance(b,Baz), isinstance(b,Bar), isinstance(b,Foo), isinstance(b,object))
# → True, True, False, True

All classes eventually inherit from the base class object, which provides default implementations of some special instance methods.

Collections

Kuroko has built-in classes for flexible arrays (list) and hashmaps (dict), as well as immutable lists of items (tuple).

let l = list()
l.append(1)
l.append(2)
l.append("three")
l.append(False)
print(l)
# → [1, 2, three, False]
l[1] = 5
print(l)
# → [1, 5, three, False]
let d = dict()
d["foo"] = "bar"
d[1] = 2
print(d)
# → {1: 2, foo: bar}

These built-in collections can also be initialized as expressions, which act as syntactic sugar for the listOf and dictOf built-in functions:

let l = [1,2,"three",False] # or listOf(1,2,"three",False)
print(l)
# → [1, 2, three, False]
let d = {"foo": "bar", 1: 2} # or dictOf("foo","bar",1,2)
print(d)
# → {1: 2, foo: bar}

Tuples provide similar functionality to lists, but are intended to be immutable:

let t = (1,2,3)
print(t)
# → (1, 2, 3)

A set type is also provided:

let s = set([1,2,3])
print(s)
# → {1, 2, 3}
s.add(2)
print(s) # No change
# → {1, 2, 3}
s.add(4)
print(s)
# → {1, 2, 3, 4}

Lists, dicts, and tuples can also be generated dynamically through comprehensions, just as in Python:

let fives = [x * 5 for x in [1,2,3,4,5]]
print(fives)
# → [5, 10, 15, 20, 25]
let d = {'a': 1, 'b': 2, 'c': 3}
let dInverted = {v: k for k, v in d.items()}
print(d,dInverted)
# → {'a': 1, 'b': 2, 'c': 3} {1: 'a', 2: 'b', 3: 'c'}

Exceptions

Kuroko provides a mechanism for handling errors at runtime. If an error is not caught, the interpreter will end and print a traceback.

def foo(bar):
    print("I expect an argument! " + bar)
foo() # I didn't provide one!
# → Traceback, most recent first, 1 call frame:
#     File "<stdin>", line 1, in <module>
#   ArgumentError: foo() takes exactly 1 argument (0 given)

When using the repl, global state will remain after an exception and the prompt will be displayed again.

To catch exceptions, use try/except:

def foo(bar):
    print("I expect an argument! " + bar)
try:
    foo() # I didn't provide one!
except:
    print("oh no!")
# → oh no!

Runtime exceptions are passed to the except block as a special variable exception. Exceptions from the VM are instances of built-in error classes with an attribute arg:

def foo(bar):
    print("I expect an argument! " + bar)
try:
    foo() # I didn't provide one!
except:
    print("oh no, there was an exception:", exception.arg)
# → oh no, there was an exception: foo() takes exactly 1 argument (0 given)

Exceptions can be generated with the raise statement. When raising an exception, the value can be anything, but subclasses of __builtins__.Exception are recommended.

def login(password):
    if password != "supersecret":
        raise ValueError("Wrong password, try again!")
    print("[Hacker voice] I'm in.")
login("foo")
# → Traceback, most recent first, 2 call frames:
#     File "<stdin>", line 5, in <module>
#     File "<stdin>", line 3, in login
#   ValueError: Wrong password, try again!

The except block is optional, and an exception may be caught and ignored.

def login(password):
    if password != "supersecret":
        raise ValueError("Wrong password, try again!")
    print("[Hacker voice] I'm in.")
try:
    login("foo")
# (no output)

try/except blocks can also be nested within each other. The deepest try block will be used to handle an exception. If its except block calls raise, the exception will filter up to the next try block. Either the original exception or a new exception can be raised.

try:
    print("Level one")
    try:
        print("Level two")
        raise ValueError("Thrown exception")
    except:
        print("Caught in level two")
except:
    print("Not caught in level one!")
# → Level one
#   Level two
#   Caught in level two
try:
    print("Level one")
    try:
        print("Level two")
        raise ValueError("Thrown exception")
    except:
        print("Caught in level two")
        raise exception
except:
    print("Caught in level one!")
# → Level one
#   Level two
#   Caught in level two
#   Caught in level one!

Modules

Modules in Kuroko work much like in Python, allowing scripts to call other scripts and use functions defined in other files.

# modules/demomodule.krk
foo = "bar"
# demo.krk
import demomodule
print(demomodule.foo)
# → bar

Modules are run once and then cached, so if they preform actions like printing or complex computation this will happen once when first imported. The globals table from the module is the fields table of an object. Further imports of the same module will return the same object.

When importing a module, the names of members which should be imported can be specified and can be renamed:

from demomodule import foo
print(foo)
# → bar
from demomodule import foo as imported
print(imported)
# → bar

Note: When individual names are imported from a module, they refer to the same object, but if new assignments are made to the name it will not affect the original module. If you need to replace values defined in a module, always be sure to refer to it by its full name.

Modules can also come in the form of packages. Packages are modules that contain other modules. To make a package, create a directory in one of the module import paths with the name of your package and place a file named __init__.krk in that directory. This file will be run when the package is imported, but if you only want to use packages for namespacing it does not need to have any content.

Say we have a directory tree as follows:

modules/
    foo/
        __init__.krk
        bar/
            __init__.krk
            baz.krk

With this directory tree, we can import foo, import foo.bar, or import foo.bar.baz.

When a module within a package is imported directly, as in import foo.bar.baz, its parent packages are imported in order and the interpreter ensures each has an attribute pointing to the next child. After the import statement, the top-level package will be bound in the current scope:

import foo.bar.baz
print(foo)
print(foo.bar)
print(foo.bar.baz)
# → <module 'foo' from './modules/foo/__init__.krk'>
#   <module 'foo.bar' from './modules/foo/bar/__init__.krk'>
#   <module 'foo.bar.baz' from './modules/foo/bar/baz.krk'>

If we want to get at the module baz we can use import ... as ... to bind it to a name instead:

import foo.bar.baz as baz
print(baz)
try:
    print(foo) # NameError
except:
    print(repr(exception))
# → <module 'foo.bar.baz' from './modules/foo/bar/baz.krk'>
#   NameError: Undefined variable 'foo'.

Note that only module names can be specified as the first argument to import or from, and that if a module within a package has never been imported it will not be available from its package.

If we define something in modules/foo/bar/baz.krk we can access it either by its full name or through a from import:

# modules/foo/bar/baz.krk
let qux = "hello, world"
import foo.bar.baz
print(foo.bar.baz.qux)
from foo.bar.baz import qux
print(qux)
# → hello, world
#   hello, world

When using from ... import, the imported name can be a module, package, or regular member of the module before the import. Multiple names can be imported at once, but only one level can be imported:

# modules/foo/bar/baz.krk
let qux = "hello, world"
let quux = 42
# This is a syntax error.
#from foo.bar import baz.qux
from foo.bar.baz import qux, quux
print(qux,quux)
# → hello, world 42

Loops

Kuroko supports C-style for loops, while loops, and Python-style iterator for loops.

for i = 1; i < 5; i++:
    print(i)
# → 1
#   2
#   3
#   4
let i = 36
while i > 1:
    i = i / 2
    print(i)
# → 18
#   9
#   4
#   2
#   1
let l = [1,2,3]
for i in l:
    print(i)
# → 1
#   2
#   3

If an iterator returns tuples, the values can be unpacked into multiple variables:

let l = [(1,2),(3,4),(5,6)]
for left, right in l:
    print(left, right)
# → 1 2
#   3 4
#   5 6

An exception will be raised if a tuple returned by the iterator has the wrong size for unpacking (ValueError), or if a value returned is not a tuple (TypeError).

Iterators

The special method __iter__ should return an iterator. An iterator should be a function which increments an internal state and returns the next value. If there are no values remaining, return the iterator object itself.

Note: The implementation of iterators in Kuroko differs from Python. In Python, iterator objects have a __next__ method while in Kuroko they are called as functions or using the __call__ method on instances, which allows iterators to be implemented as simple closures. In Python, completed iterators raise an exception called StopIteration while Kuroko iterators are expected to return themselves when they are done.

An example of an iterator is the range built-in class, which was previously defined like this:

class range:
    "Helpful iterable."
    def __init__(self, min, max):
        self.min = min
        self.max = max
    def __iter__(self):
        let i=self.min
        let l=self.max
        def _():
            if i>=l:
                return _
            let o=i
            i++
            return o
        return _

Objects which have an __iter__ method can then be used with the for ... in ... syntax:

for i in range(1,5):
    print(i)
# → 1
#   2
#   3
#   4

As in the Loops section above, an iterator may return a series of tuples which may be unpacked in a loop. Tuple unpacking is optional; if the loop uses a single variable, the tuple will be assigned to it with each iteration.

class TupleGenerator:
    def __iter__():
        let up, down, limit = 0, 0, 5
        def _():
            if limit-- == 0: return _
            return (up++,down--)
        return _
for i in TupleGenerator():
    print(i)
# → (1, -1)
#   (2, -2)
#   (3, -3)
#   (4, -4)
for up, down in TupleGenerator():
    print(up,down)
# → 1 -1
#   2 -2
#   3 -3
#   4 -4

Indexing

Objects with __get__ and __set__ methods can be used with square brackets []:

class Foo:
    def __get__(ind):
        print("You asked for ind=" + ind)
        return ind * 5
    def __set__(ind, val):
        print("You asked to set ind=" + ind + " to " + val)
let f = Foo()
print(f[4])
f[7] = "bar"
# → You asked for ind=4
#   20
#   You asked to set ind=7 to bar

Slicing

Substrings can be extracted from strings via slicing:

print("Hello world!"[3:8])
# → lo wo
print("Hello world!"[:-1])
# → Hello world
print("Hello world!"[-1:])
# → !

NOTE: Step values are not yet supported.

Lists can also be sliced:

print([1,2,3,4,5,6][3:])
# → [4, 5, 6]

String Conversion

If an object implements the __str__ method, it will be called to produce string values through casting or printing.

class Foo:
    def __str__():
        return "(I am a Foo!)"
let f = Foo()
print(f)
# → (I am a Foo!)

The __repr__ method serves a similar purpose and is used when the REPL displays values or when they are used in string representations of collections. The implementations of __str__ and __repr__ can be different:

class Foo:
    def __str__():
        return "What is a Foo but a miserable pile of methods?"
    def __repr__():
        return "[Foo instance]"
let f = Foo()
print(f)
print([f,f,f])
# → What is a Foo but a miserable pile of methods?
#   [[Foo instance], [Foo instance], [Foo instance]]

As in Python, __repr__ is intended to provide a canonical string representation which, if possible, should be usable to recreate the object.

Note: As all classes eventually inherit from object and object implements both __str__ and __repr__, these methods should always be available.

File I/O

The module fileio provides an interface for opening, reading, and writing files, including stdin/stdout/stderr.

To open and read the contents of a file:

import fileio
let f = fileio.open("README.md","r")
print(f.read())
f.close()

To write to stdout (notably, without automatic line feeds):

import fileio
fileio.stdout.write("hello, world")

To read lines from stdin:

import fileio

while True:
    fileio.stdout.write("Say something: ")
    fileio.stdout.flush()

    let data = fileio.stdin.readline()
    if data[-1] == '\n':
        data = data[:-1]
    if data == "exit":
        break
    print("You said '" + data + "'!")

Decorators

Decorators allow functions and methods to be wrapped.

def decorator(func):
    print("I take the function to be decorated as an argument:", func)
    def wrapper():
        print("And I am the wrapper.")
        func()
        print("Returned from wrapped function.")
    return wrapper

@decorator
def wrappedFunction():
    print("Hello, world")

wrappedFunction()
# → I take a function to be decorated as an argument: <function wrappedFunction>
#   And I am the wrapper.
#   Hello, world
#   Returned from wrapped function

The inner wrapper function is not necessary if all the work of the decorator can be done when the function is defined:

def registerCallback(func):
    print("Registering callback function",func)
    return func

@registerCallback
def aFunction():
    print("Hello, world!")

aFunction()
# → Registering callback function <function aFunction>
#   Hello, world!

Method wrappers work similarly, though be sure to explicitly provide a name (other than self) for the object instance:

def methodDecorator(method):
    def methodWrapper(instance, anExtraArgument):
        method(instance)
        print("I also required this extra argument:", anExtraArgument)
    return methodWrapper

class Foo():
    @methodDecorator
    def theMethod():
        print("I am a method, so I can obviously access", self)
        print("And I also didn't take any arguments, but my wrapper did:")

let f = Foo()
f.theMethod("the newly required argument")
# → I am a method, so I can obviously access <instance of Foo at ...>
#   And I also didn't take any arguments, but my wrapper did:
#   I also required this extra argument: the newly required argument

Decorators are expressions, just like in Python, so to make a decorator with arguments create a function that takes those arguments and returns a decorator:

def requirePassword(password):
    print("I am creating a decorator.")
    def decorator(func):
        print("I am wrapping", func, "and attaching",password)
        def wrapper(secretPassword):
            if secretPassword != password:
                print("You didn't say the magic word.")
                return
            func()
        return wrapper
    return decorator

@requirePassword("hunter2")
def superSecretFunction():
    print("Welcome!")

superSecretFunction("a wrong password")
print("Let's try again.")
superSecretFunction("hunter2")
# → I am wrapping <function superSecretFunction> and attaching hunter2
#   You didn't say the magic word.
#   Let's try again.
#   Welcome!

Keyword Arguments

Arguments may be passed to a function by specifying their name instead of using their positional location.

def aFunction(a,b,c):
    print(a,b,c)

aFunction(1,2,3)
aFunction(1,c=3,b=2)
aFunction(b=2,c=3,a=1)
# → 1 2 3
#   1 2 3
#   1 2 3

This will be slower in execution than a normal function call, as the interpreter will need to figure out where to place arguments in the requested function by examining it at runtime, but it allows for functions to take many default arguments without forcing the caller to specify the values for everything leading up to one they want to specifically set.

def aFunction(having=None,lots=None,of=None,default=None,args=None):
    print(having,lots,of,default,args)

aFunction(of="hello!")
# → None None hello! None None

*args and **kwargs

When used as parameters in a function signature, * and ** before an identifier indicate that the function will accept arbitrary additional positional arguments and keyword arguments respectively. These options are typically applied to variables named args and kwargs, and they must appear last (and in this order) in the function signature if used.

The variable marked with * will be provided as an ordered list, and ** will be an unordered dict of keyword names to values.

def takesArgs(*args):
    print(args)
takesArgs(1,2,3)
# → [1, 2, 3]
def takesKwargs(**kwargs):
    print(kwargs)
takesKwargs(a=1,b=2,c=3)
# → {'a': 1, 'b': 2, 'c': 3}
def takesEither(*args,**kwargs):
    print(args, kwargs)
takesEither(1,2,a=3,b=4)
# → [1, 2] {'a': 3, 'b': 4}
def takesARequiredAndMore(a,*args):
    print(a, args)
takesARequiredAndMore(1,2,3,4)
# → 1 [2, 3, 4]

Note: *args and **kwargs are especially useful in combination with Argument Expension (described below) to create decorators which do not need to know about the signatures of functions they wrap.

Argument Expansion

When used in a function argument list, * and ** before a list and dict expression respectively, will expand those values into the argument list.

let l = [1,2,3]
def foo(a,b,c):
    print(a,b,c)
foo(*l)
# → 1 2 3
let d = {"foo": "a", "bar": 1}
def func(foo,bar):
    print(foo, bar)
func(**d)
# → a 1

If an expanded list provides too many, or too few, arguments, an ArgumentError will be raised.

If an expanded dict provides parameters which are not requested, an ArgumentError will be raised.

If an expanded dict provides an argument which has already been defined, either as a positional argument or through a named parameter, an error will be raised.

Ternary Expressions

Ternary expressions allow for branching conditions to be used in expression contexts:

print("true branch" if True else "false branch")
print("true branch" if False else "false branch")
# → true branch
# → false branch

Ternary expressions perform short-circuit and will not evaluate the branch they do not take:

(print if True else explode)("What does this do?")
# → What does this do?

Docstrings

If the first expression in a module, function, class, or method is a string, it will be attached to the corresponding object in the field __doc__:

def foo():
    '''This is a function that does things.'''
    return 42

print(foo.__doc__)
# → This is a function that does things.

with Blocks

A with statement introduces a context manager:

with expr:
    ...

The value of expr must be an object with an __enter__ and __exit__ method, such as a fileio.File. The __enter__ method will be called upon entry and the __exit__ method will be called upon exit from the block.

The result of expr can also be assigned a name for use within the block. Note that as with other control flow structures in Kuroko, this name is only valid within the block and can not be referenced outside of it, and the same is true of any names defined within the block. If you need to output values from within the block, such as in the typical case of opening a file and loading its contents, be sure to declare any necessary variables before the with statement:

from fileio import open
let lines
with open('README.md') as f:
    lines = [l.strip() for l in f.readlines()]
print(lines)
# → ["![logo]...

Note that you can declare a variable for the object used with __enter__ and __exit__ before the with statement:

from fileio import open
let f = open('README.md')
print(f)
# → <open file 'README.md' ...>
let lines
with f:
    lines = [l.strip() for l in f.readlines()]
print(f)
# → <closed file 'README.md' ...>

If an early return is encountered inside of a with block, the __exit__ method for the context manager will be called before the function returns.

class ContextManager:
    def __init__(title):
        self.title = title
    def __enter__():
        print("Enter context manager", self.title)
    def __exit__():
        print("Exit context manager", self.title)
def doesANestedThing():
    with ContextManager('outer'):
        with ContextManager('inner'):
            with ContextManager('triple'):
                return 42
            print('Should not print')
        print('Should not print')
    print('Should not print')
print(doesANestedThing())
# → Enter context manager outer
#   Enter context manager inner
#   Enter context manager triple
#   Exit context manager triple
#   Exit context manager inner
#   Exit context manager outer
#   42

Note: The implementation of with blocks is incomplete; exceptions raised from within a with that are not caught within the block will cause __exit__ to not be called.

Special Decorators

The compiler implements special handling when the decorators @staticmethod and @property are used with methods of a class.

@staticmethod will mark the decorated method as a regular function - it will not receive an implicit "self" and it will be attached to the fields table of the class and instances of that class, rather than the methods table.

@property will mark the decorated method as a property object. When it is retrieved or assigned to from the class or instance through a dot-accessor (eg. foo.bar), the wrapped method will be called intead.

Properties work differently from in Python:

class Foo():
    def __init__(self):
        self._bar = 0
    @property
    def bar(self, *setter):
        if setter:
            print("Setting bar:", setter[0])
            self._bar = setter[0]
        else:
            print("Getting bar.")
            return self._bar
let f = Foo()
print(f.bar)
f.bar = 42
print(f.bar)
# → Getting bar.
#   0
#   Setting bar: 42
#   Getting bar.
#   42

Note: Special handling when using del on a property is not yet implemented.