Что это?

Это язык-обертка для Golang. Мы немного приближаемся с синтаксису Kotlin. Отсюда и название. Что-то из серии TypeScript / CoffeeScript для Javascript.

Цель: облегчить жизнь сейчас и может быть в будущем Golang займется улучшением языка. Как видно по Javascript и Java до появления альтернатив прогресса практически не было.

Соответственно, код максимально близок к Golang, но с улучшенным синтаксисом.

Сейчас это на стадии концепта: как можно улучшить язык Golang малой кровью.

Как это работает?

Транслятор Кола ищет файлы с расширением kol и генерирует из них go-файлы. Затем с ними можно делать все так же как и с обычными go-файлами.

Примеры

Примеры берем с сайта Golang. В примерах куча каких-то математических функций (в худших традициях книжек по программированию 90х и более ранних лет). И нет обработки ошибок (стыдно?)…

Поэтому берем первые 2 примера с сайта и еще один пример от себя про работу с ошибками.

Hello, World!

package main

import "fmt"

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

Кол:

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)
	}
}

Кол:

// 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")
}

Кол:

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")
}

Основные отличия

Большие отличия:

  • нормальный синтаксис классов (они все равно фактически есть)
  • работа с ошибками
    • return означает nil для error, throw означает nil для не error
    • автоматическое обертывание, если нет try-catch и есть error в ответе
    • try-catch
  • все функции с малой буквы (казалось бы причем тут C#)

Поменьше:

  • импорт отдельных функций
  • fun вместо func
  • for / forEach немного другие возможны (кстати, в последнем релизе 1.22 это немного улучшили)
  • условие в присвоении (a = if (b) 1 else 0)

Пока что еще нет описания нового языка, чтобы продумать все возможные случаи.

Понятно, что это еще первое приближение. И еще возможно много вещей поменяются, но решение проблем из секции “большие отличия” в том или ином виде останутся.

Где скачать?

Пока что нигде.

Примеры выглядят хорошо. Понятно как это можно реализовать.

Как минимум нужно:

  • описание языка
  • транслятор в golang
  • подстветка синтаксиса
  • реформат кода

Еще непонятно когда дойдут руки. Пока что у меня нет планов что-то писать на Golang платформе, а там посмотрим.