admin 03 сентября 2017

Nim: совершенный язык программирования

Статья раскрывает самые интересные особенности языка программирования Nim. Прочти, возможно, это окажется твоим выбором!

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

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

Совершенный язык

Сегодня мы обсудим такой малоизвестный нам  язык программирования как: Nim.

В настоящее время Nim можно назвать совершенным для меня языком по сравнению с Rust, C++, D, Java, JavaScript, Ruby, PHP и так далее.

Nim являет собой успешно объединённые концепции иных языков, таких как Python, Lisp, C, Object Pascal, Ada, Modula-3 и только лишь автор знает, каких еще.

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

Исходя из этого, начнём.

Особенности моего совершенного языка:

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

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

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

— Выразительность. Язык должен раздвигать рамки принятого и быть благосклонным к разного вида DSL. Ruby, Perl и несколько других языков имеют в этом успехи.

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

— Низкий уровень.

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

— Метапрограммирование. Мы не нуждаемся в использовании средств, которые создают код на нашем языке. На этапе компиляции нам необходимо, чтобы этот процесс происходил в рамках нашего языка. В числе компилируемых языков мы можем видеть Lisp, D и Rust.

— Портируемость.  Код на нашем языке обязан запускаться повсюду, так же как на С. Каждая ОС, каждая структура, в том числе ARM, PIC и AVR, применяемый в неких Arduino. Скорее было бы лучше запускать его в браузерах, поддерживающих Javascript!

Совместимость с религиозно-несовместимыми экосистемами

За десятки дней развития C, C++ и Java было написано масса грамотных библиотек. В момент нужды было бы неплохо примененить их в проекте, написанном на нашем совершенном языке.

Это может прозвучать фантастично, но Nim отвечает вышеуказанным требованиям, я бы тоже не поверил, если бы не опробовал на себе.
О синтаксисе языка, а также как начать писать на Nim и прочих мелочах вы узнаете на этих сайтах:

— Nim by example
— How I start
— Официальное руководство

В этой статье я бы хотел продемонстрировать более сложные способности Nim, почерпнув некие тонкости из презентации создателя языка Nim, Андреаса Румпфа (Andreas Rumpf), не так давно прошедшей в рамках OSCON.

Удостовериться в том, что подобные возможности могут вызывать не только академический интерес, можно  проанализировав, исходники проекта nimx, который мы создаём и применяем в нашем игровом проекте. В особенности nimx поддерживает помимо компиляции в машинный код, ещё и в Javascript+WebGL.

Шаблоны

Приступим к рассмотрению выразительности и расширяемости синтаксиса:

template html(name, body) = # Объявляем шаблон, который объявляет
  proc name(): string = # процедуру с именем name, которая возвращает строку,
    result = "<html>" #  состоящую из открывающегося тега,
    body # того, что в нее добавит код body
    result.add("</html>") # и закрывающегося тега

template head(body) = #  Объявляем шаблон, предназначенный для использования внутри шаблона html
  result.add("<head>")
  body
  result.add("</head>")

...

template title(x) = # Этот шаблон принимает выражение, которое может быть преобразовано в строку,
  result.add("<title>$1</title>" % x) # которая впоследствии станет содержимым тега title

template li(x) = # Этот шаблон работает аналогично шаблону title
  result.add("<li>$1</li>" % x)

# Используем вышеописанные шаблоны:
html mainPage:
  head:
    title "The Nim programming language"
  body:
    ul:
      li "efficient"
      li "expressive"
      li "elegant"

echo mainPage()

Произведение этого кода отобразит окончательный результат:

<html>
  <head><title>The Nim programming language</title></head>
  <body>
    <ul>
      <li>efficient</li>
      <li>expressive</li>
      <li>elegant</li>
    </ul>
  </body>
</html>

Заглянув в промежуточный С-код любознательный читатель, увидит то, что весь HTML-код записан одной C-строкой. За это несёт ответственность внушительный механизм сокращения констант (constant folding), осуществлённый в компиляторе Nim.

Макросы

В случае если сильная сторона шаблонов по каким-либо причинам вас не убедила, тогда на подмогу придёт тяжелая артиллерия — макросы. В Nim это методы, воспринимающие узлы AST (Abstract Syntax Tree — результат синтаксического анализа языка) в виде доказательств и возвращающие модифицированный AST.

