Сложнейшая проблема компьютерных наук

в 9:06, , рубрики: C, Анализ и проектирование систем, Блог компании Инфопульс Украина, именование, Программирование, Совершенный код

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

Тысяча диалектов

Знаете ли вы, что спецификация языка программирования С часто упоминает термин «объект»? Нет, это не объект в том понимании, как он описывается в ООП — объект в С определяется как «блок данных в среде выполнения, содержимое которого может представлять некоторое значение». В этом понимании объекта имеет смысл говорить о, например, «объекте типа char».

Термин «метод» достаточно распространён, но вы можете встретить программистов, которые будут говорить исключительно «функция-член класса». Язык программирования Java, поэтому, то ли имеет, то ли не имеет функций, в зависимости от того, кого вы об этом спросите. Термины «процедура» и «подпрограмма» иногда используются как аналог «функции», но в некоторых языках программирования (например, Pascal) процедура это совершенно не то же самое, что функция.

Даже в рамках одного языка программирования мы, бывает, путаемся.

Программистов на Python можно поймать на употреблении термина «свойство» (property) вместо аттрибут (attribute), хотя оба термина существуют в языке и они не абсолютно тождественны. Есть разница между «аргументом» и «параметром», но кому до этого есть дело — мы просто произносим то или иное слово, когда нам кажется удобнее. Я часто использую термин «интерфейс функции» ("signature"), но другие люди делают это очень редко, так что иногда я задумываюсь — понимает ли вообще кто-нибудь, о чём я говорю?

Когда мы говорим «тип данных float», то программист на С услышит «тип с плавающей запятой одинарной точности», а программист на Python будет уверен, что имелся в виду тип с двойной точностью. И это ещё не самый страшный случай, поскольку когда упоминается тип word — это вообще может подразумевать как минимум четыре различных толкования в плане его размера.

Часть проблемы в том, что когда мы говорим «о компьютерных науках» мы на самом деле не говорим о компьютерных науках. Мы занимаемся практическим программированием на каком-то множестве (из сотен!) неидеальных языков программирования, каждый из которых — со своими особенностями и причудами. При этом у нас есть некоторое(ограниченное) количество знакомых нам терминов, которые мы применяем к разным фичам разных языков, иногда к месту, а иногда и не очень. Человек, начавший изучать программирование с Javascript, будет иметь определённое представление о том, что такое «класс» и оно будет очень отличаться от представления того человека, чьим первым языком был Ruby. Люди приходят с бэкграундом одного языка в другой и начинают обвинять его, например, в том, что там нет нормальных замыканий, поскольку в их языке термином «замыкание» обозначалось нечто совсем другое.

Иногда со всем этим можно кое-как мириться. А иногда может случиться конфуз. Вот мои (наименее?) любимые примеры подобных ситуаций.

Массивы, векторы и списки

В языке С массив — это последовательный блок данных, в который вы можете поместить некоторое (чётко определённое) количество значение переменных одного типа. int[5] описывает массив, предназначенный для хранения пяти переменных типа int, непосредственно одна за другой.

С++ вводит понятие вектора, как аналога массива, способного автоматически изменять свой размер, подстраиваясь под текущие потребности. Также есть стандартный тип списка, под которым в данном случае понимается двусвязный список (на самом деле стандарт не выдвигает требований к конкретной реализации, но требования по функционалу делают логичным реализацию вектора на базе массива, а списка — на базе двусвязного списка). Но постойте! В С++11 вводится термин «initializer_list», в названии которого есть слово «список» (list), но по сути он является массивом.

Списки в Lisp являются, конечно же, связными списками, что делает простым их обработку в плане доступа к голове и хвосту. Так же работает и Haskell, плюс в нём есть Data.Array для быстрого доступа к элементам по индексу.

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

В Python список является фундаментальным типом данных, в котором есть свойства, аналогичные вектору в С++ и (в CPython) он реализован на базе С-массива. Стандартная библиотека также предоставляет редко используемый тип данных array, который упаковывает числа в массивы С для экономии места и дезориентирует программистов, пришедших к Python через С — они думают что «массив» это как-раз то, что нужно использовать по-умолчанию. Ах да, ещё есть встроенный тип байтового массива, что не то же самое, что массив, который хранит байты.

