Этот компьютер не будет отличаться большой сообразительностью, т.к. сможет исполнять очень простые программы на довольно специфическом языке, который мы будем называть ВыносМозга
.
В этом языке всего 7 инструкций:
>
-- перейти к следующей ячейке;<
-- перейти к предыдущей ячейке;+
-- увеличить значение в текущей ячейке на 1;-
-- уменьшить значение в текущей ячейке на 1;.
-- напечатать значение из текущей ячейки;[
-- если значение текущей ячейки ноль, перейти вперёд по тексту программы на символ, следующий за соответствующей ]
(с учётом вложенности)]
-- если значение текущей ячейки не ноль, перейти назад по тексту программы на символ [
(с учётом вложенности)Несмотря на внешнюю примитивность, ВыносМозга
представляет из себя настоящую Машину Тьюринга, а, следовательно, по потенциальным возможностям не уступает «настоящим» языкам, подобным Си, Java, Python или TypeScript.
Компьютеру требуется память, чтобы хранить результаты расчётов.
Определим тип для памяти:
interface IRAM {
get(pointer: number): number;
set(pointer: number, value: number): void;
}
Объекты с таким типом могут позволить прочитать значение по определенному адресу(метод get
), также могут позволить записать значение по определенному адресу (метод set
).
Создание класса, который реализует данный интерфейс, будет перым заданием. Подробнее в секции Задание.
То, что будет интерпретировать программы на языке ВыносМозга
.
Определим тип для процессора:
interface IProcessor {
process(): void;
}
Как видите, процессор должен уметь только вычислять. Пока всё просто.
Процессор должен уметь выполнять команды, для этого определим и их тип:
interface ICommand {
execute(): void;
}
Создадим теперь класс, который будет реализовывать процессор для языка ВыносМозга
:
class BFProcessor implements IProcessor {
commands: Map<string, ICommand> = new Map<string, ICommand>(); // Команды, которые процессор знает
program: string; // Программа, которую процессор исполняет
memory: IRAM; // Память, в которой хранятся результаты работы
stack: Array<number> = []; // Стек, он понадобится для реализации циклов
ip: number = 0; // Указатель на текущую команду программы
dp: number = 0; // Указатель на текущую ячейку памяти
constructor (program: string, memory: IRAM) {
this.loadCommands(); // Инициализировать команды, котоыре процессор знает
this.program = program;
this.memory = memory;
}
process(): void {
while (this.ip < this.program.length) {
let instrunction = this.getNextCommand();
instrunction.execute();
this.ip += 1;
}
}
private getNextCommand(): ICommand {
let commandName = this.program.charAt(this.ip);
if (!this.commands.has(commandName)){
throw Error(`Неизвестная команда ${commandName}`);
}
return this.commands.get(commandName) as ICommand;
}
private loadCommands(): void {
this.commands.set(".", new BFPrintCommand(this)); // зарегистрировать команду в процессоре
}
}
Интерфейсы помогает одним типом описать объекты относящиеся к разным типам.
У нас есть несколько типов инструкций, которые должен выполнять процессор:
Процессору нужно обрабатывать их единообразно, иначе метод process
превратится в страшную кашу, а при добавлении новых команд его придётся переписывать. Этого стараются избегать в программах, т.к. это начинает замедлять скорость разработки и добавляет места, где можно совершить ошибку.
А если в процессор добавить инструкцию для чтения с клавиатуры? А если в процессор добавить новую инструкцию вывода, которая выводит не просто значение, а переводит его числовое значение (ASCII код) в символ? Всё это приведёт к правкам в методе process.
Вместо этого метод process говорит, что будет обрабатывать все инструкции единообразно. Ему не важно как они выполняются, ему важно только то, что имеют возможность выполниться. Вот здесь и пригождаются интерфейсы. Они задают внешний вид (какие методы и свойства должны быть у объектов этого класса), а реализация этого поведения уже содержится в классах.
Отсюда у нас появляется интерфейс команды -- это просто тип того, что может выполниться. А конкретные классы команд уже реализуют этот интерфейс и определяют как это будет происходить.
Обратите внимание на метод getNextCommand
. Он берет из Map объекты конкретных команд и возвращает их в виде типа ICommand
, т.к. они реализуют этот интерфейс, соответственно являются в том числе объектами типа ICommand
, а не только конкретных классов. Кстати, сам Map тоже хранит команды в виде объектов типа ICommand
. Т.к. иначе мы бы начали перечислять конкретные типы, которые он хранит, а это перечисление опять могло бы нас привести к правкам по обозначенным выше причинам.
Вот так незатейливо вы познакомились с тем, что такое полиморфизм. Полиморфизмом в программировании называется возможность в коде единообразно работать с разными типами. Использование наследования (в т.ч. и реализация интерфейсов) -- один из механизмов полиморфизма.
Реализуем первую команду для процессора:
class BFPrintCommand implements ICommand {
constructor(private processor: BFProcessor) {} // получить аргумент, сделать его приватным полем
execute(): void {
let memory = this.processor.memory; // получить ссылку на память
let dp = this.processor.dp; // получить текущее значение указателя
console.log(memory.get(dp)); // вывести на консоль значение в ячейке памяти
}
}
Запустим вычисления:
function program1() {
/*
Программа 1: "."
1. ".": Вывести значение текущей ячейки
*/
console.log("Программа 1");
let processor = new BFProcessor(".", new RAM());
processor.process(); // Выводит 0
}
program1();
Создать класс RAM
, который реализует интерфейс IRAM
.
Для реализации рекомендуется использовать тип Map
:
let data: Map<number, number> = new Map();
data.set(0, 100); // Записать по ключу 0 значение 100
data.set(1, 200); // Записать по ключу 1 значение 200
console.log(data.get(0)) // Прочитать значение по ключу 0: 100
console.log(data.get(1)) // Прочитать значение по ключу 1: 200
console.log(data.get(2)) // Прочитать значение по ключу 2: undefined
При реализации класса учтите, что может произойти обращение к ячейкие памяти, в которой ещё нет никакого значения (undefined). В таком случае требуется вернуть значение 0
.
Проверить работоспособность на program1
Реализовать BFIncrementCommand
для инструкции +
.
Алгоритм работы команды:
Проверить работоспособность на program2
Реализовать BFDecrementCommand
для инструкции -
.
Алгоритм работы команды:
Проверить работоспособность на program3
Реализовать BFRightCommand
и BFLeftCommand
для >
и <
.
Реализуется ещё проще, чем задание 2 и 3, только требуется работать с указателем на ячейку памяти dp
.
Проверить работоспособность на program4
Реализовать BFStartLoopCommand
и BFEndLoopCommand
для [
и ]
.
BFStartLoopCommand
:
dp
). Для этого потребуется вызвать метод push
у стека.]
. Номер инструкции записать в ip
.BFEndLoopCommand
:
pop
) значение.Проверить работоспособность на program5
, program6
, program7
.
ВзрывМозга
можно посомтрть здесь. Попробуйте подставить сюда код из program1-7