О том что Huawei обнародует собственный язык программирования новости ходили уже давно. Ещё в прошлом году при поиске работы мне уже предлагали работать с этим языком - правда "не сейчас а вот-вот вскоре" :) На тот момент компилятор и прочие инструменты ещё не были в открытом доступе. Сейчас же страница скачивания - вот она - под Linux, Windows и Darwin (да ещё и плагин к VSCode)!

В этой статье - беглый обзор. Я попробовал скачать-запустить и, пройдясь по разделам документации, попробовал основные фичи - так что вы можете сэкономить себе время и за 5-10 минут составить представление о Cangjie. Сразу скажу - чего-то оригинального, инновационного - я не заметил. Нет такого, чтобы как с Haskell, Erlang или Rust на первых порах пришлось ломать голову. Для программистов на Java, Go, C++ много будет довольно привычных вещей (можно сказать - "обокрали" эти языки тут и там понемногу).

Всякое про Cangjie писали, начиная от того что там будут только китайские иероглифы - и заканчивая специальной поддержкой для создания приложений с ИИ - ничего этого в данной статье не будет. Иероглифов (кроме как в тьюториалах) не встретил, а поддержка ИИ если и есть, то самого языка она не касается.

Есть нюанс с примерами - Habr естественно не поддерживает подсветку синтаксиса для Cangjie (пока) - а для наглядности это желательно. Пожалуй буду вставлят картинками, за что заранее прошу извинения.

Скачать и запустить

На странице со ссылками для скачивания (текущая версия 0.53.18) выбираю вариант для Linux x64. Архивчик gzip весит 300+ мегабайт но в наше время этим вряд ли кого удивишь.

Скачиваю и распаковываю в какую-то папочку (у меня это ~/utils/cangjie) - удостоверяюсь что внутри есть папка bin а в ней cjc - очевидно, компилятор.

Много учебных материалов и тьюториалов на китайском языке, ох, когда-нибудь я им может и займусь - но пока обращу внимание что на странице Docs всё-таки используется английский язык, хотя местами и с акцентом :) Открываю User's Manual и нахожу пример программы Hello World. Вы его можете найти и в Песочнице (playground) впрочем.

быть может и вам фраза hello world кажется уж слишком избитой?
быть может и вам фраза hello world кажется уж слишком избитой?

Синтаксис на первый взгляд не пытается нас поразить чем-то необыкновенным, фигурные скобочки в "сишном" стиле - а на самом деле даже в стиле языка B (предшественник C, отличающийся отсутствием типизации). Запишем этот пример в файлик test.cj и попробуем скомпилировать:

~/utils/cangjie/bin/cjc test.cj

Для удобства конечно можно добавить нужную папку в PATH или сделать символьную ссылку /usr/bin/cjc - но об этом чуть дальше.

Компилируется без замечаний. Появляются три новых файла (помимо исходника test.cj)

default.bchir2  (1228 байт)
default.cjo     (392 байта)
main            (756512 байт, исполнимый)

Однако попытка запустить полученный файл обламывается:

$ ./main
./main: error while loading shared libraries: libcangjie-runtime.so:
    cannot open shared object file: No such file or directory

Ясно, тут вам не Go - исполнимый файл хотя и здоровый, а завязан на динамическую библиотеку. Впрочем поскольку язык нацелен на разработку под конкретную платформу, в этом есть смысл.

Вообще-то cjc --help показывает ключи компиляции которые вроде бы должны помочь скомпилироваться статически, но с первой попытки не заметно чтобы они на что-то влияли. Оставим этот вариант на потом и пока найдём в папке с cangjie файл envsetup.sh - беглый просмотр содержимого подсказывает что он как раз вроде бы проставляет нужные переменные окружения. Выполняем:

source ./envsetup.sh

Знатоки подскажут что вместо source можно написать просто точку, но в тексте статьи её легче потерять, поэтому пусть будет слово :) Теперь дело пойдёт на лад:

$ ./main 
Nihao, I'm a fine language :)

ну просто чудеса.

Базовые конструкции

Все что не касается вопросов компиляции и прочих околосистемных вы можете легко попробовать в вышеупомянутой песочнице. Попробуем базовые вещи - циклы, условия, переменные.

Идентификаторы. Как обычно, начинаются с буквы или подчёркивания, дальше могут идти и цифры. Буквы понимаются в смысле Unicode (поэтому иероглифами тоже писать можно). Ещё можно заключить имя идентификатора в обратные одиночные кавычки (backticks) - тогда можно и зарезервированные слова использовать.

