Возможно, у вас уже есть программа на Python, Ruby или JavaScript. Вы умеете пользоваться языком. Но вы когда-нибудь задумывались, как они работают в основном?

Это то, что я собираюсь показать вам на примере языка программирования Lua? Почему Lua, а не более известный язык, такой как Python?

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

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

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

Lua Quick Intro

Но прежде чем я вам это покажу, позвольте мне кратко познакомить вас с самим языком, с тем, как вы определяете переменные, функции и выполняете цикл for.

Определите две локальные переменные. Число и строка

local number = 20 
local str = "Hello world"

Глобально определенный массив чисел

numbers = {2, 4, 6, 8}
second_number = numbers[2]

Хеш-таблица

person = {name = "Erik", phone = 9364, company = "roxar"}

Сменить участника

person["phone"] = 543

Определить структуру

person = {}
person.name = "Erik"
person.phone = 9364
person.company = "roxar"

Структура управления

if person.name == "Erik" then
  print("That is me")
else
  print("I don't know who "..person.name.." is")
end

Определить функцию

function square(x)
  return x*x
end

Стандарт для петли

for index, value in pairs(numbers) do
  print("square of "..value.." is "..square(value))
end

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

Магия хеш-таблиц

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

Это немного похоже на то, как весь мир состоит из атомов. Единственный эквивалент информатики, о котором я могу думать, - это LISP, где все является списком, или Tcl, где все является строкой.

Вам нужен массив в Lua, просто используйте хеш-таблицу с целочисленными индексами в качестве ключей. Хотите структуру в стиле C, просто используйте хеш-таблицу, где ключи представляют собой строки с теми же именами, что и поля. Хотите класс или объект, используйте словарь! Что ж, мы забегаем вперед. Давайте сначала рассмотрим несколько примеров кода.

rectangle = {"height" = 10, "width" = 20 }

Однако до тех пор, пока строковые ключи не содержат пробелов и начинаются с буквы, мы можем записать это как:

rectangle = {height = 10, width = 20 }

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

rectangle["height"] = 150

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

rectangle.height = 150

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

Функции как объекты

Функции - это объекты первого класса в Lua. Раньше это было круче, теперь я думаю, что для языков очень необычно не делать функции первоклассными объектами. Это означает, что вы можете передавать и хранить функции так же, как вы передаете числа и строки.

function square(x)
  return x*x
end

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

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

square = function(x)
  return x*x
end

Превращение функций в методы

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

local rectangle = {width = 10, height = 20}

rectangle["area"] = function(rect)
  return rect.width * rect.height
end

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

rectangle.area = function(rect)
  return rect.width * rect.height
end

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

function rectangle.area(rect)
  return rect.width * rect.height
end

Поскольку объект прямоугольника теперь имеет объект функции на клавише area, мы можем вызвать функцию следующим образом:

rectangle.area(rectangle)

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

rectangle.area(rectangle)
rectangle:area()

Эти две формы эквивалентны. У оператора : есть приятные особенности, так как он имеет еще одно естественное использование.

function rectangle:area()
  return self.width * self.height
end

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

Утка Печатает

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

Например. мы можем определить area функцию для множества различных форм.

local circle = {radius = 5}

function circle:area()
  return pi * self.radius * self.radius
end

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

function total_area(shapes)
    total = 0
    for sh in shapes do
        total += sh:area()
    end
    return total
end

total_area({circle, rectangle})

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

Создание наследования с помощью мета-таблиц

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

Таблицы Lua могут иметь мета-таблицы. Мета-таблица похожа на обычную таблицу. Разница действительно только в его использовании.

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

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

Rectangle = {}
function Rectangle:area()
  return self.width * self.height
end

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

Мы не можем создать экземпляр прямоугольника, мы называем его r

local r = {width = 10, height = 20}
setmetatable(r, Rectangle)

Теперь, если мы вызовем area(), он не будет существовать в r, но Lua найдет его в метатаблице Rectangle и передаст r в качестве первого аргумента, который получит имя self.

