Как стать автором
Обновить

Каркас нового языка программирования

Время на прочтение11 мин
Количество просмотров6.1K

geektimes.ru/post/100171:
I'm always delighted by the light touch and stillness of early programming languages. Not much text; a lot gets done. Old programs read like quiet conversations between a well-spoken researcher and a well-studied mechanical colleague, not as a debate with a compiler. Who'd have guessed sophistication bought such noise?

– Dick Gabriel

...

The standard languages (Java, C++) are statically typed.

Most outsider languages (Ruby, Python, JavaScript) are interpreted and dynamically typed.

Perhaps as a result, non-expert programmers have confused "ease of use" with interpretation and dynamic typing.

The world has split into a false dichotomy: nice, interpreted, dynamic versus ugly, compiled, static.

Time to put things right.
Меня всегда восхищала лёгкость и спокойствие ранних языков программирования. Немного текста; но делается многое. Старые программы читаются как тихие разговоры между хорошо говорящим исследователем и хорошо обученным механическим коллегой, а не как дебаты с компилятором. Кто бы мог подумать что изощрённость принесёт так много шума?

— Дик Габриел

...

Стандартные языки (Java, C++) — статически типизированные.

Большинство языков-аутсайдеров (Ruby, Python, JavaScript) — интерпретируемые и динамически типизированные.

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

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

Пришло время чтобы исправить эту ситуацию.

На этой пафосной ноте перейду к примерам на новом языке программирования.

Вот маленький пример кода:

[Код оформлен на основе идей из следующей статьи.]
(Попробуйте угадать значения зарезервированных букв, а также мысленно переписать этот кусочек кода на языках программирования, которые вы знаете.)

А вот пример побольше (это перевод Python-кода из моей предыдущей статьи).
Код, использующий [зарезервированные] буквы
F calculate_sacred_number()
   A results = []

   L(hash_algorithm) hashlib:algorithms_available // Обходим все доступные хэш-алгоритмы
                                                  \\ (список включает в себя MD5, SHA...)
      I.unlikely "shake" C hash_algorithm // Пропускаем алгоритмы SHAKE, так как ...
         L.continue
      L(uppercase) 0B..1B // Проверяем варианты написания как строчными, так и ПРОПИСНЫМИ
         L(space)  0B..1B // Проверяем варианты написания с дефисом и через пробел
            L(n)   10..99 // Проверяем все двузначные числа
               A nw = :numbers[n] // Получаем текущее число, записанное словами на англ.
               I uppercase
                  nw .= upper()
               I space
                  nw .= replace(‘-’, ‘ ’)
               A ns = String(n)
               // Считаем хэш от записанного словами числа,
               A digest1 = hashlib:(hash_algorithm, nw.encode()).hexdigest()
               // а также от этого же числа, преобразованного в строку.
               A digest2 = hashlib:(hash_algorithm, ns.encode()).hexdigest()
               L 2 // Проверяем целый хэш, а также первую половину хэша
                  // Оба хэша должны начинаться на первую цифру текущего числа
                  // и заканчиваться на вторую цифру.
                  I  digest1[0] == ns[0] & digest1.last == ns[1]
                   & digest2[0] == ns[0] & digest2.last == ns[1]
                     results [+]= ns
                  // Берём первую половину хэша
                  digest1 = digest1[0 .< (len) I/ 2]
                  digest2 = digest2[0 .< (len) I/ 2]

   assert(results.len == 1) // Должно быть только одно "выигравшее" число
   R results[0]             // Возвращаем это число

// Based on [https://stackoverflow.com/a/8982279/2692494 ‘How do I tell Python to convert integers into words’]
A numbers = "zero one two three four five six seven eight nine".split()
numbers [+]= "ten eleven twelve thirteen fourteen fifteen sixteen".split()
numbers [+]= "seventeen eighteen nineteen".split()
L(tens) "twenty thirty forty fifty sixty seventy eighty ninety".split()
   L(ones) numbers[0..9]
      numbers [+]= I ones == "zero" {tens} E tens‘-’ones