Объявление переменных. Ключевые слова let и var из которых первое создаёт иммутабельную переменную - концепция не новая. Иммутабельными являются также параметры функций (немного неожиданно) и переменная итерации в цикле for.

Объявление функций. Если по примеру выше вы подумали, что функции объявляются без подходящего ключевого слова какого-нибудь (в стиле bash) то ошились. Используется ключевое слово func за исключением функции main - попытка доавить это слово к ней вызовет ошибку. Я не одобряю такую логику, но ладно уж, мелочь :)

Формат записи параметров функции напоминает, кажется, Паскаль
Формат записи параметров функции напоминает, кажется, Паскаль

Этот небольшой пример поможет нам проверить что:

  • параметры командной строки передаются в main массивом строк

  • доступ к элементу массива с помощью квадратных скобок, индексируются с 0

  • println может принимать только один параметр

  • зато плюсик конкатенирует строки

  • возвращаемый тип для main можно не указывать или указать Unit (не uint!) - это аналог void, но чуть более равноправный с другими типами - у него есть одно значение, его можно присваивать и сравнивать (хотя неясно зачем); позже в документации упомянуто что целочисленный тип тоже возвращать можно

  • используемые функции могут быть определены далее по коду, не обязательно раньше

Базовые типы. Числа и строки упомянем поверхностно.

  • целочисленные - тут инты всех размеров от 8 до 64, знаковые и беззнаковые а также пара платформозависимых (со знаком и без)

  • вещественные, с плавающей точкой - тут из необычных Float16 - остальные два 32 и 64 разрядный вполне привычны; полезность 16-разрядного здорово озадачивает - хотя в стандарте IEEE 754 оказывается этот тип есть.

  • логический Boolean, символьный (Rune) и строковый (String) типы - тут всё очень привычно, есть подстановка переменных в строки в стиле PHP (${var}). Строки по-видимому иммутабельны.

  • операторы для этих типов довольно привычные, как в C/Java - есть мелкие отличия (например операторы инкремента и декремента доступны только в суффиксной форме)

Массивы. Тоже все достаточно привычно с учётом мелких деталей синтаксиса:

(а Рама краснел со сраму)
(а Рама краснел со сраму)

Range. Зачем было "последовательность значений" в стандартные типы выносить? Легко догадаться - для поддержки своеобразного цикла for, который умеет итерировать по массивам и интервалам (точнее по Iterable) и не имеет классического "сишного" трёхчастного варианта:

такой синтаксис много что напоминает, включая bash - но это лишь синтаксис
такой синтаксис много что напоминает, включая bash - но это лишь синтаксис

Вот здесь это 1..10 это не какой-то синтаксический сахар, а полноценный элемент типа Range<Int64> например. Шаг можно указать через двоеточие, отрицательным он тоже может быть.

Tuple. По аналогии с Python и C# кортежи встроены в базовых типах - ну и правильно, легче сделать чем объяснять всем почему это не сделано (не будем показывать пальцем на парочку очень популярных языков). Синтаксис совершенно привычный, распаковывание кортежей тоже имеет место где это актуально. Кортежи иммутабельны.

приятно что println умеет печатать и массивы и кортежи - или что у них есть ToString метод...
приятно что println умеет печатать и массивы и кортежи - или что у них есть ToString метод...

Unit и Nothing. Про первый уже упоминали - замечательный тип с единственным значением (оно представляется пустыми круглыми скобками). Второй ещё замечательнее - у него на одно значение меньше чем у первого! В общем-то это "технический" тип - его имеют выражения вроде break и continue. Да, они являются выражениями (а не statements).

Null-ы и указатели. Их нет. Есть тип Option<...> который может либо содержать значение данного типа, либо не содержать ничего. Мы такое точно видели в Scala и других подобных языках.

Выражения. Тут создатели языка поступили просто: всё является выражениями, любая вычисляемая (выполняемая) конструкция. В случае с if это понятно - его можно использовать и как "конструкцию ветвления" и как "тернарный оператор" - он просто возвращает значение вычисленное в той ветке, которая выполнилась:

println(if (true) { 5 } else { 8 })

Результатом оператора for является значение типа Unit. Почему не Nothing? В общем тут глубоко полезной логики пока не видно.

Циклы. Присутствуют кроме for-in также и while и даже do-while. Есть интересная возможность использовать where в цикле for - напоминает list-comprehension в питоне:

for (i in 0..8 where i % 2 == 1)