В Javascript есть тип массива, но он построен поверх хэш-таблицы со строковыми (!) ключами. Есть также ArrayBuffer для сохранения чисел в С-массивах (очень похоже на тип array в Python).

В PHP тип данных, который называется массивом на самом деле является упорядоченной хэш-таблицей со строковыми(!) ключами. Также в PHP есть списки, но это не тип данных, а лишь некоторый синтаксический сахар. Люди, который переходят из PHP в другие языки иногда удивляются, что классические хэш-таблицы, оказывается, не сохраняют упорядоченность.

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

Ну и, чтобы два раза не вставать, пройдёмся по именам типов данных ассоциативных контейнеров:

C++: map (а на самом деле это двоичное дерево. С++11 добавляет unordered_map, который является хэш-таблицей)
JavaScript: object (!) (это вообще-то не классический ассоциативный массив, но в нём можно хранить значения, доступные по строковому ключу. А ещё есть тип данных Map.)
Lua: table
PHP: array (!) (и только строковые ключи)
Perl: hash (тоже «форма», а не тип, плюс неоднозначность из-за того, что хэшами называют также нечто совершенно другое, плюс опять-таки только строковые ключи)
Python: dict
Rust: map (хотя существует в виде двух отдельных типов — BTreeMap and HashMap)

Указатели, ссылки и алиасы

В языке С есть указатели, которые являются адресами хранения некоторых данных в памяти. Для С это естественно, поскольку всё в С — об управлении данными в памяти и о представлении всех данных, как адресов в одном большом блоке данных (ну, более или менее так). Указатель — всего лишь индекс в этом большом блоке данных.

С++, унаследовав указатели из С, сразу предостерегает вас от злоупотребления ими. В качестве альтернативы предлагается ссылка, которые вроде бы в точности как указатели, но для доступа к значениям в которых не нужно использовать оператор "*". Это сразу создаёт новую (очень странную) возможность, которой не было в С: две локальных переменных могут указывать на один и тот же блок данных в памяти, так что строка а=5; вполне себе может изменить значение переменной b.

В Rust есть ссылки, и они даже используют синтаксис С++, но по факту являются «заимствованными указателями» (т.е. указателями, но прозрачными). Также в языке есть менее распространённые «чистые указатели», которые используют синтаксис указателей С.

В Perl есть ссылки. Даже два отдельных типа ссылок. Жесткие ссылки (аналог указателей в С, за тем лишь исключением, что адресс недоступен и подразумевается, что он не должен быть использован непосредственно) и мягкие ссылки, где вы используете содержимое некоторой переменной в качестве имени другой переменной. Также в Perl есть алиасы, которые работают аналогично ссылкам в С++ — но не работают для локальных переменных и вообще по сути не являются типом данных, а просто манипуляцией над символьной таблицей.

В PHP есть ссылки, но не смотря на влияние Perl, синтаксис ссылок был взят из С++. С++ определяет ссылку по типу переменной, на которую она ссылается. Но в PHP нет объявления переменных, так что переменная начинает считаться ссылкой с того момента, как она участвует в некотором специфическом наборе операций, включающем оператор &. Этот магический символ «заражает» переменную «ссылочностью».

Python, Ruby, JavaScript, Lua, Java и ещё куча языков не имеют указателей, ссылок или алиасов. Это несколько затрудняет понимание этих языков для людей, пришедших из мира С и С++, поскольку по ходу объяснения тех или иных высокоуровневых вещей часто приходится говорить фразы вроде «это указывает на...», «это ссылается на ...», что вводит людей в заблуждение, создавая впечатление, что у них действительно есть некоторый указатель или ссылка на некоторую область в памяти, к содержимому которой можно непосредственно получить доступ. По этой причине я называю поведение ссылок в С++ алиасингом, поскольку это более ясно отражает суть происходящего и оставляет слово «ссылаться» для более общего применения.

Передача по ссылке и по значению

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

Фундаментальная проблема здесь в том, что С имеет синтаксис для описания структур, но сама семантика языка структур в коде не видит — лишь набор байтов. Структура вроде бы выглядит как контейнер, хороший такой надёжный контейнер: содержимое заключено в фигурные скобки, нужно использовать оператор "." для доступа к внутренним членам. Но для С ваша структура — всего лишь блок бинарных данных, не сильно отличающийся от int, ну разве что немного больший по размеру. Ах, ну да, и ещё можно посмотреть на какую-то отдельную часть данных. Если вы помещаете одну структуру внутрь другой, язык С тупо выделит во внешней структуре блок данных для внутренней. Когда вы присваиваете одну структуру другой — происходит банальное побайтовое копирование, такое же, как при присваивании, например, переменных типа double. Грань иллюзорна. В результате, единственный действительно «настоящий» контейнер в языке С — это указатель!

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

