Некоторое время назад я узнал о языке V, ссылка на его репозиторий. Стало интересно поизучать его, так как он похож на Go. Меня удивило то, что на Хабре нет ни одной статьи с ним. Синтаксис языка очень схож с Go и Rust, V так же не придерживается концепции языка со стандартной иерархией типов.
Как заявляют разработчики V, он очень быстр, прост, безопасен и что его можно выучить за одни выходные. Насчет последнего я, конечно, очень сомневаюсь))) V не может соревноваться с Go по распространенности, размеру комьюнити и прочим другим вещам, присущим языкам, это и так понятно. Но взглянуть на него несомненно стоит.
Нехитрый тест на производительность
Сравним производительность V с помощью прогона по массиву и вычислению суммы элементов.
import time
fn main() {
start_time := time.now()
mut sum := i64(0)
mut i := i64(0)
for i <= 100000000 {
sum += i
i++
}
println(time.since(start_time).milliseconds())
println(sum)
}
V дает результат от 125 до 181 миллисекунд против 28 на Go. Дальше рассмотрим реализацию интерфейсов и многопоточности, так как это фишки Go, отличающие его от других языков.
Интерфейсы
Интерфейсы в V реализуются неявно, разрешено встраивание, но также тут присутствует возможность хранения данных в интерфейсе. Обращение к полям данных осуществляется через точку.
struct Dog {
animal_type string
}
fn (d Dog) speak() string {
return 'woof'
}
struct Cat {
animal_type string
}
fn (c Cat) speak() string {
return 'meow'
}
interface Speaker {
animal_type string
speak() string
}
fn main() {
dog := Dog{'dog'}
cat := Cat{'cat'}
mut arr := []Speaker{}
arr << dog
arr << cat
for item in arr {
println('a $item.animal_type says: $item.speak()')
}
}
Через интерфейсы можно менять значение полей структуры, вроде бы обычное поведение, но чтобы это сделать надо везде указать ключевое слово mut, собственно, как и в любых других попытках изменить значение. Так как стандартно любые переменные в V неизменны. Подобное поведение в Go с использованием указателей мне кажется реализовано более удобным образом, но нужно иметь ввиду какие типы используют передачу по значению, а какие по ссылке.
//mut section
interface Bar {
mut:
write(string, int) string
}
struct MutStruct {
mut:
n int
}
//immut section
interface Foo {
write(string) string
}
struct Struct {
n int
}
fn (mut s MutStruct) write(a string, new_n int) string {
s.n = new_n
println(s.n) //n=5
return a
}
fn (s Struct) write(a string) string {
return a
}
fn main() {
s1 := Struct{n: 2}
mut s2 := MutStruct{n: 2}
fn1(s1)
fn2(mut s2, 5)
}
fn fn1(s Foo) {
println(s.write('Foo'))
}
fn fn2(mut s Bar, n int) {
println(s.write('Bar', n))
}
Так же интерфейсы могут иметь свои собственные методы, как и структуры в Go. В случае использования интерфейса со структурой, имеющей переопределенный метод, V вызывает базовую реализацию интерфейса, так же происходит, если интерфейс не имеет переопределенного метода. Go не позволил бы скомпилировать подобную ситуацию, выдав ошибку о необходимости добавления отсутствующего метода. В какой-то мере поведение Go более интуитивно и безопасно. В случае явного кастинга через оператор if мы можем добиться вызова метода на желаемой структуре.
interface Adoptable {}
fn (a Adoptable) speak() string {
return 'adopt me!'
}
struct Cat {}
fn (c Cat) speak() string {
return 'meow!'
}
struct Dog {}
fn main() {
cat := Cat{}
println(cat.speak()) //meow
a := Adoptable(cat)
println(a.speak()) //adopt me
if a is Cat {
println(a.speak()) //meow
}
dog := Dog{}
b := Adoptable(dog)
println(b.speak()) //adopt me
}
Многопоточность
Потоки в V являются объектами. Объект потока использует скрытый defer, который позволяет программе не упасть, в Go же необходимо делать это явным образом. Но это применимо только для объектов типа потока. Однако существует и более привычное использование функций без включения их в поток.
import math
fn p(a f64, b f64) {
c := math.sqrt(a * a + b * b)
println(c)
}
fn main() {
mut threads := []thread{}
threads << go p(3, 4)
threads << go p(3, 7)
threads.wait()
}
Если говорить о примитивах синхронизации, присутствующих в Go, это все так же имеется - Atomics, sync.WaitGroup. Синхронные и буферизированные каналы также имеются. Но размер последних не является частью типа, а должен инициализироваться следующим образом:
ch := chan int{} // unbuffered - "synchronous"
ch2 := chan f64{cap: 100} // buffer length 100
Также сохранен механизм с оператором select и закрытием каналов. Если канал не закрыт мы считываем его актуальное значение, в противном случае выполнятся действия внутри скобок.
fn main() {
ch := chan int{}
//ch.close()
go fn(ch chan int) {
ch <- 10
}(ch)
m := <-ch or {
println("closed!")
}
println(m)
}
Каналы, а именно работа с ними, реализованы более хитрым образом. Оператор select здесь заменил традиционную нотацию ", ok", с его помощью можно проверить в каком статусе находится канал. Мы имеем возможность считать значение из канала, если канал готов, а так же знать находится он в открытом или закрытом состояние.
fn main() {
ch := chan int{}
go fn(ch chan int) {
ch <- 10
}(ch)
//ch.close()
if select {
m := <- ch {
println(m)
}
} {
println("open") //open
} else {
println("closed") //closed
}
}
Дополнительно у каналов имеется методы try_push и try_pop(). Но сами разработчики языка не советуют их использовать, так как они подвержены состоянию гонки.
Еще одним механизмом синхронизации является shared object. Это структура со встроенным мьютексом, которая позволяет использовать совместно общие данные.
struct SharedStruct {
mut:
x int // data to be shared
}
fn (shared st SharedStruct) g(newValue int) {
lock st {
st.x = newValue
}
}
fn main() {
shared a := SharedStruct{
x: 10
}
go a.g(15)
go a.g(20)
rlock a {
println(a.x) //20
}
}
Но использование такой структуры замедляет работу с данными, части из которых, возможно, не требуется параллельный доступ, тем не менее общая скорость работы будет снижена. Именно поэтому в Go отказались от использования SyncMap как стандартного механизма языка.
Кстати, для VSCode есть плагин, поддерживающий этот язык.
Заключение
В языке есть много других интересных концепций, которые я бы с удовольствием рассмотрел, но это выходит за рамки этой статьи. Было бы интересно почитать об особенностях структур, типе Option и других вещах.
V поддерживает различные фишки, взятые от разных языков. Но думаю, что желание запихнуть в язык как можно больше всего в какой-то момент может его убить.
Комментарии (8)
mbeloshitsky
15.11.2022 11:47+3Язык V безусловно можно обвинить в том, что он "неправильно" работает, видел я его начальные версии.
С другой стороны нельзя не отметить, что по крайней мере его автор пишет на нем достаточно сложные штуки, далеко зашедшие. Один только ved чего стоит.
Так что я все таки желаю успехов проекту, пусть он и не столь академичен, как хотелось бы.
AnthonyMikh
16.11.2022 17:46+1А смысл обозревать язык, автор которого постоянно врёт и тупо собирает донаты под обещания?
iskateli
Тоже повёлся на красивый синтаксис и удобство, а под капотом оказывается не всё так гладко, вот недавняя разгромная статья по этому языку https://mawfig.github.io/2022/06/18/v-lang-in-2022.html
Alekstet Автор
Твоя? Мощно!
Alekstet Автор
Кстати, многие ошибки из статьи уже исправлены
Bessnov
Проект очень молодой, в стадии "Beta". Если сейчас погонять примеры из этой статьи, то уже не всё так плохо.
Ну и эта хабростатья вроде про новое интересное. Совсем не обязательно бежать и заменять Vlang'ом Golang и прочее. Где-то оно может и пригодиться. Вполне может прижиться где-нибудь рядом с bash'ем как скриптовой язык.
https://github.com/vlang/v/blob/master/doc/docs.md#cross-platform-shell-scripts-in-v
Посмотрим как будет развиваться.