впрочем то же самое можно пожалуй написать используя шаг 2 в описании интервала и выбрав подходящий начальный элемент

for (i in 1..8:2)

поэтому выглядит как избыточная возможность для узкого опционального применения

Операторы для мэтчинга. Не будем углубляться, но есть Enum-ы и Option-ы (уже упоминавшиеся) - и для них "pattern matchinig" - эти фишки присутствуют и в других языках но в синтаксисе как обычно с непривычки леко запутаться.

Быстродействие на простых примерах

Вообще наши типичные приложения в основном теряют время на операциях ввода-вывода (работа с базой, с сетью) - тем не менее и быстродействие самого языка порой имеет значение. Мы ожидаем что Cangjie компилируется в нативный код и выполняется по скорости сравнимо с C++ или Go.

Попробуем, благо теперь мы уже знаем базовые возможности достаточно для того чтобы написать пару простых программ. Я для этой цели держу под рукой репозиторий с небольшим количеством двух коротких программ на разных языках - одна из них оперирует скалярными величинами во вложенных циклах - вторая делает примерно то же с массивами. Воспользуемся первой, этого на первый раз будет достаточно.

Гипотеза Коллатца

Вы знаете эту старинную (ей почти сто лет) нерешенную задачу - начиная с произвольного числа мы либо делим пополам если оно чётное, либо утраиваем и добавляем единицу если нет. Последовательность, по-видимому, всегда приходит к 1 но количество итераций сложно предсказать. Программа вычисляет эти последовательности для всех чисел вплоть до заданного maxn и выдаёт общее количество итераций (для проверки) - получается двойной вложенный цикл. Этот код оставлю в виде текста на случай если кому-то понадобится для экспериментов.

import std.os.*;
import std.convert.*;

main() {
  var maxn = Int.parse(if (let Some(v) <- getEnv("MAXN")) { v } else { "10" })
  var sum = 0
  for (i in 1..(maxn+1)) {
    sum += collatz(i)
  }
  println("sum=${sum}")
}

func collatz(n: Int): Int {
  var cnt = 0
  var res = n
  while (res > 1) {
    res = if (res % 2 > 0) {
      res * 3 + 1
    } else {
      res / 2
    }
    cnt++
  }
  return cnt
}

Здесь мы заодно попробуем пару функций стандартной библиотеки (она также документирована хотя и не супер-подробно) - чтобы количество итераций можно было задать из переменной среды, как и в остальных программах в репозитории.

Что ж, сверим выполнение на C и на Cangjie:

$ time MAXN=3000000 ./a.out
sum=428343467, avg=142.78

real	0m0,734s
user	0m0,729s
sys	0m0,005s

$ time MAXN=3000000 ./main
sum=428343467

real	0m1,026s
user	0m1,012s
sys	0m0,013s

В общем быстродействие характерное для компиляции в нативный код, чуть медленнее C и практически идентично Go. Отметим что при компиляции с настройками по умолчанию вы получите гораздо худший результат - но легко найти что есть ключи оптимизации, например -O2, хотя более тонкие нюансы я пока не разбирал.

Простые числа - быстродействие на массивах

Второй пример отыскивает простые числа и складывает в массив, по которому при поиске каждый раз пробегает. Этот пример использует ArrayList и отрабатывает примерно в 2 раза быстрее чем пример на Go. (код можно видеть в обложке поста).

Объектно-Ориентированные Фишки

Сразу бросается в глаза - язык имеет и классы и структуры. Напоминает ситуацию в C++, но здесь одно не является частным случаем другого. Структуры могут содержать методы но не могут наследоваться. Есть и другие нюансы (например, обычно методы структуры не могут менять её поля). Притом структуры могут реализовывать интерфейсы, как и классы.

В целом реализация ООП близка к Java-нской:

  • наследование присутствует, но только одиночное

  • интерфейсы, реализуемые классом, явно указываются в его определении

  • четыре модификатора доступа

  • абстрактные и "финальные" определения классов

Из отличий, которых нет в Java или Go:

  • возможность переопределять операторы для объектов класса (как в C++)

  • Extensions - возможность расширять чужие классы дополнительными методами в пределах данного модуля (в Go есть похожая возможность но не для стандартных типов, например) - в качестве примера "добавление" метода к строковому типу

нужно нечасто, но будет удобно
нужно нечасто, но будет удобно

Присутствуют Generics, на первый взгляд тоже очень близки к Java-нским. Поскольку тема это обширная, не будем сейчас углубляться (фактически, копировать документацию).