С++ ввёл понятие ссылки, ну как раз на тот случай если вдруг в С с его указателями вам всё было слишком легко и понятно. Теперь вы, как и раньше, можете передать структуру «по значению», но если вызываемая функция принимает ссылку, то вот вы уже передаёте свою структуру «по ссылке» и функция может её модифицировать. Аргумент функции становится алиасом передаваемой в неё переменной, так что даже простые типы вроде int могут быть переписаны. Эту «передачу по ссылке» лучше назвать «передачей по алиасу».

Java, Python, Ruby, Lua, JavaScript и многие другие языки оперируют контейнерами как отдельными сущностями. Если у вас есть переменная, внутри которой есть структура и вы присваиваете эту переменную другой переменной, то по факту никакого копирования не происходит. Просто теперь обе переменные ссылаются… нет, не ссылаются, указывают...(нет, не указывают)…

И вот она — проблема терминологии! Когда кто-нибудь спросит, передаёт ли язык Х параметры по значению или по ссылке — скорее всего этот человек мыслит в терминах модели языка С и представляет все остальные языки как нечто, что должно обязательно так или иначе ложиться на эту фундаментальную модель. Если я скажу «обе переменные ссылаются», то можно подумать, что речь идёт о С++ ссылках (алиасинге). Если я скажу «обе переменные указывают», то можно решить, что речь идёт об указателях в стиле С. Во многих случаях в языке может не быть ни первого, ни второго. Но в английском языке нет других слов для выражения того, о чём мы хотим сказать.

Семантически языки ведут себя так, как будто содержимое переменных (их значения) существуют сами по себе, в некотором абстрактном мире, а переменные — это просто имена. Присваивание связывает имя со значением. Заманчиво объяснять это новичкам как «теперь а указывает на b» или «теперь они ссылаются на одит и тот же объект», но эти объяснения добавляют косвенность, которой на самом деле в языке не существует. а и b просто оба именуют один и тот же объект.

Вызов функции в данном случае является формой присваивания, ведь аргументы внутри функции теперь именуют те же значения, которые вызывающий код передал в функцию. Вы можете модифицировать их — и вызывающий код увидит результат этих модификаций, поскольку он ведь тоже именует те самые значения. Внутри вызываемой функции вы не можете переприсвоить переменные: переменная в данном случае — не алиас, присвоение ей какого-либо значения лишь приведет к связыванию её имени (внутри функции) с новым значением, но никак не повлияет на переменные в вызывающем коде. Всё это несколько выходит за рамки классических «передачи по ссылке» и «передачи по значению». Здесь вообще нет устоявшейся терминологии, я слышал, как это называют передачей объектов, передачей по имени, передачей дележом.

В принципе, передачу по ссылке в стиле С++ тоже можно реализовать и в других языках (как я упоминал, PHP умеет передавать по алиасам, используя синтаксис ссылок С++). Но передача по алиасам существует лишь как альтернатива передаче по значению, а передача по значению существует, потому, что в низкоуровневом С полвека назад ничего другого реализовать было невозможно.

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

Свободная типизация

Это, конечно, вопрос интерпретации, но лично я уверен, что такой штуки как «свободная типизация» не существует. По крайней мере, я не слышал ни одного конкретного определения для этого термина.
Я напомню:

  • Есть сильная типизация, которая означает, что переменная не меняет свой тип чтобы «подстроиться» под те операции, которые код хочет с ней сделать. Rust — язык с сильной типизацией, сравнение 32-битного и 64-битного целых значений вызовет ошибку.
  • Есть слабая типизация, которая означает, что переменная может изменить свой тип ради того, чтобы подойти к вычисляемому выражению. JavaScript — слабо типизированный язык, в нём 5 + «3» неявно сконвертирует строку в число и выражение даст результат 8 (шучу-шучу, результат будет, конечно же, «53»). Также слабо типизированным является С: вы можете просто взять и присвоить переменной типа int значение «3» и получить хоть и чудаковатый, но вполне компилирующийся код.
  • Есть статическая типизация, которая означает, что тип переменной известен на этапе компияции. Java — язык со статической типизацией. Вы только посмотрите на любой Java-код — такое впечатление, что он на 70% состоит из одних только названий используемых типов.
  • Есть динамическая типизация, которая означает, что тип переменной определяется по ходу выполнения программы. Ruby — язык с динамической типизацией, типы определяются на этапе выполнения.