Дальнейший образец диктует необходимость расширенных знаний языка. Для этого попытаемся дополнить в Nim анализ покрытия кода (code coverage). В качестве эксперимента пример может служить данный способ:

proc toTest(x, y: int) =
  try:
    case x
    of 8:
      if y > 9: echo "8.1"
      else: echo "8.2" # не покрыто
    of 9: echo "9" # не покрыто
    else: echo "else"
    echo "no exception"
  except IoError:
    echo "IoError" # не покрыто

toTest(8, 10)
toTest(10, 10)

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

# Это код, который будет сгенерирован нашим макросом!

var
  track = [("line 11", false), ("line 15", false), ...] # Флаги о прохождении потока управления
                                                        # через контрольные строки

proc toTest(x, y: int) =
  try:
    case x
    of 8:
      if y > 9:
        track[0][1] = true # Контрольная строка
        echo "8.1"
      else:
        track[1][1] = true # Контрольная строка
        echo "8.2"
    of 9:
      track[2][1] = true # Контрольная строка
      echo "9"
    else:
      track[3][1] = true # Контрольная строка
      echo "foo"
    echo "no exception"
  except IoError:
    track[4][1] = true # Контрольная строка
    echo "IoError"

toTest(8, 10)
toTest(1, 2)

# Выводим результат анализа покрытия кода
proc listCoverage(s: openArray[(string, bool)]) =
  for x in s:
    if not x[1]: echo "NOT COVERED ", x[0]

listCoverage(track)

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

import macros

macro cov(n: untyped): untyped = # Наша цель
  result = n # AST остается прежним
  echo treeRepr n # Вывести структуру AST

cov: # Применяем макрос
  proc toTest(x, y: int) =
    try:
      case x
      of 8:
        if y > 9: echo "8.1"
        else: echo "8.2"
      of 9: echo "9"
      else: echo "foo"
      echo "no exception"
    except IoError:
      echo "IoError"

  toTest(8, 10)
  toTest(10, 10)

В итоге сбора данных мы можем видеть следующее:

...
      TryStmt
        StmtList
          CaseStmt
            Ident !"x"
            OfBranch
              IntLit 8
              StmtList
                IfStmt
                  ElifBranch
                    Infix
                      Ident !">"
                      Ident !"y"
                      IntLit 9
                    StmtList [...]
                  Else
                    StmtList [...]
            OfBranch
              IntLit 9
              StmtList
                Command
                  Ident !"echo"
                  StrLit 9
            Else
              StmtList
                Command
                  Ident !"echo"
                  StrLit foo
          Command [...]
        ExceptBranch
          [...]

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

## Code coverage macro

import macros #  Для манипуляции узлами AST мы используем функции из стандартного модуля macros

proc transform(n, track, list: NimNode): NimNode {.compileTime.} =
  # Вспомогательная процедура transform, вызываемая макросом во время компиляции
  result = copyNimNode(n)
  for c in n.children:
    result.add c.transform(track, list)

  # Рассматриваем AST ветвления
  if n.kind in {nnkElifBranch, nnkOfBranch, nnkExceptBranch, nnkElse}:
    let lineinfo = result[^1].lineinfo

    template trackStmt(track, i) =
      track[i][1] = true
    result[^1] = newStmtList(getAst trackStmt(track, list.len), result[^1])

    template tup(lineinfo) =
      (lineinfo, false)
    list.add(getAst tup(lineinfo))

macro cov(body: untyped): untyped = # Собственно, макрос
  var list = newNimNode(nnkBracket)
  let track = genSym(nskVar, "track")
  result = transform(body, track, list)
  result = newStmtList(newVarStmt(track, list), result,
                   newCall(bindSym"listCoverage", track))
  echo result.toStrLit # Ради отладки, выведем измененный код

cov: # Применяем макрос
  proc toTest(x, y: int) =
    ...

  toTest(8, 10)
  toTest(10, 10)

Результат проведения:

8.1
no exception
else
no exception
NOT COVERED coverage.nim(42,14)
NOT COVERED coverage.nim(43,12)
NOT COVERED coverage.nim(47,6)

