Koi is a language that seeks to make many traditional bugs impossible by preventing them at the language level. Each of these are discussed in more detail below.
- Prevent all runtime errors. Runtime errors are, by definition, unexpected and either have to be caught and emit and error under a sitution that can't be handled safely or cause the program to blow up.
- No garbage collector, but no manual memory management either. It's unsafe to manage memory manully, but we also don't want the overhead of a garbage collector.
- Interoperability with C. This is critical to making sure we can use existing libraries and also makes the lanaguage a lot easier if we can offload the lowest level logic to C.
- All objects are also interfaces. Any object can be provided for a type if it fits the receiving interface.
- First class testing. Language constructs for dealing with tests, assertions, mocks, etc.
- Sum types handle behaviors like errors.
Number
String
And some more specific domained types:
type PositiveNumber = Number where value > 0
type Int = Number where math.floor(value) == value
type PositiveInt = Number where math.floor(value) == value and value > 0
There is a differnce between detecting and avoiding. We have to be careful avoiding doesn't become a burdon.
- Overflow and underflow.
- All matching must be exhaustive?
- Array and map out of bounds.
- Casting to an invalid type.
- Signals and other interupts.
This is not possible becuase there is no concept of nil/null values because all values are instantiated. There are times when a missing value is needed, you must use a sumtype for this:
type LinkedListNode<T> {
value T
next LinkedListNode<T> | None
}
The denominator must be a NonZeroFloat
type NonZeroFloat Float where x != 0
func main() {
var a = 7 / 5 // OK
var b = 3
var c = 7 / b // ERROR: divide expects NonZeroFloat for denominator, got Float
var d = 7 / NonZeroFloat(b) // ERROR: does not catch DomainErr
var e = 7 / NonZeroFloat(b) -> DomainErr { 1 } // OK
var f = 0
if b != 0 {
f = 7 / b // OK
}
}
Force a rounding mode rather than just truncating?
func main() {
var a = 1.23
var b = a as Int // ERROR: cannot cast Float to Int
var c = math.floor(a) // OK
}
Rather than make NaN and +/- inf special values, these are actually types that must be handled separately if the function possibly returns them.
// math package
func log(x Float) (Float | Infinity | NotANumber) {
if x == 0 {
return Infinity{negative: true}
}
if x < 0 {
return NotANumber{}
}
return C.log(x)
}
func main() {
io.printLine(log(8)) // 0.903089987
io.printLine(log(0)) // -Inf
io.printLine(log(-1)) // NaN
var result = log(8) * 2 // ERROR: log() may return multiple types
// Or, translate other types into a Float
var result = log(8) as {
NotANumber, Infinity: 0
} * 2
}
However, this would be quite painful to do everytime. Especially when we know the inputs are valid, so instead we can use a checked type:
type PositiveFloat Float where value > 0
func log(x PositiveFloat) Float {
return C.log(x)
}
func main() {
var result = log(8) * 2 // OK
result = log(-1) * 2 // ERROR: -1 is not a valid PositiveFloat
sneaky = -2
result = log(sneaky) * 2 // ERROR: expected PositiveInteger, got Int
safeSneaky = PositiveNumber(sneaky + 4) // ERROR: PositiveNumber() may fail check
safeSneaky = PositiveNumber(sneaky + 4) on Domainrr { 1 } // OK
log(safeSneaky)
}
DomainErr
when trying to cast value into a compatible type that's not allowed.
- Explicit order of operations.
- Zero out memory.
- Explicit mutability.
- No jumps/gotos (including breaking).
- No operator overloading.
- No type overloading.
Memory cannot be shared between processes.
Launching a process returns a different type (ie. Process[MyObject]) that itself provides the API for syncronizing calls.
Any value that attempts to cross a process boundary must implement a Copy
interface.
Reference counting.
- Tests
- Assertions
- Mocks
type NumberOrString = number | string
type MultiValues = (number, bool) | None
type NamedTypes = @high (number, number) | @low (number, number) | number
func static[doStuff] :good number | :bad number | error {
}
func {
const result = match static[doStuff] {
:good n number {
n + 5
}
:bad number {
-1
}
error {
0
}
}
}
Erorrs are just a return type. Auto snapshotting?
import io
func main() {
io.printLine("hello world")
}
// FIXME
import io
func main() {
io.printLine("go" + "lang")
fmt.Println("1+1 =", 1+1)
fmt.Println("7.0/3.0 =", 7.0/3.0)
fmt.Println(true && false)
fmt.Println(true || false)
fmt.Println(!true)
}
import io
func main() {
var a = "initial"
io.printLine(a)
var b, c int = 1, 2
fmt.Println(b, c)
var d = true
fmt.Println(d)
var e int
fmt.Println(e)
f := "apple"
fmt.Println(f)
}