Понятия «сильной» и «слабой» типизации создают гармоничную картину мира. «Статическая» и «динамическая» типизации тоже понятны и взаимодополняемы. Языки могут иметь в себе элементы и сильной и слабой типизации, так же как статической и динамической, хотя какая-то одна позиция всё-же является превалирующей. Например, хотя язык Go считается статически-типизируемым, interface{} в нём имеет признаки динамической типизации. И наоборот, Python формально статически-типизированный и каждая переменная имеет тип object, но удачи вам с этим.

Поскольку отношение «сильной»«слабой» типизации касается значений переменных, а «статической»«динамической» касается их имён, все четыре комбинации существуют. Haskell сильный и статический, С слабый и статический, Python сильный и динамический, Shell слабый и динамический.

Что же тогда такое «свободная типизация»? Кто-то говорит, что это аналог «слабой», но многие люди называют «свободно типизируемым» Python, хотя Python относится к языкам с сильной типизацией. (По меньшей мере, сильнее, чем С!).

И, поскольку термин «свободно типизируемый» я в основном встречаю в уничижительном смысле, могу предположить, что люди имеют в виду «не так типизируемый, как это происходит в С++». Тут, надо отметить, что чья бы корова мычала, а С++ помалкивал бы. Система типов С++ далеко не без изъянов. Какой, например, будет тип у указателя на тип T? Нет, это не T*, поскольку ему можно присвоить нулевой указатель (а это совсем не указатель на переменную типа T) или случайный мусор (что тоже вряд ли будет указателем на переменную типа Т). Какой смысл гордиться статической типизацией, если переменные некоторого типа по факту могут не содержать в себе значение данного типа?

Кэширование

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

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

class Monster:
    def think(self):
        # do something smart

    @reify
    def inventory(self):
        return []

Здесь monster.inventory на самом деле не сущесвует пока вы не попробуете прочитать его. В этот момент вызывается reify (всего лишь один раз) и список, который она возвращает, становится аттрибутом. Всё абсолютно прозрачно, как только значение создано, это обычный аттрибут без каких-либо последующих накладных затрат на непрямой доступ. Вы можете добавить в него что-то, и вы будете видеть один и тот же результат при каждом доступе. Аттрибут не существовал пока вы не призвали его к жизни попыткой посмотреть на него.

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

reify длительное время не был представлен в репозиториии PyPI в качестве отдельного компонента. Наверное, потому, что его можно реализовать с нуля в десяток строк. Когда я говорил о том, что видел reify во многих проектах, я имел в виду «многие проекты скопипастили или написали на коленке реализацию reify». И вот, наконец, данный компонент был добавлен в репозиторий под именем… cached-property. Документация даже показывала как можно «инвалидировать кэш» — порчей внутреннего состояния объекта.

Большая проблема, которую я вижу здесь, это то, что буквально абсолютно каждое использование данного декоратора, которое я видел, не было кэшем в его классическом понимании. Пример выше несколько простоват, но даже для него «инвалидация» кэша приведёт к необратимым последствиям — мы полностью потеряем состояние Monster.inventory. Реальные применения @reify часто открывают файлы или соединения с базой данных, и в этих случаях «инвалидация» будет равнозначна уничтожению данных. Это совершенно не кэш, потеря которого должна лишь замедлить работу, но не испортить данные в памяти или на диске.

Да, с помощью @reify можно создать и кэш. А ешё его можно создать с помощью dict и разными другими способами тоже.

Я пробовал выдвинуть предложение о переименовании cached-property в reify на ранней стадии появления данного компонента в репозитории (это было важно, особенно учитывая желание автора добавить его в стандартную библиотеку языка) — но никому не понравилось название reify и разговор быстро перешел к обсуждению и критике других альтернативных названий. Так что именование сущностей — действительно важнейшая проблема в компьютерных науках.

Автор: Инфопульс Украина

Источник



https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js