Функциональные Ништяки

На уровне самого языка - они заметны только в виде синтаксиса для лямбда-функции, тут ещё дополнительный синтаксический сахар для случаев "когда параметром функции является одна функция можно опустить скобки" и т.п... В остальном же присутствуют в качестве методов коллекций - похоже на то что появилось в Java 8 но со своими особенностями синтаксиса:

печатает [3, 3, 5, 4, 4]
печатает [3, 3, 5, 4, 4]

То есть функция map применяется к лямбде описывающей преобразование (получение длины от строки) - и на выходе даёт новую функцию, уже собственно мэппер - который можно применить наконец к требуемой коллекции (и получить массив с помощью collectArray).

Возможно есть ещё какой-то "синтаксический сахар" для именно этих "функциональных" возможностей - ибо выглядит всё-таки немного неуклюже - так что если найдёте, поделитесь пожалуйста.

Исключения

Тут углубляться не будем - главное - что они присутствуют, и во многом похожи на Java, за исключением фишки с "принудительным" отловом/пробросом (по-моему это уникально для Java). Есть try-with-resource, типичные ключевые слова try, catch, finally, throw.

Коллекции

Джависты найдут здесь что-то очень родное: ArrayList, LinkedList, HashSet, HashMap - в пакете std.collections который мы уже видели ранее - а также блокирующие и неблокирующие очереди и конкуррентную хэш-мэпу в std.collections.concurrent - в отличие от Go-шных каналов неблокирующая очередь не ограничивает размер принудительно.

Вышеупомянутый базовый тип Array хотя синтаксически похож, но является именно базовым типом фиксированного размера (как и массив в Java) - но в то же время реализует общие для коллекций методы (вообще в смысле унификации с базовыми типами язык больше напоминает C# а не Java).

Многопоточность

Реализована типичная "вытесняющая" модель, для программиста предоставлены типичные Thread-ы, которые выполняются поверх тредов ОС (т.е. "супервизор" выбирает на каком треде ОС выполнять в данный момент тот или иной тред языка) - решение аналогичное "горутинам" в Go - позволяющее меньше зависеть от особенностей ОС и быстрее переключать контексты. И синтаксис в общем-то похож:

а будет ли напечатан второй println раньше чем программа завершится?
а будет ли напечатан второй println раньше чем программа завершится?

Здесь слово spawn вместо go (привет от Erlang?) - а фигурные скобки и "псевдострелочка" - это синтаксис лямбды, описывающей анонимную функцию без параметров. Даже вызов sleep и Duration кажутся подозрительно знакомыми для Go.

Для взаимодействия и синхронизации присутствуют Atomic типы данных, есть и ThreadLocal. Мьютексы используются с ключевым словом synchronized, что отчасти напоминает Java но с той разницей что "монитор" не встроен в любой объект (если меня уже склероз не подводит).

Рефлексия, Аннотации, Макросы

Язык богат этими задумками для "метапрограммирования" или "динамическими возможностями". Правда несколько неожиданно увидеть сразу всё это разом (но кажется и такое бывало). Сведения однако несколько разбросаны по разным участкам разной документации. Мы углубляться сейчас не будем (т.к. это обычно актуально становится в "матёрых" проектах, а матёрые сейчас наверное писать никто из нас на Cangjie не соберется - да и версия языка внушает подозрения что эта-то часть ещё будет меняться).

Есть условная компиляция - можно над объявлениями проставлять условия вроде

@When[os == "Linux"]

нечто промежуточное между билд-тегами в Go и иф-дефами в C++.

Ввод и вывод

Этот раздел я добавлю лишь для того чтобы обратить внимание - на текущий момент соответствующая часть в Development Guide содержит три пустые страницы. Тем не менее в документации API можно пользоваться разделами про std.io и std.fs например (также std.socket) чтобы получить представление об этих возможностях по мере возникновения необходимости.

Инструментарий

В наше время большинство языков не выглядят полноценно без некоторых актуальных инструментов сопровождающих разработку. Посмотрим что нам предлагают:

  • менеджер пакетов cjpm

  • отладчик cjdb

  • профилировщик cjprof

  • форматтер исходников cjfmt

  • подсчет покрытия в тестах cjcov

Также присутствует плагин для VSCode. Я пока не пытался его использовать т.к. не использую VSCode. Гугление позволяет отыскать плагины для Vim и Idea. C компанией IntelliJ мы больше не дружим :) и я в последний год использую как раз vim для go. Плагин установить попробовал - но это просто подсветка синтаксиса, немного ядрёная. Оно и понятно, языковой сервер видимо для Cangjie поддержки ещё не имеет.

