Pull to refresh
1975.3

Во всем привычном есть место для истории

Reading time5 min
Views2K
Original author: Hilel Wayne

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

Почему vim использует hjkl

Вопрос: Почему vim использует hjkl, а не стрелки для навигации?

Типичное объяснение: Для того, чтобы пальцы не покидали домашний ряд.

Историческое объяснение: Билл Джой разработал vi на видеотерминале ADM-3A, на котором отсутствовали отдельные клавиши со стрелками. Если вы посмотрите на клавиатуру ADM, стрелки там изображены на клавишах hjkl. Джой использовал ту же логику для vi, а позже и для Vim.

Клавиатура ADM
Клавиатура ADM

Ещё более историческое объяснение: Вообще-то, странно, что на ADM hjkl использовались для стрелок. Почему именно эти буквы?

Для этого есть отличная причина. Посмотрите на таблицу символов ASCII версии 1967 года:

На каждый символ выделено 7 бит. Первые 32 — "управляющие символы", важные для коммуникации, но не являющиеся символами по-настоящему. Компьютерным клавиатурам, созданным по подобию пишущих машинок с QWERTY-раскладкой, требовался способ использовать эти символы, соблюдая при этом то же расположение клавиш. Проблема была решена добавлением дополнительной "управляющей" клавиши, которая переключала ввод с физических символов на управляющие. Зажатие этой клавиши обнуляло шестой и седьмой старшие биты выбранного дополнительного символа. Например, если вы хотели отправить "backspace", следовало зажать ctrl+H (или ^H). Это преобразовало бы 100 1000 в 000 1000. Аналогично, чтобы добавить разделитель строк, нужно было нажать ^J.

Если полистать мануал к ADM, можно увидеть, что "backspace" в ADM использовался в значении "сдвинуть курсор налево" без удаления текущего символа. Так как ^H и ^J уже использовались как "влево" и "вниз", имело смысл превратить ^K и ^L в "вверх" и "вправо". Так что пользователи ADM уже использовали hjkl для навигации, а Билл Джой просто последовал этому примеру при разработке vi.

Почему месяцы в JavaScript начинаются с нуля

Вопрос: В JavaScript'овом date API месяцы считаются 0-11, а не 1-12. Почему?

Типичное объяснение: Так проще индексировать массивы. Нам ведь нужны названия месяцев, не номера. Так можно создать массив, содержащий имена и использовать date.GetMonth() для получения информации.

Историческое объяснение: Это способ поддерживать совместимость с Java, которому, в свою очередь, нужно быть совместимым с C.

Ещё более историческое объяснение: Тогда почему в C сделано так? И почему всё, связанное с датами в C, нумеруется с нуля, кроме дней?

Впервые структура полей tm_date была официально установлена в стандарте ANSI C89, который остается практически абсолютно неизменным и по сей день. Стандарт языка вышел через семнадцать лет после C и формализовал многое из того, что уже было стандартным для различных Unix-систем. Если заглянуть в историю Unix, обнаружится, что в самом раннем примере <ctime.c> в C не используется отдельная структура данных, информация о дате хранится в массиве.

#define SEC   0
#define MIN   1
#define HOUR  2
#define MDAY  3
#define MON   4
#define YEAR  5
#define WDAY  6
#define YDAY  7
#define ISDAY 8

ctime хранит время как секунды-минуты-часы (SMH), хотя отображает их как HMS. Такая странность начинает иметь смысл, когда принимаешься разбираться в том, как код использовался. Unix 5 использовал эти данные только для того, чтобы показывать время пользователям:

int *t;
{
    register char *cp, *ncp;
    register int *tp;
  
    cp = cbuf;
    for (ncp = &quot;Day Mon 00 00:00:00 1900\\n&quot;; *cp++ = *ncp++;);
    ncp = &amp;&quot;SunMonTueWedThuFriSat&quot;[3*t[6]];
    cp = cbuf;
    *cp++ = *ncp++;
    *cp++ = *ncp++;
    *cp++ = *ncp++;
    cp++;
    tp = &amp;t[4];
    ncp = &amp;&quot;JanFebMarAprMayJunJulAugSepOctNovDec&quot;[(*tp)*3]; // (a)
    *cp++ = *ncp++;
    *cp++ = *ncp++;
    *cp++ = *ncp++;
    cp = numb(cp, *--tp); // (b)
    cp = numb(cp, *--tp+100); // (c)
    cp = numb(cp, *--tp+100);
    cp = numb(cp, *--tp+100);
    cp =+ 2;
    cp = numb(cp, t[YEAR]);
    return(cbuf);
}

Я прокомментировал пару интересных строк. Сначала мы используем сохранённый месяц в (a). Вместо того, чтобы хранить имена всех месяцев в массиве, разработчики записали трёхбуквенные аббревиатуры каждого месяца в одну строку, а затем использовали номер месяца для вычисления указателя, чтобы получить три необходимых байта. Затем, в (b) они получают день и время в HMS в (c), декрементируя адрес указателя трижды. Хранение времени в SMH позволяет избавиться от дополнительного явного перехода (так как они итерируют по значениям в обратном порядке). Разработчики воспользовались тем фактом, что, поскольку поля хранятся как элементы массива, они расположены в одном месте в памяти.

Всё это говорит нам о том, что они оптимизировали каждую мелочь. Что целесообразно, ведь первые версии Unix были созданы на компьютерах PDP-7. В приличном компьютере 1970-х могло быть всего пара килобайт памяти. И если попробовать хранить имена месяцев в памяти, не исключено, что они заняли бы почти 10% всей RAM!

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

Эта история так же объясняет небольшую несостыковку в структуре: MDAY (день месяца) начинается с единицы, а YDAY (день года) — с нуля. Однако это логично для дилеммы "вычисление против отображения": день года никогда не показывался пользователю. Он использовался только для расчёта момента перехода на летнее время (в воскресенье по местному времени).

Заключение

Оба объяснения неполные. Можно закопаться еще глубже, чем "всего" на два уровня истории вниз. Относительно вопроса про hjkl, резонно было бы поинтересоваться, почему ASCII-таблица устроена именно таким образом. С историей про tm_date, можно разобрать на кусочки более ранние версии Unix, посмотреть, что происходило там, или поговорить с создателями напрямую. И даже это — не единственные варианты. Всегда есть куда погрузиться дальше, преодолевая слои истории один за другим.

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

Такая разница между первым и вторым слоями истории приводит в очень досадную ловушку. Люди смотрят на верхушку и предполагают, что на этом всё — отчего история начинает казаться неуместной. И даже когда мы понимаем, что историческая подоплёка больше, чем думалось изначально, раскопки каждого нового слоя требуют значительно больше работы, чем предыдущего. Узнать то, что Билл Джой использовал ADM-3A, можно из первой строчки гугла. Чтобы узнать более глубинные причины, мне потребовалось два часа.

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

Tags:
Hubs:
+7
Comments0

Articles

Information

Website
timeweb.cloud
Registered
Founded
Employees
201–500 employees
Location
Россия
Representative
Timeweb Cloud