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

Однако задумывались ли вы о том, как появились те языки программирования, которые мы любим и ненавидим? Как те особенности, которые нам нравятся (или не нравятся), разрабатываются и реализуются и почему? Как работают волшебные черные ящики - компиляторы и интерпретаторы? Как код, написанный на JavaScript, Ruby, Python и т. Д., Превращается в исполняемую программу? Или вы когда-нибудь думали о создании собственного языка программирования?

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

В этой серии статей вы познакомитесь с 0 до 1 в построении функционального интерпретатора для языка программирования. В конце цикла у вас будет интерпретатор, который вы построили с нуля, для запуска программ, написанных на языке программирования, который мы разработали бы вместе.

Почему?

Зачем учиться реализовывать язык программирования?

  • Сборка компилятора или интерпретатора сделает вас лучшим программистом. Компиляторы и интерпретаторы включают в себя интересные структуры данных и алгоритмы, знание которых применимо и полезно для других областей. Вы можете применить эти знания для написания парсеров для различных форматов файлов, создания языков, специфичных для предметной области, например, языка запросов к базе данных,…. Вы также лучше поймете, как и когда оптимизировать код, будете лучше подготовлены, чтобы выбрать лучший язык программирования для конкретной задачи, и вы, наконец, поймете некоторые из тех странных сообщений об ошибках, которые иногда выдает ваш компилятор / интерпретатор :).
  • Участвуйте в развитии вашего любимого языка программирования. Многие интересные языки программирования имеют открытый исходный код и приветствуют новых участников, но часто знания, необходимые для внесения вклада, являются препятствием для входа для большинства людей, которые никогда не проходили курс компилятора CS. Если вы заботитесь о предпочитаемом вами языке программирования и / или когда-либо задумывались о том, чтобы внести свой вклад в его развитие, вы будете лучше подготовлены для этого после создания игрушечного компилятора или интерпретатора.
  • Это очень весело. :)

Как?

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

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

Какие?

Если все вышеперечисленное звучит хорошо, давайте начнем с описания языка программирования, который мы будем реализовывать. Наш язык программирования, специально разработанный для этой серии статей, называется Blink. Это интерпретируемый, объектно-ориентированный и статически типизированный язык программирования, вдохновленный Swift, Kotlin и Scala.

Экскурсия по Blink

Привет, Блинк!

Типичный «Hello, World» записывается следующим образом.

Console.println("Hello, Blink!")

Типы данных

Blink поддерживает значения знакомых типов, таких как Int, Double, String и Bool. Кроме того, существует специальный тип Unit для выражения отсутствия значения (аналогичный void в Java), и все типы в Blink наследуются от супертипа Object.

Комментарии

Комментарий в Blink начинается с //. Все, что находится от символа // до конца строки, будет игнорироваться интерпретатором Blink.

// This is a comment.

Объявление переменных

Переменные объявляются с помощью let выражения.

let message: String = "Hello, Blink!" in {
    Console.println(message)
}

Выражение let состоит из 2 частей:

  • объявление (перед ключевым словом in), в котором переменная объявляется и, возможно, инициализируется. Объявление переменной состоит из имени переменной, за которым следует двоеточие :, за которым следует тип переменной message: String. Чтобы инициализировать переменную при ее объявлении, добавьте равное = и значение переменной message: String = "Hello, Blink!".
  • тело (после ключевого слова in), в котором можно получить доступ к переменной. Переменные, объявленные с let, доступны только в связанном теле.

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

let message = "Hello, World!" in {
    Console.println(message)
}

Одновременное объявление нескольких переменных. Чтобы объявить несколько переменных, разделите их запятыми , в части объявления let выражения.

let a = 42, b = 12, c = 24 in {
    Console.println(a + b + c)
}

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

let a = 42, b = a * 2, c = b * 3 in {
    Console.println(a)
    Console.println(b)
    Console.println(c)
}

Выше будет напечатано

42
84
252

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

let message = "Hello, Blink!" in Console.println(message)

Условия

Как и в большинстве языков программирования, условия выражаются с помощью if-else выражения.

if (<condition>) {
    <do something>
} else {
    <do something>
}

Условие должно оцениваться как значение типа Bool. При необходимости блок else можно опустить, а фигурные скобки можно опустить для блока if или else, если блок состоит только из одного выражения.

if (answer == 42) {
    Console.println("That's the answer of life!")
} else {
    Console.println("That's not the answer of life.")
}

Зацикливание

while выражение используется для выполнения одного или нескольких выражений, пока выполняется условие.

while (<condition>) {
    <do something>
}

Опять же, фигурные скобки можно опустить, если тело while состоит только из одного выражения.

Определение функций

Функции определяются с помощью ключевого слова func.

func sum(a: Int, b: Int): Int = {
    a + b
}

Параметры функции в Blink разделены запятыми , и заключены в скобки (a: Int, b: Int). Каждый параметр определяется своим именем, за которым следует двоеточие :, за которым следует его тип a: Int.

Тип возврата. Тип возвращаемого значения определяется добавлением двоеточия :, за которым следует тип значения, возвращаемого функцией, после списка параметров. Если функция не возвращает значение, тип возврата должен быть Unit.

func greet(): Unit = {
    Console.println("Hello, Blink!")
}

Однако в качестве синтаксического сахара, если функция не объявляет какой-либо тип возвращаемого значения, Blink автоматически предполагает, что типом возвращаемого значения является Unit. Итак, предыдущий пример можно было бы записать следующим образом.

