Pull to refresh

Какие у нас есть клавиши в клавиатуре или как непрограммист язык разрабатывал

Reading time11 min
Views5.3K

Несколько лет назад я решил под свои скромные производственные задачи сделать какую-то такую - не знаю какую - систему, но чтоб была похожа на таблички excel на стероидах.

На вторую неделю разработки нашего супер-пупер-проекта «базы данных для неразработчиков», наш ведущий разраб спрашивает меня — «ок, и как ты хочешь, в одной ячейке таблички вызывать другую? а если из другой таблицы? а как их складывать и умножать?».

Возник выбор — прикручивать JS и SQL (как это делают нормальные люди) или разрабатывать собственный синтаксис. Я недолго подумал и решил, что собственный синтаксис лучше, но при условии, что он будет каким-то таким, незнаю каким, но простым...

Хочу рассказать про некоторые моменты, которые стали понятны в этом процессе — может, вы передумаете делать свой DSL ?

Это ссылка на статью на Habr с описанием проекта и с чрезмерным количеством смайликов

Картинка таблички для примера:

Зачем может быть нужен DSL (domain-specific language)

Ситуация двойственная. Если бы я был программист — однозначно выбрал бы JS или Lua, или что-то известное. Но я не программист. У меня, вроде как, инженерное образование и реально программировать я не умею. То, что на VB писал в институте, чтоб руками не считать задачки по сопромату — это не в счет.

Если мне сказать: «а теперь надо выучить JS», я наверное оставлю это дело. А если сказать: «ну так здесь язык то и не язык вовсе, а такое что-то типа формул, да еще и ориентированный на непрограммистов» — я наверное попробую.

Во вторых, если вы делаете заточенный под приложение DSL, то эффективность использования приложения растет. За это вы платите усилиями на обучение пользователей. Но если подумать, то у Nginx свой язык конфига и у Apache тоже. В CI/CD инструментах тоже и так много где — те все-равно пользователям придется читать даташит.

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

Конечно, здесь не идет речь о языке широкого назначения — я рассказываю про DSL для конкретного приложения.

Перед тем как приступать

Выбрал я разрабатывать синтаксис. Может, это и было печальным решением с коммерческой точки зрения, но сейчас, когда сделанных не нем проектов стало больше 100, я решил немного рассказать, каким образом мы этот Totum-code придумывали. Может, кому-то, кто разрабатывает свой DSL, будет интересен наш опыт.

Опыт здесь такой — если вы идете в DSL, то до момента его шлифовки все печально. И этот период длится довольно долго, у нас он занял 3 года. Но зато потом, вроде, ниче.

Что у нас получилось в итоге

Что-то типа этого:

// Вызываем Ю-кассу из Totum-code:

=: getFromScript(uri: str`"https://" + $store_id + ":" + $store_key + "@api.yookassa.ru/v3/payments"`; headers: $headers; posts: $post)
    store_id: "034942"
    store_key: "test_HUoYJ_cyBhB"

headers: listCreate(item: str`"Idempotence-Key:" + $idempotence`; item: "Content-Type:application/json")
    idempotence: numRand(min: 1000; max: 10000)