Отсюда следует, что меньше чем в 30 строках кода мы смогли создать дельный функционал, для которого в других языках понадобились бы дополнительные средства. При помощи макросов у вас есть возможность увеличивать в языке количество функций, добавив те которых вам не хватает: интерполяция строк, pattern matching или что-то еще. Проверенный синтаксис Nim гарантирует определенность грамматики при применении расширений. Заметив «загадочный» DSL-код, вы всегда знаете, с какими аргументами вызвать grep ;).

Управление памятью

Как я уже говорил ранее, главной стороной языка программирования является присутствие развитых средств управления памятью. Nim предоставляет возможность применения как ручного, так и автоматического способа управления памятью. Поэтому в нём представлены такие модификаторы, как ref и ptr.

Разберём их функции на примере объектов:

type Person = object
  name: string

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

var p = Person(name: “John”)

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

В противовес этому, у нас есть возможность осуществлять выделение памяти под объект в контролируемой сборщиком мусора куче, применяя функцию new:

var pp = Person.new()

Тогда переменная pp опираться на объект, находящийся в хранилище, контролируемом сборщиком мусора, а действительным тип переменной будет ref Person (ref — управляемая ссылка). Подобные объекты передаются в коде по ссылкам и умирают тогда, когда ссылки на такой объект в памяти отсутствуют.

Сейчас приступим к заключительному, самому низкоуровневому способу работы с памятью, точнее – к процессу работы с неконтролируемым  хранилищем памяти, которая проводится при помощи функций alloc, realloc и dealloc, по поведению аналогичная C-шные malloc, realloc и free.

var up = cast[ptr Person](alloc(sizeof(Person)))
# ...
dealloc(up)

Как вы можете видеть из примера, в этой ситуации переменная up, как и в предыдущем, сменяет тип, то есть ее тип — не Person, а ptr Person, что означает рискованный неконтролируемый указатель, хранящий адрес объекта в памяти.

Наряду с этим в момент потребности, для комфортного применения данных типов с модификаторами, определенно их следует отметить при помощи ключевого слова type:

type
  PPerson = ref Person
  UPerson = ptr Person

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

Взаимосвязь с другими экосистемами

В структуре Nim находится C, C++, Objective-C или JavaScript как переходный код. Это означает то, что применение библиотек, прописанных на этих языках, довольно банально. Иные языки  обычно предполагают механизмы расширения через C-интерфейс. Даже в этом Nim управляется отлично, допуская писать бриджи к другим языкам, как библиотеки.

Итак, я обрисовал небольшую библиотеку jnim, понятную на GitHub. jnim делает возможным «импортировать» модули Java. Отражается это так:

import jnim

jnimport:
  # Импортируем пару классов
  import java.lang.System
  import java.io.PrintStream

  # Импортируем статическое свойство
  proc `.out`(s: typedesc[System]): PrintStream

  # Импортируем метод
  proc println(s: PrintStream, str: string)

# Запускаем JVM. Это делать необязательно, если JVM уже запущен, к примеру, на Android.
let jvm = newJavaVM()

# Вызываем! :)
System.`.out`.println("This string is printed with System.out.println!")

Всё волшебство возникает в глубине jnim. Для любого обозначения jnimport образовывается аналогичное содержание в Nim, и создается весь нужный glue-код. В дальнейшем итогом развития jnim послужит перспектива того, что можно будет не отмечать поля и процедуры, а автоматически импортировать обозначения классов из Java-окружения на этапе компоновки.

Заключение

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

Кроме этого мы пропагандируем данный язык внутри Альянса, проводим некоторые образовательные мероприятия для наших сотрудников, а также в планах стоит пригласить в Украину автора Nim, Андреаса Румпфа.

Любопытно, а кто также развивается в этом направлении в Украине? Буду рад прочесть в комментариях ваши мнения на этот счёт. Есть ли у вас опыт использования Nim? Сталкивались ли вы с задачами, для решения которых Nim был бы более результативным инструментом?

Комментарии

ВАКАНСИИ

Добавить вакансию
Разработчик C++
Москва, по итогам собеседования

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