Variants

Variants are a way to maintain type safety while representing variable state. The best way to explain this is by example:

struct StoreItem {
    variant Available {
        name: string
        price: dec
    )
    variant Missing {
        name: string
    }
}

fn main() {
    var item: StoreItem.Missing(name: "CLIF Bars")

    item->Available(
        name: item.name
        price: 2.0
    )
}

-> syntax is better than vary keyword/block, and it probably means we don't need match either (can use if and switch).

The compiler doesn't need to keep track of what variant type is active in every scope, because the reference/ownership rules save us from potential conflicts. So normal async/gen rules apply, nothing special.

Note, -> is a statement, not an expression. There is no value and thus it can't be used in an expression and can't be assigned to. For example, this will return an error:

store_result->FullyStocked.total += 3 + item->Available.total

-> can be used to vary an existing variant though, and if you like, you can wrap it in an iferr:

fn append!(store: *Store) {
    if (store->NotListening) { # This is cool
        iferr(store->Listening(name: store.name
                               prices: store.prices
                               channel: store.channel
                               store.listener: store.listen()) {
            echo("Store failed to start listening: ${e.msg}")
            return
        }
    }
    StoreList.append(*store)
}

Also variant fields should be optional if their name and type match:

fn append!(store: *Store) {
    if (store->NotListening) { # This is cool
        iferr(store->Listening(store.listener: store.listen()) {
            echo("Store failed to start listening: ${e.msg}")
            return
        }
    }
    StoreList.append(*store)
}

Old Variants

Variants are a way to maintain type safety while representing variable state. The best way to explain this is by example:

type Path (string) {
    :get {
        exists:     os.path.exists(:base)
        is_file:    os.path.is_file(:base)
        is_folder:  os.path.isdir(:base)
        readable:   os.path.access(:base, "r")
        writable:   os.path.access(:base, "w")
        executable: os.path.access(:base, "x")
    }
}

const FileModeString (string) {
    ReadWriteExecute: "rwx"
    ReadWrite:        "rw"
    ReadExecute:      "rx"
    WriteExecute:     "wx"
    Execute:          "x"
}

type Data {
    :fields {
        is_null: bool(true)

        :variants {
           switch (is_null) {
                case true {
                    is_null: bool(true)
                    error:   Error
                }
                case false {
                    is_null: bool(false)
                    data: StringBuffer
                }
            }
        }
    }

    static from_file(File file) (Data) {
        with (file.read() as file_data) {
            return Data(
                is_null: false,
                data: StringBuffer(file_data)
            )
        }
        else {
            return Data(
                is_null: true,
                error: :error
            )
        }
    }
}

flag FileMode {
    Read
    Write
    Execute

    fn <FileModeString>() {
        switch (:base) {
            case Read | Write | Execute {
              return ReadWriteExecute
            }
            case Read  | Write | Execute {
                return ReadWriteExecute
            }
            case Read  | Write {
                return ReadWrite
            }
            case Read  | Execute {
                return ReadExecute
            }
            case Write | Execute {
                return WriteExecute
            }
            case Execute {
                return Execute
            }
        }
    }
}

const FileState {
    Closed
    Open
    Error
}

type File {
    :fields {
        FileState state: Closed

        :variants {
            switch (state) {
                case FileState.Open {
                    state: FileState.Closed
                    handle: sys.stdio.FileObject
                }
                case FileState.Error {
                    state: FileState.Error
                    error: Error
                }
            }
        }
    }

    static open(Path path, FileMode mode) (File) {
        with (sys.stdio.fopen(path, mode) as file_handle) {
            return File(
                state: FileState.Open,
                handle: file_handle
            )
        }
        else {
            return File(
                state: FileState.Error,
                e: :error
            )
        }
    }

    fn read() (Data) {
        return Data.from_file(self)
    }

    fn close() {
        sys.stdio.fclose(self.file_object)
    }
}

fn echo_a_file_using_context_errors() {
    file = File.open(
        path: Path('/etc/passwd'),
        mode: FileMode.Read
    )

    if (file.state == FileState.Error) {
        die("Opening ${file.path} failed (${file.error}), try again later")
    }

    Data file_data = file.read()

    if (file_data.is_null) {
        die("Reading ${file.path} failed (${e}), try again later")
    }

    echo(data)

    file.close()
}

This idiom has some similarities with exceptions and optional return types.

Like exceptions, it allows you to avoid the "pyramid of doom": a phenomenon where your code moves further and further to the right as you handle all the potential error cases. Pyramids of doom are still possible because with blocks (and try blocks) can be nested, but it's very bad practice in general.

Exceptions in most languages will let you leave variables uninitialized however. In Ki, this isn't possible; if code later tries to access file.handle, it is a compile-time error. This is similar to optional return types, where you handle the different return types right there in the function call.

The main difference, and the standout feature, is that there is never any question about return types. file is always assigned a File instance, even if this instance might vary from other instances. It isn't a NULL pointer (also not allowed in Ki), it isn't a special, reserved type of File that means an error occurred, and it isn't a special "error" instance.