немного напоминает игры на ZX-spectrum
немного напоминает игры на ZX-spectrum

Заключение

Пока из того что видно можно сделать вывод - солидная и уже достаточно матёрая вещь - но по сути своей именно "сборная солянка" из фичей присутствующих в топе языков с индекса Tiobe. Чего-то оригинального и нового я с первого подхода не замечаю. Конечно, документация обширная и довольно "раскидистая", так что может просто упустил по невнимательности - с другой стороны если у языка есть какая-нибудь killer-feature, обычно о ней пишут большими буквами во всех пресс-релизах и инструкциях в самом начале.

Интересная ситуация - всяких мелких новых возможностей - много (местами даже, кажется, излишне). А какой-то радикально новой и важной особенности - нет.

Язык Cangjie смотрелся бы довольно круто лет 20 или даже 15 назад, но сейчас по-видимому удивить или завлечь кого-то будет затруднительно (т.к. нужна же инфраструктура, коммьюнити, мегатонны библиотек) - конечно, имея в виду кого-то вне Китая вообще и Huawei в частности.

Комментарии (12)


  1. IisNuINu
    07.08.2025 06:40

    спасибо за обзор!


  1. Tiriet
    07.08.2025 06:40

    алгоритм на КДПВ работает, если for (d in primes) работает снизу вверх, но это из семантики языка не очевидно. А primes- вообще глобальная переменная. Вывод? КДПВ годная, привлекла внимание.


    1. RodionGork Автор
      07.08.2025 06:40

      можно для тех кто в танке - КДПВ это кто, простите? :)


      1. Tiriet
        07.08.2025 06:40

        https://habr.com/ru/articles/376207/comments/#comment_16672575

        Ответ на Ваш вопрос уже есть на Хабре :-)


        1. Tiriet
          07.08.2025 06:40

          Ой как зря Вы его откорректировали, ой как зря...


          1. RodionGork Автор
            07.08.2025 06:40

            будучи в танке откорректировал попросту в соответствии с Вашим комментарием


      1. RodionGork Автор
        07.08.2025 06:40

        а, дошло, всего лишь "картинка для привлечения внимания". она впрочем не для привлечения внимания а просто типовой элемент оформления статьи на хабре. что касается содержания - ну не прилеплять же очередную тётку в выпирающем топике. это сексизьм и к делу не относится.


  1. Jebediah_Kerman
    07.08.2025 06:40

    Результатом оператора for является значение типа Unit. Почему не Nothing? В общем тут глубоко полезной логики пока не видно.

    Потому что нельзя создать инстанс Nothing, а вернуть какое то значение нужно. Nothing обычно в языках ещё и является подтипом всех типов, поэтому continue/break/throw имея такой тип могут быть согласованы с любым другим возвращаемым из функции типом.


    1. RodionGork Автор
      07.08.2025 06:40

      но break ведь возвращает Nothing :)

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


  1. CloudlyNosound
    07.08.2025 06:40

    Очень похоже на язык для отделения своих от чужих. Кто хочет работать в Хуавее, тот изучает. Остальные восторженно сторонятся.

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


    1. RodionGork Автор
      07.08.2025 06:40

      Мне тоже интересно. но как человек мигрировавший с Java в Go (после многих лет в этой самой джаве) я мотивацию отчасти понимаю - джава по нынешним временам кажется немного излишней (нам не нужно "run everywhere" поскольку все запускают в докерах - и например куча уровней приватности - для микросервисов это лишнее). К сожалению Go тоже прям не сказать что со всех сторон огурчик. Поэтому некоторая мотивация есть.

      Но пройдясь по языку я вижу что и он местами впал в грехи излишеств всяких и поэтому-то я и приписал честно что у меня есть сомнения насчет того что он бурно пойдёт в массы.

      Хотя всё зависит от коньюнктуры. Котлин бы в массы не пошёл если бы его не взял под крыло Гугл для Андроида.


      1. CloudlyNosound
        07.08.2025 06:40

        Вот, когда читаешь про очередной супер-язык, сразу и вспоминаешь, и о Go, и об остальных трендах последних лет.

        В принципе, каждому гиганту и должно хотеться иметь свой карманный "заменитель Си или Си++". Нужно же иметь тотальный контроль над экосистемой. Но для народа это буквально ничто.