What is it?

This is a wrapper language for Golang. We are getting a little closer to the Kotlin syntax. Hence the name. Something from the TypeScript/CoffeeScript series for Javascript.

The goal: to make life easier now and maybe in the future Golang will improve the language. As can be seen from Javascript and Java, there was practically no progress before the advent of alternatives.

Accordingly, the code is as close as possible to Golang, but with improved syntax.

It’s at the concept stage now.: how to improve the Golang language with little blood.

How does it work?

The Cola translator searches for files with the kol extension and generates go files from them. Then you can do everything with them in the same way as with regular go files.

Examples

We take examples from the Golang website. There are a lot of mathematical functions in the examples (in the worst traditions of programming books of the 90s and earlier). And there is no error handling (shame?)…

Therefore, we take the first 2 examples from the site and another example from myself about working with errors.

Hello, World!

package main

import "fmt"

func main() {
	fmt.Println("Hello, 世界")
}

Kol:

package main

import fmt.println

fun main() {
	println("Hello, 世界")
}

Conway’s Game of Life

// An implementation of Conway's Game of Life.
package main

import (
	"bytes"
	"fmt"
	"math/rand"
	"time"
)

// Field represents a two-dimensional field of cells.
type Field struct {
	s    [][]bool
	w, h int
}

// NewField returns an empty field of the specified width and height.
func NewField(w, h int) *Field {
	s := make([][]bool, h)
	for i := range s {
		s[i] = make([]bool, w)
	}
	return &Field{s: s, w: w, h: h}
}

// Set sets the state of the specified cell to the given value.
func (f *Field) Set(x, y int, b bool) {
	f.s[y][x] = b
}

// Alive reports whether the specified cell is alive.
// If the x or y coordinates are outside the field boundaries they are wrapped
// toroidally. For instance, an x value of -1 is treated as width-1.
func (f *Field) Alive(x, y int) bool {
	x += f.w
	x %= f.w
	y += f.h
	y %= f.h
	return f.s[y][x]
}

// Next returns the state of the specified cell at the next time step.
func (f *Field) Next(x, y int) bool {
	// Count the adjacent cells that are alive.
	alive := 0
	for i := -1; i <= 1; i++ {
		for j := -1; j <= 1; j++ {
			if (j != 0 || i != 0) && f.Alive(x+i, y+j) {
				alive++
			}
		}
	}
	// Return next state according to the game rules:
	//   exactly 3 neighbors: on,
	//   exactly 2 neighbors: maintain current state,
	//   otherwise: off.
	return alive == 3 || alive == 2 && f.Alive(x, y)
}

// Life stores the state of a round of Conway's Game of Life.
type Life struct {
	a, b *Field
	w, h int
}

// NewLife returns a new Life game state with a random initial state.
func NewLife(w, h int) *Life {
	a := NewField(w, h)
	for i := 0; i < (w * h / 4); i++ {
		a.Set(rand.Intn(w), rand.Intn(h), true)
	}
	return &Life{
		a: a, b: NewField(w, h),
		w: w, h: h,
	}
}

// Step advances the game by one instant, recomputing and updating all cells.
func (l *Life) Step() {
	// Update the state of the next field (b) from the current field (a).
	for y := 0; y < l.h; y++ {
		for x := 0; x < l.w; x++ {
			l.b.Set(x, y, l.a.Next(x, y))
		}
	}
	// Swap fields a and b.
	l.a, l.b = l.b, l.a
}

// String returns the game board as a string.
func (l *Life) String() string {
	var buf bytes.Buffer
	for y := 0; y < l.h; y++ {
		for x := 0; x < l.w; x++ {
			b := byte(' ')
			if l.a.Alive(x, y) {
				b = '*'
			}
			buf.WriteByte(b)
		}
		buf.WriteByte('\n')
	}
	return buf.String()
}

func main() {
	l := NewLife(40, 15)
	for i := 0; i < 300; i++ {
		l.Step()
		fmt.Print("\x0c", l) // Clear screen and print field.
		time.Sleep(time.Second / 30)
	}
}

Kol:

// An implementation of Conway's Game of Life.
package main

import (
	bytes
	fmt
	math/rand
	time
	fmt.print
	time.sleep 
)

// Field represents a two-dimensional field of cells.
class Field {
	val field: [][]bool
	val width: int
	val height: int

	// returns an empty field of the specified width and height
	init {
		field = make([][]bool, h)
		field.forEach { i ->
			i = make([]bool, w)
		}
	}

	// sets the state of the specified cell to the given value
	fun set(x: int, y: int, value: bool) {
		field[y][x] = value
	}
	
	// reports whether the specified cell is alive
	// If the x or y coordinates are outside the field boundaries they are wrapped
	// toroidally. For instance, an x value of -1 is treated as width-1.
	fun isAlive(x: int, y: int): bool {
		x += width
		x %= width
		y += height
		y %= height
		return field[y][x]
	}