r:area()

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

function Rectangle:new(w, h)
  local r = {width = w, height = h}
  setmetatable(r, Rectangle)
  return r
end

Теперь мы можем легко создавать и использовать прямоугольники:

local r = Rectangle:new(4, 5)
local s = Rectangle:new(3, 2)
local total = r:area() + s:area()

Ключ __index

Итак, мы получили почти рабочий код Lua. Я упустил деталь, которую мне нужно объяснить. Фактически Lua не ищет отсутствующий ключ напрямую в своей мета-таблице. Требуется обходной путь с помощью клавиши __index. В таблицах Lua есть несколько ключей со специальным значением. Все они имеют префикс __, чтобы не путать их с вашими обычными ключами.

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

local r = {width = 3, height = 2}
local s = {width = 5, height = 4}

Что произойдет, если мы сделаем это:

local t = r + s

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

  • __add Для оператора +
  • __eq Для == оператора
  • __index Функция этого ключа вызывается всякий раз, когда ключ не найден.

Таким образом, Lua на самом деле не сразу обращается к мета-таблице в поисках неизвестного ключа.

Мы могли бы добавить, например, что-то вроде этого для объекта прямоугольника:

function r.__index(table, key)
  if key == "typename"
    return "Rectangle"
  elseif key == "area"
     return table.width * table.height
  else
     return nil
  end
end

Что делает Lua, так это то, что он переходит в метатаблицу, когда специальные ключи, такие как __add и __index, не найдены.

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

local Rectangle = {}
function Rectangle.__add(a, b)
  local result  = {}
  result.width  = math.max(a.width , b.width)
  result.height = math.max(a.height, b.height)  
  return result
end

В отличие от area(), теперь мы можем использовать оператор +, определенный в мета-таблице.

local r = Rectangle:new(4, 5)
local s = Rectangle:new(3, 2)
local big_rect = r + s

Однако нам не нужно сильно менять, чтобы использовать area().

function Rectangle.__index(table, key)
		return table[k]
end

Реализация функции __index для выполнения подобного поиска в таблице настолько распространена, что Lua позволяет вам сокращать простое присвоение таблицы __index для выполнения поиска. Затем мы можем реализовать реальное наследование для нашего Rectangle класса следующим образом:

Rectangle = {}
Rectangle.__index = Rectangle

function Rectangle:new(w, h)
  local r = {width = w, height = h}
  setmetatable(r, self)
  return r
end

self в этом случае будет таким же, как Rectangle.

Наследование

Теперь у нас есть все необходимое для объектно-ориентированного программирования в стиле Python и Ruby. У нас есть классы, утиная типизация и полиморфизм. Чего я вам еще не показал, так это наследования, хотя вы, наверное, уже догадались, как это сделать.

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

SolidRectangle {}
SolidRectangle.__index = SolidRectangle
setmetatable(SolidRectangle, Rectangle)

Теперь мы можем создать функцию и методы создания экземпляра точно так же, как в нашем Rectangle «классе».

function SolidRectangle:new(w, h, d)
  local r = {width = w, height = h, density = d}
  setmetatable(r, self)
  return r
end

function SolidRectangle:mass(thickness)
	self:area() * self.thickness * self.density
end

Сравнение с Python

Итак, теперь вы увидели, как вы создаете систему ООП на Lua. Давайте посмотрим, чем это отличается от, скажем, Python. Начнем с создания аналогичного класса.

class Rectangle:   
    def __init__(self, w, h):
        self.width = w
        self.height = h
        
    def area(self):
        return self.width * self.height

Я делаю это в Python REPL. Как я уже писал ранее, я не поклонник встроенного в Python REPL. Поэтому я советую вам использовать PtPython или bpython. В этих примерах я использую PtPyhon.

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

>>> r = Rectangle(20, 30)
>>> r.width
20

>>> r.height
30

>>> r.area()
600

Однако мы можем заглянуть под капот и увидеть, что Python имеет много общего с Lua. Я могу получить доступ к переменным-членам как к ключам в словаре:

>>> r.__dict__['width']
20

>>> r.__dict__['height']
30

Однако, в отличие от Lua, Python по умолчанию не будет искать в своем классе записи, не относящиеся к объекту, например метод area():

>>> r.__dict__['area']
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 'area'
'area'

Надо пойти на сам класс.

>>> Rectangle.__dict__['area']
<function area at 0x1051bdc08>

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

>>> f = Rectangle.__dict__['area']

>>> f(r)
600

Сравнение с Objective-C

Objective-C не является строго языком сценариев, но является очень динамичным языком, во многом похожим на Python и Ruby. Больше всего он похож на Smalltalk, но сегодня о нем мало кто слышал. В Objective-C 2 все немного изменилось, поэтому я сравниваю здесь старый Objective-C. Прямоугольный объект в Objective-C, по сути, будет выглядеть так в памяти

struct objc_object {
    Class isa;
    int width;
    int height;
};

Таким образом, каждый объект в основном представляет собой структуру в стиле C, указывающую на другую структуру типа Class, которая содержит информацию о своем классе.

По сути, класс выглядит так. Так же, как в Lua есть таблицы, указывающие на таблицы, у нас есть объекты классов, указывающие на объекты классов.

struct objc_class {
    Class isa;

    Class super_class;
    const char *name;
    long instance_size;
    struct objc_ivar_list *ivars;
    struct objc_method_list **methodLists;
    struct objc_cache *cache;
};

isa в объекте класса, в отличие от объектов, указывает на мета-класс. Суперкласс - отдельная запись.

В Objective-C, если я сделаю такой вызов метода:

[rectangle area];

По сути, это синтаксический сахар для вызова функции C:

objc_msgSend(rectangle, "area");

Objective-C будет использовать указатель isa, чтобы найти объект класса, а затем просмотреть связанный список methodLists, чтобы найти указатель функции на метод area. На практике он сначала смотрит cache, чтобы увидеть, был ли area метод, который недавно использовался. Это сделано для того, чтобы ускорить поиск в правом циклах, вызывая один и тот же метод несколько раз.

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

void objc_addClass(Class myClass);

Программист Objective-C, конечно, этого не видит. Они просто пишут определение класса, и компилятор превратит его в код, который во время выполнения зарегистрирует объект класса. Это в некоторой степени похоже на то, как разработчику Python не нужно самому связывать таблицы с метатаблицами, как это требуется программисту на Lua.

Сравнение с Юлей

Мой любимый язык сценариев Julia работает немного иначе, потому что он использует множественную отправку, а не единственную отправку, как большинство языков сценариев ООП. Я попытаюсь объяснить это на языке Lua. Скажем, у меня есть два объекта, представляющих такие формы, как прямоугольник и круг, и я хочу вычислить их пересечение. Используемый алгоритм будет зависеть от обоих типов. В Julia это работает, потому что он всегда учитывает типы всех аргументов.

Скажем, у нас есть эта строка:

intersect(circle, rect)

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

specialized_functions = generic_functions["intersect"]
f  = specialized_functions[{typeof(circle), typeof(rect)}]
f(circle, rect)

Итак, в Julia мы, по сути, выполняем два поиска функций в хэш-таблице, а не только одну, как в Lua или Python.

  1. Для всех функций существует глобальный словарь (хеш-таблица). Где мы находим функцию intersect.
  2. У каждой функции есть несколько разновидностей или специализированных функций, в зависимости от количества и типа аргументов, которые она принимает. Мы используем кортеж {typeof(circle), typeof(rect)} всех типов аргументов для поиска функции, которая принимает именно эти аргументы.

Я использовал здесь хеш-таблицу, поскольку Lua не имеет типов кортежей. У него тоже нет функции typeof, поэтому вместо этого вам придется использовать метатаблицы. Таким образом, ключ поиска будет больше похож на:

{getmetatable(circle), getmetatable(rect)}

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

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