func greet() = {
    Console.println("Hello, Blink!")
}

Тело функции. После типа возвращаемого значения следует оператор равенства =, а затем тело функции, заключенное в фигурные скобки. В Blink нет ключевого слова return, значение последнего выражения в теле функции является значением, возвращаемым функцией.

Если тело состоит только из одного выражения, фигурные скобки можно опустить. Наш первый пример можно переписать так:

func sum(a: Int, b: Int) = a + b

Определение классов

Классы определяются с помощью ключевого слова class.

class Person {
}

Это определяет простой (хотя и бесполезный) класс Person. Объекты класса Person теперь можно создавать с помощью ключевого слова new.

let p = new Person() in { }

Свойства. Класс может иметь одно или несколько свойств, объявленных с ключевым словом var.

class Person {
    var firstname: String = "Klaus"
    var lastname: String
    var age: Int
}

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

Ресурсы в Blink являются частными. Их нельзя сделать общедоступными. Геттеры и сеттеры (которые являются просто функциями) могут быть созданы для доступа к свойствам вне класса.

Функции. Класс может иметь функции (или methods, если хотите).

class Person {
    var firstname: String = "Klaus"
    var lastname: String
    var age: Int
    func firstname(): String = {
        firstname
    }
    func setFirstname(name: String) = {
        firstname = name
    }
}

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

class Person {
    var firstname: String = "Klaus"
    var lastname: String
    var age: Int
    func firstname(): String = firstname
    func setFirstname(name: String) = firstname = name
}

Функции в Blink по умолчанию являются общедоступными. Однако можно сделать функцию частной, добавив модификатор private перед ключевым словом func.

private func age(): Int = // ...

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

class Person(firstname: String, lastname: String) {
    func firstname(): String = firstname
    func setFirstname(name: String) = firstname = name
    func lastname(): String = lastname
    func setLastname(name: String) = lastname = name
}

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

let person = new Person("Klaus", "Baudelaire") in {
    Console.println(person.firstname())
    Console.println(person.lastname())
}

Выше будет напечатано

Klaus
Baudelaire

Наследование. Наследование в Blink выражается ключевым словом extends.

class Employee extends Person {
}

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

class Employee(firstname: String, lastname: String, company: String) extends Person(firstname, lastname) {
}

Переопределение функций. Модификатор override используется для переопределения функции в суперклассе.

class Person(firstname: String, lastname: String) {
    override func toString(): String = firstname + " " + lastname
}

В приведенном выше примере мы переопределяем toString, доступный в классе Object. Все классы наследуются от Object в Blink.

Определение операторов

Подобно тому, как можно использовать арифметические операторы +, -, * или / для выполнения операций между 2 или более Int секунд, Blink позволяет нам определять поведение бинарных операторов +, -, *, /, % и унарных операторов и ! для наших собственных классов.

Бинарный оператор определяется путем добавления функции, имя которой является символом определяемого оператора. Например, давайте определим оператор + для класса Rational (представляющего рациональные числа, такие как 3/4).

class Rational(num: Int, den: Int) {
   func +(other: Rational): Rational = // ...
}

Учитывая две переменные a и b типа Rational, теперь мы можем написать let c: Rational = a + b. Blink автоматически преобразует выражение a + b в вызов функции a.+(b).

Унарный оператор определяется добавлением функции, имя которой начинается с unary_, за которым следует символ определяемого оператора.

class Rational(num: Int, den: Int) {
    func unary_-(): Rational = new Rational(-num, -den)
}

В приведенном выше примере мы определяем поведение унарного оператора в нашем классе Rational. Учитывая переменную a типа Rational, мы можем написать let b: Rational = -a. Blink преобразует выражение -a в вызов функции a.unary_-().

Перегрузка операторов также поддерживается в Blink, что означает, что можно определить несколько + операторов, например, если список параметров каждой операторной функции отличается.

Например, для нашего класса Rational мы можем захотеть сложить 2 Rational вместе, но также хотим добавить Rational и Int. Перегрузка операторов позволяет нам это делать.

class Rational(num: Int, den: Int) {
   func +(other: Rational): Rational = // ...
   func +(integer: Int): Rational = // ...
   func +(double: Double): Rational = // ...
}

В приведенном выше примере и учитывая 2 переменные a и b типа Rational, мы можем написать выражения типа a + b, a + 42 или b + 3.14.

Все является объектом

Все в Blink - это объект. Даже такие примитивы, как Ints или Doubles, на самом деле являются объектами. А арифметическое выражение, подобное 32 + 21, на самом деле является вызовом функции 32.+(21). Это изящная функция, которая позволяет нам, среди прочего, определять операторы для наших собственных типов в Blink.

В Blink нет оператора, только выражения (и определения)

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

Например, в JavaScript инструкция if выполняет определенный блок кода, если условие равно true или false. В Blink if является выражением и оценивается как значение.

Это означает, что в Blink вполне допустимо писать

let isLegallyAdult = if (age > 18) { true } else { false }

isLegallyAdult будет равно true, если age > 18, или будет равно false, если нет.

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

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

Для получения дополнительной информации о том, как конструкции Blink работают вместе, ниже приведены некоторые полные программы Blink.

Вы дошли до конца. 🎉

Хакерский полдень - это то, с чего хакеры начинают свои дни. Мы часть семьи @AMI. Сейчас мы принимаем заявки и рады обсудить рекламные и спонсорские возможности.

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