print(calculate_sacred_number())
Код, использующий [зарезервированные] слова
fn calculate_sacred_number()
   var results = []

   loop(hash_algorithm) hashlib:algorithms_available // Обходим все доступные хэш-алгоритмы
                                                     \\ (список включает в себя MD5, SHA...)
      if.unlikely "shake" in hash_algorithm // Пропускаем алгоритмы SHAKE, так как ...
         loop.continue
      loop(uppercase) 0B..1B // Проверяем варианты написания как строчными, так и ПРОПИСНЫМИ
         loop(space)  0B..1B // Проверяем варианты написания с дефисом и через пробел
            loop(n)   10..99 // Проверяем все двузначные числа
               var nw = :numbers[n] // Получаем текущее число, записанное словами на англ.
               if uppercase
                  nw .= upper()
               if space
                  nw .= replace(‘-’, ‘ ’)
               var ns = String(n)
               // Считаем хэш от записанного словами числа,
               var digest1 = hashlib:(hash_algorithm, nw.encode()).hexdigest()
               // а также от этого же числа, преобразованного в строку.
               var digest2 = hashlib:(hash_algorithm, ns.encode()).hexdigest()
               loop 2 // Проверяем целый хэш, а также первую половину хэша
                  // Оба хэша должны начинаться на первую цифру текущего числа
                  // и заканчиваться на вторую цифру.
                  if digest1[0] == ns[0] & digest1.last == ns[1]
                   & digest2[0] == ns[0] & digest2.last == ns[1]
                     results [+]= ns
                  // Берём первую половину хэша
                  digest1 = digest1[0 .< (len) I/ 2]
                  digest2 = digest2[0 .< (len) I/ 2]

   assert(results.len == 1) // Должно быть только одно "выигравшее" число
   return results[0]        // Возвращаем это число

// Based on [https://stackoverflow.com/a/8982279/2692494 ‘How do I tell Python to convert integers into words’]
var numbers = "zero one two three four five six seven eight nine".split()
numbers [+]= "ten eleven twelve thirteen fourteen fifteen sixteen".split()
numbers [+]= "seventeen eighteen nineteen".split()
loop(tens) "twenty thirty forty fifty sixty seventy eighty ninety".split()
   loop(ones) numbers[0..9]
      numbers [+]= if ones == "zero" {tens} else tens‘-’ones

print(calculate_sacred_number())

Как можно заметить, импорт модулей осуществляется неявно — можно просто обращаться к нужной функции пиша имя_модуля:имя_функции(...). То есть, простое обращение к fs:path:dirname(), re:compile()\рв:компил(), math:log()\матем:лог(), time:sleep()\время:спи(), json:load(), html:escape() автоматически импортирует все необходимые модули (fs, fs:path\фс:путь, re, math, time, json, html).