	// returns the state of the specified cell at the next time step
	fun next(x: int, y: int): bool {
		// Count the adjacent cells that are alive.
		alive := 0
		for i := -1; i <= 1; i++ {
			for j := -1; j <= 1; j++ {
				if (j != 0 || i != 0) && isAlive(x+i, y+j) {
					alive++
				}
			}
		}
		// Return next state according to the game rules:
		//   exactly 3 neighbors: on,
		//   exactly 2 neighbors: maintain current state,
		//   otherwise: off.
		return alive == 3 || alive == 2 && isAlive(x, y)
	}
}

class Life(
	val width: int,
	val height: int,
) {
	a: Field
	b: Field

	// creates a new Life game state with a random initial state
	init {
		a = Field(w, h)
		b = Field(w, h)
		randomizeField(a)
	}

	fun randomizeField(field: Field) {
		for (i in 0..(width * height / 4-1)) {
			field.set(rand.intn(width), rand.intn(height), true)
		}
	}

	// advances the game by one instant, recomputing and updating all cells
	fun step() {
		// Update the state of the next field (b) from the current field (a).
		for y in 0..height-1 {
			for x in 0..width-1 {
				b.set(x, y, a.next(x, y))
			}
		}
		// Swap fields a and b.
		a, b = b, a
	}

	// returns the game board as a string
	fun string(): string {
		var buf: bytes.Buffer
		for y in 0..height-1 {
			for x in 0..width-1 {
				b: byte := if (a.isAlive(x, y)) '*' else ' '
				buf.writeByte(b)
			}
			buf.writeByte('\n')
		}
		return buf.string()
	}
}

fun clearScreen() {
	print("\x0c")
}

fun main() {
	life := Life(40, 15)
	for i in 0..299 {
		life.step()
		
		clearScreen()
		print(life)
		
		sleep(time.second / 30)
	}
}

Пример с ошибками

///usr/bin/true; exec /usr/bin/env go run "$0" "$@"
package main

import (
    "errors"
    "fmt"
)

type User struct {
  ID       string
  Username string
  Age      int
}

func Find(username string) (*User, error) {
    return nil, errors.New("Some error")
}

func FindUser(username string) (*User, error) {
    u, err := Find(username)
    if err != nil {
        return nil, fmt.Errorf("FindUser: failed executing db query: %w", err)
    }
    return u, nil
}

func FindAndSetUserAge(username string, age int) error {
  var user *User
  var err error

  user, err = FindUser(username)
  if err != nil {
      return fmt.Errorf("FindAndSetUserAge: %w", err)
  }

  // code that updates user
  fmt.Println("user: %s", user.Username)

  return nil
}

func main() {
    if err := FindAndSetUserAge("user1@example.com", 1); err != nil {
        fmt.Println("failed finding or updating user:", err)
        return
    }

    fmt.Println("successfully updated user's age")
}

Kol:

package main

import (
    errors
    fmt
    fmt.errorf
    fmt.println
)

type User struct {
  ID       string
  Username string
  Age      int
}

fun find(username: string): *User, error {
    // throws -- special return case aka return nil, err
    throws errors.new("Some error")
}

fun findUser(username: string): *User, error {
	// executing db query
    u, err := find(username)
    // err is handled automatically including "executing db query" parse
    return u
}

fun findAndSetUserAge(username: string, age: int): error {
  var user: *User
  var err: error

  user, err = findUser(username)

  // code that updates user
  println("user: %s", user.Username)
}

fun main() {
	// explicit checks are still possible
    if err := findAndSetUserAge("user1@example.com", 1); err != nil {
        println("failed finding or updating user:", err)
        return
    }

	// try-catch syntax added
	try {
		findAndSetUserAge("user2@example.com", 5)
		findAndSetUserAge("user3@example.com", 10)
	} catch (err) {
        println("failed finding or updating user:", err)
        return
    }

    println("successfully updated user's age")
}

The main differences

Big differences:

  • the normal syntax of classes (they actually exist anyway)
  • error handling
    • return means nil for error, throw means nil for non-error
    • automatic wrapping if there is no try-catch and there is an error in the response
    • try-catch
  • all functions with a small letter (it would seem that C# has nothing to do with it)

Smaller:

  • import of individual functions
  • fun instead of func
  • for / forEach slightly different are possible
  • assignment condition (a = if (b) 1 else 0)

So far, there is no description of the new language to think through all possible cases.

It is clear that this is still the first approximation. And maybe a lot of things will change, but solving problems from the “big differences” section in one form or another will remain.

Where can I download it?

Nowhere yet.

The examples look good. It is clear how this can be implemented.

At a minimum, you need:

  • a description of the language
  • translator in golang
  • syntax subtype
  • code reformation

It is still unclear when the hands will reach. So far, I have no plans to write anything on the Golang platform, and then we’ll see.