Features
Karina is a statically typed, general-purpose, high-level programming language focused on:
- Simplicity
- Interoperability
- Concise notation
- Null safety
Key features:
- Fully compatible with Java
- Seamless use of existing libraries and frameworks
- Modern programming experience
- Algebraic Data Types
- First-class functions
You can also find some examples in the test directory of the KarinaC repository.
Table of Contents
Structs
Structs are basically java classes, but with a more lightweight syntax.
pub struct Vec2 {
pub x: double
pub y: double
// static function
pub fn fromAngle(angle: double) -> Vec2 =
Vec2 { x: Math::cos(angle), y: Math::sin(angle) }
// member function
pub fn length(self) -> double {
Math::sqrt(self.x * self.x + self.y * self.y)
}
}
A default constructor and a toString
function are automatically created.
let v = Vec2 { x: 1.0, y: 1.5 }
println(v) // Vec2{x=1.0, y=1.5}
Structs, Fields and Functions are private by default. Use the pub
keyword to make them public.
Fields are also immutable by default. Use the mut
keyword to make them mutable.
Functions
Karina has five types of functions:
- Member Functions
- Static Functions
- Abstract functions
- Closures
- Extension Functions
Member functions
Member functions are defined inside a struct, enum or interface and can be called on an instance of that type.
They have access to the instance via the self
keyword as the first parameter.
struct User {
name: string
fn getNameUpperCase(self) -> string =
self.name.toUpperCase()
}
user.getNameUpperCase()
Static functions
Static functions are defined on the top level, inside a struct, enum or interface and can be called without an instance of that type.
fn display<T: ToStr + Display>(obj: T) {
//...
}
This also shows the use of generic types and type constraints.
Abstract functions
Abstract functions are defined in interfaces and do not have a body. They have to be implemented by the struct or enum that implements the interface.
interface ToStr {
fn toStr(self) -> string
}
Closures
Closures are both a type and a expression.
let add = fn(a: int, b: int) -> int { a + b }
add(1, 2) // 3
Parameter types and return types can be omitted if they can be inferred.
Closures compile to interfaces. You can explicitly define the interfaces a closure implements.
fn () -> string impl Callable<string>, Supplier<string>
Extension functions
Extension functions behave like member functions, but are defined outside of the type.
@Extension
pub fn sqrt(value: double) -> double = Math::sqrt(value)
2.sqrt() // 1.41421...
They can be defined on any type, including primitives.
The underlying function has to be in scope, so you might need to import it first.
Enums
Karina supports Algebraic Data Types, as a way to define a type with a fixed set of values.
This enum
does not behave like a Java enum, but more like enums in languages like Rust or Swift.
Coming from a Java background, think of them more like a sealed interface + record classes.
enum AuthState {
Guest(name: string)
User(name: string, id: int, token: string)
fn name(self) -> string
}
The Karina standard library provides the Option
and Result
enum. These Types can be written as T?
and R?E
respectively.
Static Fields
You can define statics inside structs, enums and interfaces and on the top level.
static PI: double = 3.1415927
They are immutable and private by default. Use the mut
and pub
keywords to make them mutable and public.
Interfaces
interface ToStr {
fn toStr(self) -> string
fn toString(self) -> string = self.toStr() // default implementation
impl Display // extends another interface
}
To implement an interface, use the impl
keyword.
struct Thing {
name: string
impl ToStr {
fn toStr(self) -> string = self.toString()
}
}
Expressions
Table of Contents
Variables
let name: string = "Karina"
Local variables are always mutable. The type can be omitted if it can be inferred.
let number = 0.5 // type is inferred as double
Branching
if condition {
//...
} else if otherCondition {
//...
}
if
expressions also supports pattern matching:
let orElse = if value is Result::Ok r {
r.toString()
} else is Result::Err e {
e.toString()
}
As everything is an expression in Karina, if
expressions can return a value
Creating Enums and objects
let opt = Option::Some { value: 1 }
let lang = Language { _: "Karina" }
This constructs finds and calls the underlying constructors.
The name and order of the fields have to match the constructor, but you can use _
to ignore the name of the field, mainly for working with Java classes.
String interpolation
let name = "Karina"
let greeting = 'Hello, $name!'
String interpolation works with single quotes.
Use $name
to insert a variable. Complex expressions are not supported... yet.
Closures
Closures are both a type and a expression.
let add = fn(a: int, b: int) -> int { a + b }
add(1, 2) // 3
Parameter types and return types can be omitted if they can be inferred.
Closures compile to interfaces. You can explicitly define the interfaces a closure implements.
fn () -> string impl Callable<string>, Supplier<string>
Unwrapping
The ?
operator can be used to unwrap an Option
or Result
. If the value is None
or Err
, the function will return early.
fn trim(opt: string?) -> string? {
let trimmed = opt?.trim()
Option::some(trimmed)
}
Arrays
Arrays are created using square brackets.
let arr = [1, 2, 3]
You can also give it a type explicitly.
let arr = <int>[1, 2, 3]
Creating empty arrays it not supported. Use functions in the karina::lang::Values
and karina::lang::Option
namespaces instead.
try-with-resources
using stream = Files::createStream(path)? {
// ...
}
The using
expression automatically closes the resource when it goes out of scope. The resource has to implement the AutoCloseable
interface.
Cast
10.0 as int // cast double to int
Casting is only allowed between primitives and types that can be safely casted.
Use Option::instanceOf(class, value)
for safe casting.
Instance check
let isObject = value is MyClass
Literals
All JVM primitive are supported: int
, long
, float
, double
, byte
, short
, char
and bool
.
Numbers can be written using decimal, hexadecimal and binary notation.
0x1A // hexadecimal
0b1101 // binary
1_000_000 // underscores are ignored
String literals can be written using double quotes.
let str = "Hello, World!"
Characters can be written using single quotes.
let char = 'A'
Be aware that single quotes are used for characters and string interpolation.
Strings, String interpolation and Characters support escape sequences like \n
, \t
, \\
, \'
and \"
and even unicode characters like \u03A9
(Ω)
Paths
Paths are written using ::
as a separator, for types, static fields and functions.
let path = org::example::MyClass::MY_STATIC
while, for, continue, break
while
, for
, continue
and break
work as expected.
while true {
if false {
break
} else {
continue
}
}
Yielding values from loops is not supported... yet.
Super calls and custom constructors
struct Thing {
name: string
fn (self) {
super<Object>()
self.name = "Thing"
}
}
You can define a custom constructor by defining a member function without a name.
Be aware that if you define a custom constructor, the default constructor will not be generated and you have to call a super constructor manually.
Throw
throw java::io::IOException { message: "File not found" }
The throw
expression can be used to throw exceptions. These have to extend java.lang.Throwable
.
I would advise against using exceptions for control flow. Use the Option
and Result
types instead.
The Result::safeCall*
functions can be used to convert exceptions into Result::Err
values.
Imports
Karina uses a hierarchical import system based on the file structure. Each file is a unit that can contain multiple structs, enums, interfaces and functions. \
import org::example::mylib::MyStruct // import the MyStruct struct inside the mylib.krna file
import org::example::mylib::* // import everything inside the mylib.krna file
import org::example::mylib::MyEnum { Case1, Case2 }
import org::example::mylib MyStaticFunctionOrField
// collision handling
import java::util::List
import java::awt::List as AwtList // import the List class from java.awt and rename it to AwtList
You are only allowed to rename imports when there is a collision. The new name has to contain the original name as a substring.