Для сырых строк можно использовать одиночные парные кавычки — на мой взгляд строки в таких кавычках выглядят понятнее. [Ещё варианты для сырых строк: '"строка"', ''""строка с "'""'', `строка`.]
Согласен, что этот пример не слишком впечатляет не так уж и проще Python кода, на котором он был основан, но предлагаемый язык, в отличие от Python, — компилируемый язык со статической типизацией, дающей преимущества как в производительности (с возможностью без проблем объявлять функции\closure в теле циклов), так и в безопасности (тот же Cython не обнаруживает такие ошибки на этапе компиляции: "http://...?rev=" + revision).

Язык поддерживает всего 11/12/13 базовых зарезервированных букв/слов:
Буквы Слова Пояснение
A var перем объявление переменной автоматического типа (аналог auto из C++11)
C in С contained in\содержится в
I Е if если
E И else иначе
exception исключение
F Ф fn фн объявление функции
L Ц/П loop цикл/повтор/пока замена for, while и do-while
N Н null нуль
R Р return вернуть вернуть результат функции
S В switch выбрать switch/select
T type тип объявление нового типа
T() typeof тип() получить тип выражения (typeof в GCC, decltype в C++11)
X Э extra экстра для дополнительных зарезервированных подслов
(Исходные файлы на языке должны быть оформлены единообразно — либо используя зарезервированные слова, либо используя буквы.)

Данный/предлагаемый язык дополнительно отличается от других тем, что его зарезервированные слова идут не просто в виде списка (как принято в[о всех[?]] других языках программирования), а структурированы в иерархию, на верхнем уровне которой располагаются 11/12/13 корневых/базовых зарезервированных букв/слов.
Примеры зарезервированных подслов:
I\Е I.likely\Е.часто
I.unlikely\Е.редко
Е\И E.try
E.throw
E.catch\И.перехват
E.try_end (аналог try-else в Python)
F\Ф F.args\Ф.арг
F.virtual.new\Ф.виртуал.новая
F.virtual.override\Ф.виртуал.переопр
F.virtual.final\Ф.виртуал.финал
F.virtual.abstract\Ф.виртуал.абстракт
F.destructor\Ф.деструктор
L\Ц L.continue\Ц.продолжить
L.on_continue\Ц.при_продолжении
L.break\Ц.прервать
L.on_break\Ц.при_прерывании
L.again — перейти к началу цикла
L.was_no_break\Ц.не_был_прерван
L.index — номер текущей итерации цикла (начиная с 0)
L.next\Ц.след — следующее значение переменной цикла
L.prev\Ц.пред — предыдущее значение переменной цикла
S S.break
S.fallthrough
T T.base\Т.базовый — для доступа к базовому типу
T.enum\Т.перечисл — объявление перечисления


Два вида деструкторов


  1. Вызывается при выходе из области видимости (как обычный деструктор в С++ и других языках с поддержкой RAII) — F.on_scope_exit (также эту запись можно использовать как аналог defer в Go и scope(exit) в D).
  2. Объект разрушается сразу после последнего использования/обращения — F.destructor.

Основным я предлагаю сделать второй вариант.
Это позволит удалять временный файл сразу после окончания работы с ним без явного вызова close():
tmpfile, fname = tempfile.mkstemp(text=True)
tmpfile = open(tmpfile)
r = subprocess.call(cmd, stdout = tmpfile, stderr = tmpfile)
tmpfile.seek(0)
print(tmpfile.read(), end = '')
tmpfile.close() # этот вызов можно не делать, если tmpfile будет разрушен в предыдущей строке
os.remove(fname)
А также избавит от необходимости писать move() в случаях:
Person p
p.name = ...
p.age = ...
persons.append(p) // в С++ здесь пришлось бы писать persons.push_back(std::move(p)) для оптимальности
А также позволит избежать копирования при вызове функции sorted()\сортй():
for root, dirs, files in os.walk(path):
    for file in sorted(files): # в C++ бы пришлось писать что-то вроде (files.sort(), files) для избежания копирования массива files
        ...

Развивая идею вывода типов (type inference)


Для начала скажу про такой момент, что в предлагаемом языке есть такие служебные функции как copy()\скоп() и share()\разд().
Тип умного указателя я предлагаю определять по его использованию (также как тип массива или словаря в Nemerle определяется по типу первого добавленного в него элемента). Получается эдакий autounishared_ptr — гибрид unique_ptr и shared_ptr из C++11. Если где либо в коде встречается share(p), тогда p становится shared_ptr-ом, иначе остаётся unique_ptr-ом.

Функция copy()\скоп() используется для копирования тяжелых объектов, которое, как я считаю, должно быть обозначено явно в коде (просто через оператор = можно копировать только лёгкие объекты).
Например, рассмотрим такую строчку кода (a1 и a2 являются массивами):
a2 = a1;
В языке C++ в этом случае будет выполнено поэлементное копирование массива. А в языке Python эта же строка сделает a2 ссылкой на a1. Я предлагаю всегда писать явно: a2 = copy(a1) для копирования, и a2 = share(a1) для создания новой ссылки на массив.
(Более подробное описание предлагаемого мной механизма работы с памятью — это тема для отдельной статьи, но вкратце скажу, что я предлагаю модель памяти несколько отличную от C++ и Rust (но также без сборщика мусора).)

Задумывались ли вы над тем, что флаги открытия файла можно не указывать явно, а выводить из его использования?
A fstr = File(fname).read()  //  А фстр = Файл(имя).прочитать()
File(fname).write(contents)  //  Файл(имя).записать(содержимое)

I File(fname)    // заменяется компилятором на I fs:is_file(fname)
File(fname).size // заменяется компилятором на fs:stat(fname).size

Единая/одна конфигурация сборки


Сталкивались ли вы с проблемой отладки Release/Optimized сборок? Или низкой производительностью Debug сборки? Или с различным поведением в Debug и Release/Optimized? Возможно, проблемы от такого разделения возникают не так уж часто, но когда/если они возникают, это становится большой головной болью для программистов. Я считаю, что стоит попробовать избавить программистов-пользователей языка от такой боли путём отказа от этого разделения, добавив полноценную отладку в Release/Optimized build. К примеру, взять gdb, он выдаёт optimized out в случае когда переменная не хранится в стеке. Но если переменная используется, то где-то же (в регистре или где-либо в памяти) она хранится! Почему в отладочной информации это не предусмотрели — для меня загадка. Ведь посмотреть значение регистров можно на каждом шаге отладки, а значения переменных, хранящихся в регистрах, уже нельзя.

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

Явное обозначение области видимости переменных при обращении к ним (синтаксическая соль)


Глобальные переменные я предлагаю обозначать префиксом : (смотри :numbers в примере выше).

Переменные объектов — префиксом . (так, например, .x означает обращение к переменной[/члену] объекта с именем x), переменные типов (или статические\static в терминологии C++) — префиксом .:.

Префикс @ (символ похож на объединение двух букв C и a — Capture\Схватить) — для захвата внешних переменных внутри локальных функций, аналог nonlocal из Python.

Префикс ^ — для доступа к переменным из внешней ‘области видимости’\scope, это может быть полезно во время отладки (например, есть цикл по i, внутри него ещё какой-то цикл, внутри которого ещё маленький цикл по i, находясь в котором хочется получить текущее значение переменной i верхнего уровня, это можно сделать посредством записи ^i). Также ^ можно использовать для возврата из внешней функции внутри локальной функции:
F outer_func(...)
   F local_func(...)
      ^R // (or ^(outer_func)R) return from outer_func 

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

P.S. В заключение приведу ещё несколько примеров кода.
T Person
   String name
   Int age

   F (name, age)
      .name = name // or (.).name = name, because (.) is this/self
      .age  = age

A persons = []
persons [+]= Person("Name", 17)

// Translation of Python's `def parenthesize(s: Union[str, bytes]) -> Union[str, bytes]: ...` from [http://neopythonic.blogspot.com]:
F parenthesize(T C (String, Bytes) s)
   ...

// Doubly linked list
T DLListItem[T Type]
   Type& prev // ‘unsafe pointer’/‘unowned reference’ [true weak pointer/reference `Type??` is much more expensive]
   Type? next

   F is_in_list // if method is defined without parentheses, than it must be called also without, i.e.: I it.is_in_list {...} 
      R next != N

T DLList[T Type(DLListItem)] // Type(DLListItem) means that Type must be derived from DLListItem
   Type? first
   Type& last

   F.destructor // destructor is needed/necessary because some list item may be shared, and in that case it will not be removed from the list and also all following items will not be removed
      .clear()

   F clear()
      Type? p = move(first) // this also sets `first` to N
      L p
         p.prev = N
         Type? n = move(p.next) // this also sets `p.next` to N
         p = n // move(n) is not needed here as the compiler put `move` automatically at all places of last use of variable
      last = N

   F append, [+]=(Type &item) // define both method `append()` and operator [+]=
      item.prev = .last
      I .last {.last.next = item} E .first = item
      .last = item

   F calc_len()
      Int len = 0
      L (.)
         len++
      R len

   F L
      F () -> Type& // returns iterator to the first element of this container
         R .first
      F next(it) -> Type&
         R it.next
      F prev(it)
         R it.prev


Т ЛичныеДанные
   Строка имя
   Цел возраст

   Ф (имя, возраст)
      .имя = имя // или (.).имя = имя, так как (.) это this/self
      .возраст = возраст

А массив_лд = []
массив_лд [+]= ЛичныеДанные("Имя", 17)

// Перевод Python-кой записи `def parenthesize(s: Union[str, bytes]) -> Union[str, bytes]: ...` из [http://neopythonic.blogspot.com]:
Ф заключить_в_скобки(Т С (Строка, Байты) с)
   ...

// Двусвязный список
Т ЭлДССписка[Т Тип]
   Тип& пред // небезопасная ссылка/указатель [честный слабый указатель `Тип??` значительно дороже]
   Тип? след

   Ф в_списке // если метод объявляется без скобок, то вызывать его необходимо также без скобок, например: Е эл.в_списке {...} 
      Р след != Н

Т ДССписок[Т Тип(ЭлДССписка)] // Тип(ЭлДССписка) означает, что Тип должен быть производным от ЭлДССписка
   Тип? первый
   Тип& последний

   Ф.деструктор // деструктор нужен, так как какой-то элемент списка может быть shared\разделён, и в этом случае он не будет удалён из списка также как и последующие элементы
      .очистить()

   Ф очистить()
      Тип? п = перем(первый) // это также сбрасывает `первый` в Н
      Ц п
         п.пред = Н
         Тип? с = перем(п.след) // это также сбрасывает `п.след` в Н
         п = с // перем(с) здесь не обязательно, так как компилятор вставляет `перем` автоматически в места последнего использования переменной
      последний = Н

   Ф добавить, добавь, [+]=(Тип &эл) // также может быть полезно для "локализованных" исходных файлов: F append,добавить ...
      эл.пред = .последний
      Е .последний {.последний.след = эл} И .первый = эл
      .последний = эл

   Ф вычисл_длину()
      Цел длина = 0
      Ц (.)
         длина++
      Р длина

   Ф Ц
      Ф () -> Тип& // возвращает итератор контейнера (в данном случае — указатель на первый элемент списка)
         Р .первый
      Ф след(эл) -> Тип&
         Р эл.след
      Ф пред(эл)
         Р эл.пред

Ещё вот код по рисованию диаграммы, которая помогла мне определить оптимальное время для публикации данной статьи: сетевая часть (на Python и новом языке программирования) и сохранение файла изображения [диаграммы] (на C++ и новом языке программирования).
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Какое название вы бы дали предлагаемому языку программирования?
0.7% 11a1
2.11% 11c (c — characters)3
0.7% 11l (l — litterae)1
0.7% 11s (s — symbols)1
2.82% другое название4
92.96% я против создания подобного языка программирования132
0% Дюжинакс (12 ключевых слов)0
0% 13kw (13 keywords)0
0% 13rw (13 reserved words)0
Проголосовали 142 пользователя. Воздержались 56 пользователей.
Теги:
Хабы:
Всего голосов 33: ↑7 и ↓26-19
Комментарии161

Публикации

Истории

Ближайшие события