post: jsonCreate(data: $post_row)
    post_row: rowCreate(field: "amount" = $amount; field: "capture" = true; field: "confirmation" = $confirmation)
        amount: rowCreate(field: "value" = #total; field: "currency" = "RUB")
        confirmation: rowCreate(field: "type" = "redirect"; field: "return_url" = "https://ru.totum.online")

Или этого:

= : #h_json[[firstLevel_2]][[secondLevel_Date]][$single] * $count

single: $key[0]
	key: listSearch(list: #h_json[[firstLevel_2]][[secondLevel_3]][[id]]; key: "value" = 11)
  
count: listCount(list: $list)
	list: selectList(table: $#ntn; field: 'id')

С правильной подсветкой читается намного лучше :)

Что я могу про это рассказать

Всегда думайте о том, как вы будете потом обучать других людей

Первое, на что я обратил внимание, что очень важно сделать так, чтоб потом было легко объяснить, что в этом синтаксисе происходит. Это оказалось легче сказать, чем сделать, так-как «просто» в этом случае — это синоним слова «логично». Второй пункт — минимальное количество исключений.

Придется быть диктатором

Командная разработка языка на первых этапах – это зло.

Для того, что бы уменьшить количество исключений, надо представлять себе всю систему целиком, во всех вариантах работы одновременно. Т.е. надо запихнуть всю систему в одну голову, разом. Что?!

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

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

Начинайте с чего-то совсем простого и знакомого

Так как у нас таблички, первое, что нужно было решить — это как взять значение из соседнего поля. И здесь начинается известный принцип «а какие у нас есть клавиши в клавиатуре?».

Я подумал, что раз в интернетах активно используется # как обозначение тега, то его и будем использовать — #field_name и получаем вместо него значение этого поля.

Удаляйте лишнее

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

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

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

Например:

Для меня прямо сразу лишним являются элементы синтаксиса, без которых можно было бы обойтись. Например обозначение конца строки ; или какой-то другой символ. Есть же конец строки — это перенос на следующую строку. Поэтому обозначение конца строки без сомнений пошло под нож. Да, мы потеряли возможность делать классные переносы, но, так как большинство строк в нашем случае короткие, могли себе это позволить.

Чуть больше я думал про скобки и вложенность. Дело в том, что когда вы разработчик 80 левела, вложенность это норм: функция, в ней сразу внутри другая функция и все в одном месте. Но я прекрасно понимал, что объяснять мне потом придется неразработчикам, а для них батареи скобок — это боль.

Оставили одни скобки — которые обозначали функцию: listSum(list: $list).

Почему круглые? Просто, фигурные сложно читаются.

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

Откладывайте непонятное, добавляйте очевидное

Иногда сходу не получается придумать хорошее решение. Если вы видите, что решение начинает обрастать лишними элементами — отложите на некоторое время.

Когда мы отказались от скобок, возникло две проблемы:

  • Если нет вложенности, то как быть, если нам результат одной функции надо вложить в другую? это решилось просто — ссылкой на другую строку кода по $. Почему доллар? Потому что это соседняя кнопка с решеткой и второй самый используемый символ в нашем синтаксисе, т.к. вложений много.

    =: listSum(list: $list)
    list: listCreate(item: 1; item: 2; item: 3)

  • К вызову строки нужна ответка — сделали именование строки name:. Почему двоеточие? Потому что мы в обыкновенном письме через двоеточие разворачиваем мысль. Чем больше похоже на нормальную жизнь, тем лучше.

«Ага, а с какой строки код начнет исполнятся, умник?» Спросил у меня разраб.

«Ок, будет обозначена стартовая строка =:», вздохнул я...

  • Если скобки только у функций, как быть с математическими операциями (A + B) * 10? И вот на это сразу ответа не нашлось, поэтому волевым решением решили, что с математикой разберемся позже. По умолчанию все как калькулятор типа работает и на худой конец можно через отдельную строку посчитать:

    =: $sum * 10
    sum: #field_a + #field_b

    Пока что дичь, но задачу, если надо, решит. Учитывая, что большинство вычислений в наших реальных проектах оказались простые и последовательные.

Здесь надо оговорится, что речь идет о вычислении значения в отдельном поле таблицы, и в каждом поле это вычисление производится заново, а сохранение состояния осуществляется сама таблица. Поэтому мы могли сразу отбросить переменные, их определение с типами и тому подобное, а также идти по пути частичной динамической типизации — все строка, числа тоже строка. Но тут сразу встал вопрос «а как же массивы?». Массивы пришлось выделить отдельно. Но на первом этапе оставили только списки, то есть массивы с последовательными ключами от 0.

Функции, заточенные под задачу

У нас весь расчет строится из заранее запрограммированных функций, работающих одним и тем же образом при поступлении одинаковых параметров на вход и не сохраняющих своего состояния — чистых функций. Собственно, такие функции — один из самых удобных инструментов упаковывать ваш опыт в DSL, так как вы знаете, какие бывают ограничения на входе и что должно получатся на выходе.

Не увлекайтесь сокращением

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

А если dateAdd(#date; 10; ; ; ; ; "Y-m-d H:i"; ). Что? Понятное дело, если вы работаете в IDE — она вам подсказывает, какой параметр по счету что означает. А если где-то интегрированное — то не всегда. Сюда пригодились отмененные из концов строк точки с запятой. Могли бы сделать запятые? Могли, но точка с запятой нам показалась более читаемой. Правда если не подписывать параметры, то лучше, наверное, запятую.

Но мы решили подписывать параметры. Вот так: dateAdd(date: ; days: ; hours: ; minutes: ; months: ; years: ; format: "Y-m-d H:i"; lang: ) — больше текста, но понятнее тому, кто первый раз видит. Надо только сделать автозаполнение, чтоб руками не пришлось набирать.

Также такой подход позволяет не указывать часть параметров и ставить их в произвольном порядке (что ухудшает читаемость и мы просим так не делать) — интерпретатору и человеку понятно, какие пропущены: dateAdd(date: #date; days: 10; format: "Y-m-d").

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

Хотя бы в начале, не нарушайте собственные принципы в базовых вещах

Несмотря на то, что хочется пропустить как listSum($list) так и listSum(list: $list), я противник такой истории. Исключения неизбежно будут и даже больше, чем вам хочется. Надо постараться не далать их на самом базовом уровне.

Вовремя остановитесь

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

Сокращайте возможности в пользу упрощения

В нашем случае, анализ потенциального решения показал, что нам не нужно все разнообразие SQL. Поэтому мы отказались от SQL и упаковали запросы к БД в наши же функции:

=: select(table: 'table_name'; field: 'field_name'; where: 'field_name_id' = #anchor)

И здесь мы сознательно ввели ограничения:

  • where сравнивает по пересечениям и не обращает внимание на тип (те 3 = 3 и 3 = "3" и 3 = [1,2,3] и 3 = ["1","2","3"]);

  • Пустота и пустое значение равны для поиска "" = nil;

  • Запретили сравнение больше/меньше со списками;

  • И самое страшное отменили OR, оставили только AND через множественный параметр ...; where: 'f_1' > 10; where: 'f_2' = #sum. Почему отменили OR? Потому что его можно было эмулировать архитектурой решения, и практика применения показала невысокую частоту применения. Поэтому решили, что упрощение в приоритете (непростое было решение, и в PRO версии мы его добавим).

Добавляйте автозаполнение везде, где получится

Здесь нам помог codemirror. За счет того, что мы ввели именованные параметры функций, мы смогли сделать их осмысленное автозаполнение. Единственное, что сначала было неудобно, что хиты подсказок выпадают по делу и без дела. Поэтому мы для параметров, в которых надо указывать названия полей и таблиц, ввели одинарные кавычки и сделали хит автозаполнялки именно по 'table_name'. А также сделали заполнение функции по умолчанию и поиск по ее параметрам.

Не забывайте про хоткеи

Одно дело добавить параметр, другое дело удалить. Заполнил по умолчанию, а там лишние параметры для конкретного случая попали. Сначала не поняли как с этим быть. А потом пришла идея: выделять параметр хоткеем и потом по бэкспейсу его удалять. В зависимости от того, где стоит курсор относительно ;, выделяется параметр вперед или назад.

Вроде бы ничего фантастического, но скорость работы с кодом выросла в 10 раз.

Добавляйте сахар только для тех мест, от бойлерпринта в которых вы сами устали

На первом этапе приходится прилагать усилия, что бы не добавлять в синтаксис сокращения. Это можно делать только поработав несколько месяцев. А некоторые сахара мы добавляли только через 3 года после начала разработки, и их вид кардинально отличался от того, как мы их представили в начале разработки.

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

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

// json`[1,2,3]`

=: select(table: $#ntn; field: 'id'; where: 'type' = json`[1,2,3]`)

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

=: select(table: $#ntn; field: 'id'; where: 'sum' = math`(#field_a + #field_b)/10`)

И уже это решение пошло в рабочую версию.

Комбинации лучше, чем отдельные экзотические символы

В примере выше можно увидеть вот такой элемент —$#ntn. Есть и другие с $#. Дело в том, что тяжко было каждый раз набирать таблицу, и мы пытались приспособить что-то, что возвращало бы какие-то системные константы и переменные. Сначала мы для этого сделали функции без параметров. nowTableName() например возвращало имя текущей таблицы. И именно их на первом этапе мы решили вызывать через быстрые переменные. И тут стало понятно, что из понятных символов есть еще @ % и &.

Проценты были отброшены как слабочитаемые, а амперсанд из-за того, что он на другой стороне клавиатуры. Мне хотелось, что бы тыки в клавиши были рядом. И дальше волей случая @ уехал в другой сахар, а для остальных мы решили использовать комбинации # и $ так как их быстро и привычно набирать. Причем в логике приложения получались разные результаты их комбинаций:

  • $# — системные переменные

  • #$ — определить имя поля через другую строку

  • $$ — определить имя строки через другую строку

  • @ — аналог функции select

И уже сильно потом досыпались:

  • @$ — глобальная внетранзакционная переменная

  • $@ — переменная передаваемая в рамках одного процесса

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

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

Постарайтесь избегать диких цветов в подсветке

Понятно, что все о чем я написал в предыдущем пункте чуть-чуть по разному подсвечивается. Но это какой-то неперадаваемый вопрос вкуса. Иногда открываешь терминал и там такая приятная подсветка, а иногда застрелится можно. Но кто-то же ее сделал такой, наверное ему норм?

Здесь я не знаю — напишите что-нибудь в комментариях, что вы думаете про подсветку, насколько критично делать настройку подсветки для пользователя?

Не пытайтесь казаться крутыми

Признаюсь. Был грешок. Очень хотелось, как в больших языках, добавить что-то крутое. И мы добавили — рекурсии. С теми самыми фигурными скобками :)

=: $split{var: "fruits" = $code2}

 split: if(condition: $#fruits != $#lc; then: $str1; else: "")
    str1: strAdd(str: $#fruits[0]; str: ", "; str: $split{var: "fruits" = $fruitsCut})
        fruitsCut: listCut(list: $#fruits; cut: "first"; num: 1)

code2: listCreate(item: "apple"; item: "banana"; item: "lemon")

// Результат: "apple, banana, lemon, "
// Данный пример приведен здесь как пример, эта конкретная операция выполняется функцией listJoin.

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

Подмечайте, где у вас не стыкуется или не удобно

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

=: linkToInput(title: "Some title"; code: $save_action; button: "Сохранить")

```save_action:totum
=: insert(table: 'some_table'; field: 'some_field' = $#value)
```

И здесь нам снова на выручку пришел маркдаун с его тремя засечками.

Конкретно в этом месте ведущий разраб говорил, что «это невозможно никогда» из-за особенностей предварительной обработки этих кодов интерпретатором.

Но я 4 года не терял надежды, что может быть ситуация разрешится. И это случилось.

Однако, в таких случаях не стоит доставать разрабов тем, что они это никак не сделают. В моем случае, я подходил с этим вопросом не чаще 1 раза в год.

Заключение

Вот как-то так получилось.

Что люди делают можно посмотреть в этой ветке форума на GitHub.

Если вы тоже разрабатывали DSL — поделитесь в комментах своими находками.

Tags:
Hubs:
Total votes 7: ↑6 and ↓1+8
Comments32

Articles