avva: (Default)
quadratic2.png

А вы знаете про альтернативную формулу решений квадратного уравнения?

Вместо "минус бэ плюс-минус корень... поделить на два а", в ней "2це поделить на минус бэ минус-плюс корень...".

Ну, разобраться в том, что она дает ровно те же два решения, не очень тяжело.

Почему там "минус-плюс" в знаменателе, тоже не бином Ньютона: суть в том, что один и тот же корень получается, если в первоначальной формуле взять плюс, а в этой минус. И наоборот, если в обеих взять плюс, то выйдут два разных корня, в общем случае.

Но вот зачем это все надо? Те, кто знают, уже усмехаются и кивают, а я расскажу.

Это нужно, если обычная формула заставляет сделать "катастрофическое сокращение". Что такое катастрофическое сокращение, спросите вы? Это когда мы делаем вычитание двух чисел, очень близких друг к другу. Из-за того, как вещественные числа записываются в памяти компьютера (вспоминайте эти странные слова: мантисса... экспонента...),
происходит потеря точности.

Например, если b очень большое в сравнении с a,c, то получится, что дискриминант sqrt(b^2-4ac) очень близок по значению к b. Тогда операция -b+sqrt(D) потеряет почти все значащие биты в представлениях b и sqrt(D). Мы получим какое-то решение, но оно легко может на 10-20% отличаться от истинного. А второе решение, где берется минус, будет в порядке.

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

Все старое опять становится новым. Когда-то в 70-х и 80-х годах прошлого века, такие трюки были обыденным делом для программистов. Потом мы все привыкли к 64-битным вещественным типам и обленились, хотя конечно эксперты в определенных областях знали и это и гораздо более продвинутые техники численного анализа. А сейчас в нейронных сетях один из главных трендов - снижение точности до 32- и 16-битных представлений, чтобы выиграть скорость и память на миллионах и миллиардах параллельных операций внутри GPU. Не то чтобы с 64-битными числами не надо никогда следить за точностью, но в 32- и 16-битных эта проблема опять выходит на передний план.
avva: (Default)
Исключительно подробный и очень интересный (для тех, кто в теме, как минимум) пост одной дотошной индийской исследовательницы о том, как она искала работу в области языковых моделей буквально пару месяцев назад. У нее есть Ph.D. и публикации, она искала роль ML Research Scientist или что-то в этом роде.

Она рассказывает о том, как устроен процесс в AI-стартапах, в компаниях-единорогах (это стартапы, выросшие до размеров больше миллиарда долларов в оценке) и в больших техно-компаниях типа Google, Apple, Amazon, Meta итд. Дает примерные размеры предложенных зарплат включая бонусы и акции (150-200 тысяч долларов в год в стартапах, 345-400 тысяч долларов в год в больших компаниях), рассказывает о том, что прошло хорошо и что не понравилось, какие виды интервью были, какого рода вопросы задавали, как она рекомендует к интервью готовиться. В общем, интересно.
avva: (Default)
Недавно помогал ребенку подготовиться к контрольной по программированию (школьному предмету; они учат C#). Главной темой были связные списки и работа с ними, что в данном случае было работой с классами типа Node<T>, с методами Get/SetValue(), Get/SetNext().

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

Подумав немного, я решил не разбирать пока эти старые контрольные и их решения, а подтянуть основы. Сидя рядом, просил ребенка писать код НА БУМАГЕ, решающий очень простые задачи одну за другой. Немедленно обсуждали написанное, ошибки в нем, исправляли их и шли дальше. Задачи такие (во всех случаях предполагаем переменную list типа Node<int>, указывающую на первый элемент списка, возможно равную null, если список пустой):

- проверить, пустой ли список
- проверить, есть ли в списке минимум 3 элемента
- проверить, есть ли в списке ровно 3 элемента
- проверить, верно ли, что второй элемент списока равен 4 (не забывать проверки существования элементов)
- если третий элемент списка существует, изменить его значение на 12
- если есть минимум 2 элемента, удалить второй
- распечатать все элементы списка
- проверить, есть ли в списке элемент со значением 5
- если есть хотя бы один элемент, вставить новый элемент с значением 10 на второе место в списке
- удалить все элементы, равные 5, предполагая, что первый не такой
- то же самое, но без предположения, что первый не такой
- найти все элементы в списке, равные 2, и для каждого такого, если следующий тоже 2, а предыдущий не 2, удалить этот следующий
- найти элемент со значением 13, и если после него есть следующий, поменять их местами
- найти минимальный элемент
- вставить элемент на правильное место в отсортированном списке

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

После того, как все эти задачи решаются без сложностей, без ошибок и практически без рассуждений, можно переходить к задачам типа "поменять порядок на обратный" или "найти и удалить все дубликаты в списке с помощью двух вложенных циклов". Не надо с них *начинать*, если основы не делаются быстро, правильно и без сомнений. А это произойдет, когда ментальные образы станут ясными и четкими и будут правильно отражать происходящее на удобном уровне абстракции. Начинающий программист часто не понимает, насколько важны эти ясность и четкость. В таком случае задача наставника - понять это и показать на живых примерах, как и почему они важны.
avva: (Default)
Давно не покупал книг на бумаге, но захотелось чего-то для души, чего-то доброго. Книги о вечном, не всей этой мути в соц. сетях. Книги, с которой можно вечером в кровати полежать при теплом ламповом свете.

cuda.jpeg
avva: (Default)
Я заметил, что когда я запускаю команду git commit -m "причина..." в командной строке в Windows (если вы не программист, то просто примите за данное, что мне надо это часто делать), то она нормально работает, если нет закрывающих кавычек. Разумеется, я это обнаружил случайно, потому что палец сорвался на кнопку Enter, мне бы не пришло в голову самому попробовать такую извращенную идею.

Теперь передо мной стоит ужасная дилемма.

9ljkaz.jpg
avva: (Default)
(может быть интересно программистам)

Некий Grant Slatton написал в Твиттере список из задач, которые давал на интервью программистам в Амазоне (группа AWS). Привожу ссылку и пишу о них не потому, что это что-то из ряда вон выходящее, а просто любопытно, можно обсудить и сравнить впечатления.

Итак, три задачи:
1) проверить, что строка из скобок типа ((())()()())) правильно сбалансирована
2) дана матрица из нулей и единиц, где 0 - вода, а 1 - суша, найти кол-во "островов". Суша соединяется по 4 направлениям, не по диагонали
3) построить структуру данных, дающую insert(value), delete(value), sample_uniform_random() -> value, все операции быстрее, чем O(N).

Я не понял, это задачи на предварительное интервью (phone screen) или "настоящее". По моему опыту из Гугла, этот набор задач немного сложнее, чем типичное предварительное, и немного легче, чем настоящее. Но в принципе годные задачи, как мне кажется.

Про первую задачу: Грант написал, что самое быстрое решение было за 10 секунд, некий соревновательный программист из Казахстана. Я подумал: объяснил за 10 секунд? - ну так и я могу. Но оказалось, что чувал буквально за 10 секунд написал код, "a Python two-liner". Это, конечно, жесть. Мне стало любопытно, и я заморочился написанием двухстрочного решения на Питоне, и через пару минут (никак не 10 секунд, да), вымучил следующее:

def balanced(a):
cnt=0; b=[cnt:=cnt+(1 if i=="(" else -1) for i in a]
return not any([c<0 for c in b])

Про вторую задачу: можно делать DFS, но я бы обошелся простым проходом по матрице с слиянием островов. Если интересует только число островов, слияние можно сделать за O(1) через уровень индирекции, без сложных структур данных.

Третью задачу можно по-разному решать, напишу свой вариант в комментах.
avva: (Default)
В HN обсуждают, как некоторые программисты обходятся без autocomplete, и пишут код не в IDE.

В принципе там много правильного уже сказано такими странными людьми в обсуждении.

От себя, как еще одного представителя этого племени, скажу так. Я много лет писал код в vim. Последний год нужно работать полностью под Windows по ряду причин, и я пишу код (на C++ и Питоне) в Notepad++. Все варианты autocomplete отключены, включая автоматическое добавление закрывающих скобок (это меня особенно раздражает почему-то).

Среда IDE слишком отвлекает изобилием всего в ней.

ripgrep в командной строке находит использования нужной функции, изредка (2% случаев) ему нужно дать regexp, чтобы нашел именно то, что нужно, и это не проблема.

Print debugging работает лучше (для меня), чем отладчик, в большинстве случаев. Я не настроен фанатически против отладчиков; бывают базы кода и проблемы, в которых без них не обойтись. Но редко.

Большинство времени, в которое я занимаюсь написанием кода, я на самом деле читаю код или смотрю в экран и думаю или смотрю в пространство и думаю. На клац-клац-клац уходит меньшая часть времени. Поэтому оптимизировать клац-клац-клац с помощью autocomplete не так важно, как может показаться.

ору

Dec. 20th, 2024 01:49 am
avva: (Default)
Сегодня узнал, почему код на SQL принято писать заглавными буквами, хотя в принципе язык позволяет маленькими тоже и любые сочетания: "Просто надо орать на базу данных. Она тогда немного быстрее работает."
avva: (Default)
Проект tinygrad - библиотека на Питоне для программирования нейронных сетей - поставил целью ограничить размер исходников и не разрастаться и не надуваться кодом, как другие такие проекты. Это и из названия видно. Они поставили 10 тысяч строк исходного кода в качестве жесткого лимита.

Увы, от принципа "you are what you measure" не убежишь, поэтому их код выглядит часто вот так (тыкабельно):

Gd54YRVW4AAW2rK.jpeg

Поучительно.

AoC

Dec. 1st, 2024 11:36 am
avva: (Default)
Напоминаю, что сегодня начинается Advent of Code - испытание в виде задачек на программирование, которые даются в течение 25 первых дней декабря. Каждый день в полночь открывается новое задание, в двух частях (вторая тяжелее первой). Первые несколько дней задания легкие, потом постепенно усложняются, но не до уровня крутых соревнований. Есть состязание между теми, кто присылает решение как можно быстрее, со списками лидеров, которые ждут полуночи и в течение пары минут шлют ответ, но я советую не обращать внимания на эти гонки, а просто решать в свое удовольствие.

Многие используют Advent of Code, чтобы научиться писать на новом для себя языке. Я так делал дважды в прошлом, и хочу попробовать и в этот раз, но пока не решил, на каком языке. Хотя я не писал нетривиальное количество кода на Rust и Go, мне кажется, что достаточно знаю о них из прочитанного с годами, что не хочется сейчас - лучше что-то более незнакомое. На Zig делал этот конкурс пару лет назад. Относительно известные "старые" языки: Lisp, Haskell, Forth, SmallTalk - когда-то знал или пробовал. Пока что размышляю между OCaml и Crystal. Хотите посоветовать мне что-то другое?
avva: (Default)
Продолжение и окончание истории об испорченных данных, начало см.
https://avva.livejournal.com/3705558.html

Испорченные данные возвращаются!

В комментариях к прошлой записи меня не раз спрашивали, почему я просто не сделал rsync между двумя иерархиями файлов. На это у меня есть сразу три ответа, два из них пустяшные (надо найти и установить rsync под Windows; мне было любопытно разобраться, где и что испорчено), а третий важный: я не знал только, а лишь подозревал, что копия данных в Америке - чистый неиспорченный экземпляр. Анализ файлов должен был эту версию подтвердить.

Когда я писал свою запись, программа, сравнивающая отличающиеся файлы из Америки и в Израиле, еще не закончила работу, но все указывало на то, что так и есть. Через пять часов после того, как я запостил, я посмотрел на логи и увидел, что все видимо сложнее. Так в итоге и оказалось.

Вернемся к началу и проведем учет того, что у нас есть. А есть у нас три массива данных, в идеале идентичных, назовем их O, A и B. O - это данные экспериментов, которые записывались на встроенный диск компьютера во время самих экспериментов. A и B - это подключаемые внешние SSD-диски. Размер диска O всего 1 терабайт, а данных генерировалось сотни гигабайт в день, поэтому во время исследования мы периодически сбрасывали данные с O на A и B, а потом стирали на O. Несколько раз мы торопились и копировали только на A или только на B, а потом отдельно копировали между ними - но это было реже. В каком порядке в точности что копировалось откуда и куда, сейчас уже никто не помнит. В конце всей этой вакханалии на O почти ничего не осталось, а на A и B были идентичные - по замыслу - копии всех данных. A остался в Америке, а B уехал в Израиль. И все было бы хорошо, если бы не плохой бит в памяти компьютера. (Да, в следующий раз мы все сделаем по-другому и лучше).

Итак, я скопировал все файлы A, у которых контрольная сумма отличается от B, в одно место с B, и запустил программу, которая находит все отличия. Программа написала мне лог-файл из 350 тысяч строк такого типа:

filename: offset 139709: 51 != 55, diff. -4

Для каждого различия написано, в каком файле и на каком смещении, приведен байт из Α и байт из B, и разница между ними.
Когда я писал запись, в этом файле, который продолжал создаваться, были только строки с разницей -4, что подтверждало следующую теорию (мне также казалось логичным, что порча была внесена во время одной сессии массивного копирования именно на B):

ТЕОРИЯ 1. A - это чистые исходные данные , а порча в B всегда состоит в том, что третий бит ставится в 1.

Но когда сравнение всех файлов закончилось, оказалось, что есть также много строк в другую сторону:

filename: offset 14467: 56 != 52, diff. 4

Это опровергает ТЕОРИЮ 1.

У нас есть файл всех изменений, который можно легко изучать с помощью текстового поиска (я настоятельно рекомендую программу ripgrep). Давайте разбудим нашего внутреннего Шерлока Холмса, усядемся поудобнее в мягком кресле, раскурим трубку и подумаем: что там могло случиться?

ТЕОРИЯ 2. A - это все еще чистые исходные данные, но порча в B иногда ставила бит в 1, иногда в 0.

Изменения в бинарных файлах не могут легко помочь подтвердить или опровергнуть эту теорию. Текстовые CSV файлы - другое дело. В них встречаются только символы: 0-9, английский алфавит (только первая строка заголовка, нерелевантна, слишком редка, чтобы попасть под порчу), запятая, точка (десятичная), перенос строки (коды 13 и 10 - перенос строки под Windows это два символа, а эти файлы писала программа в C++ под Windows).

Быстро находим много строк такого типа:

filename.csv: offset 1234: 14 != 10, diff. 4

Это опровергает ТЕОРИЮ 2: эта порча создалась изменением переноса строки 10 в 14, но в файле A как раз записано 14, значит, он испорченный. К сожалению, чистого экземпляра всех данных у нас просто нет. Значит ли это, что все потеряно?

Предположим, что между A и B есть разница: которую можно схематически обозначить так: xXxxxxxx и xxxxxxXx. Есть +4 в одном байте и -4 в другом, в разных файлах или даже в одном. Казалось бы, это значит, что бит менялся случайным образом. Но есть и другая возможость: что в исходном O было xxxxxxxx, и бит менялся в сторону увеличения дважды, при записи O->A и O->B, в разных местах.

ТЕОРИЯ 3. A и B оба испорчены, но во время копирования 3-й бит менялся произвольным, случайным образом.

ТЕОРИЯ 4. A и B оба испорчены, но во время копирования 3-й бит всегда ставился в 1.

Если верна ТЕОРИЯ 3, то у нас нет шансов восстановить чистые данные. Кое-где, как в различии 14 != 10, мы знаем, что там было 10, но если это 51 != 55 (цифры "3" и "7"), мы никак не можем узнать, какая цифра правильная. И в бинарных файлах нет шансов узнать.

Если же верна ТЕОРИЯ 4, то мы можем восстановить чистые данные почти целиком: надо просто исправить все байты в B, которые на 4 больше байтов в A, а те, которые на 4 меньше, оставить как есть - они как раз правильные. Крайне маловероятно, что где-то есть порча, попавшая на тот же байт в A и B в разное время копирования. Возможно, мы упустим небольшое количество испорченных данных, которые были скопированы с O на A, а потом без изменений с A на B, но тут уж ничего не поделаешь - они вообще не в списке наших измененных файлов.

Можно ли понять, какая из теорий верна? Можно. Если верна ТЕОРИЯ 3, то должно быть свидетельство именно падения бита из 1 в 0. Различие типа 51 != 55, где оба байта законны в CSV, не может таким считаться. Различие типа 10 != 14 доказывает как раз обратное: что было возрастание бита из 0 в 1, потому что 14 в исходном файле не было. Какой символ имеет выставленный 3-й бит, но если его сбросить, получается символ, который не бывает в CSV?

Это запятая. ASCII-код 44, если сбросить третий бит, выходит 40 - код левой скобки "(", которая в CSV не встречается. Запятых в CSV-файлах много, больше чем переносов строки. Примеров "10 != 14" в моем файле десятки. Примеров "40 != 44" ни одного. Вывод: ТЕОРИЯ 3 неверна, и все-таки бит всегда выставлялся в 1. Верна ТЕОРИЯ 4, и мы можем восстановить побитые файлы. Скрипт, который это делает, работает прямо сейчас.

(честности ради должен добавить: изначально я думал, что одна только запятая доказывает мои рассуждения, и мне нравилась эта драматичность. Оформляя эту запись, я осознал, что и точка, и carriage return (код 13, часть переноса строки) тоже дают свидетельства того же самого. Уже не так драматично, но истина дороже)
avva: (Default)
(для программистов и сочувствующих)

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

Через пару месяцев обнаружили, что данные слегла подпорчены. Там на 99% бинарные файлы, а 1% - большие файлы .CSV с числами, числами, числами внутри. По ряду причин все данные хранятся как обычная большая файловая иерархия (а не, скажем, zip-архивы или база данных).

Код на питоне читает CSV-файлы с помощью библиотеки Pandas, которая подбирает для каждой колонки наиболее узкий тип данных - т.е. если в файле в 3-й колонке везде числа с плавающей точкой, это будет float, а если иногда что-то другое, будет string для всех. Обнаружилось, что в некоторых файлах числовые данные читаются как string, что потом приводит к странным результам, когда с ними делают числовые операции. Когда я стал разбираться, почему так, обнаружил, что мегабайтном файле, целиком состоящем из чисел, записано в одном месте "число" -0.053208=523, и этот знак = все портит. Всего таких испорченных csv-файлов оказалось мало, около 3%. Но я не знал, есть ли подобные ошибки также в бинарных файлах.

Если бы данных было меньше, я бы просто скопировал из Америки. Но копировать 2TB было немного болезненно, и заодно хотелось разобраться, откуда взялась порча и сколько ее. Поэтому я сделал следующее:

- запустил рекурсивный md5sum на все дерево в Америке и дома (10 миллионов файлов)
- скопировал файлы с контрольными суммами в одно место и сравнил, нашел, сколько файлов отличаются по сумме (300 тысяч), сгенерировал список этих файлов
- в Америке сделал zip-архив только с оригиналами испорченных файлов (80GB), скопировал в Израиль, раскрыл
- набросал скрипт, который прошелся по оригиналам, сравнил с испорченными копиями.

Оказалось следующее:

- испорчен примерно один байт из каждых 300 килобайт, т.е. 0.0003% данных
- порча есть в файлах всех типов, и бинарных, и CSV, просто в CSV заметили раньше
- порча всегда одного вида: третий бит с конца становится 1, неважно, был он до того 0 или 1. Т.е. значение байта увеличивается на 4 (или не меняется, если и так этот бит стоял).
- при этом в файлах CSV, состоящих почти целиком из цифр и запятых, происходит следующее. Запятые не меняются, их ASCII-код содержит этот бит. Цифры 0,1,2,3 переходят в 4,5,6,7 и мы эту порчу не заметили. Цифры 4,5,6,7 остаются как есть. И только 8,9 переходят в <,= и это дает вышеописанный эффект в чтении файла из питона. Поэтому только когда очень редкая порча попадает именно на текстовый файл, и в нем на одну из этих двух цифр, получается испорченный файл, который мы замечаем.

На данный момент моя лучшая версия - это плохая память в десктопе или лаптопе, на котором мы копировали все данные с диска-оригинала на диск, который взяли с собой домой. Видимо, сбоит один бит в одном байте памяти. Копировали с помощью Windows-программы robocopy. То, что в длинных последовательностях бинарных файлов одного размера ошибка почти всегда оказывается на одном оффсете от начала файла, косвенно подтверждает это объяснение. Альтернатива - это что какая-то херня происходит в контроллере SSD-диска, кажется маловероятным, но я недостаточно понимаю, как там все устроено. Других вариантов вроде нет.

Урок на будущее: после копирования большого кол-ва данных отдельно проверить, что копия идентична оригиналу, с помощью программы сравнения файлов/директорий.

(Сначала я хотел написать "копировать архивами, даже и без сжатия можно, просто чтобы была контрольная сумма", но если порчу вносит плохой бит в памяти во время *чтения* с диска, то архиватор создаст отличный архив с правильной суммой и плохими данными)

Прикольно.
avva: (Default)
Я задремал сегодня днем и мне приснилось, что у Бога есть возможность выводить чью-то жизнь в статус неслучайной. Если вам достался такой статус, то с этого момента ваша жизнь - это нарратив, и все с вами происходит по какой-то причине - включая ваши действия, у вас есть свобода воли. Не факт, что все будет в жизни хорошо, может как раз наоборот. Главное - что все неслучайно.

Но таких статусов у Бога есть только сорок тысяч. У всех остальных тоже есть свобода воли, но жизнь складывается наобум, происходит все время какая-то рандомная фигня.

Во сне я обнаружил, что можно манипулировать в определенных рамках тем, кому достается этот статус. Я открыл какие-то хаки, как не гарантированно, но с неплохой вероятностью можно получить себе этот статус или подарить другому. Я пытаюсь этим поделиться с другими, но никто мне не верит, потому что инструкции кажутся тупыми, типа встань в таком-то месте враскоряку и прокукарекай глупо, или сделай такой-то нелепый жест. В конце концов меня приглашают к Богу на беседу, я прихожу и он мне рассказывает, что я все правильно открыл, но теперь это только головная боль для него закрывать все эти дырки и глитчи. Но вообще круто, говорит Бог, молодец, если хочешь, я тебе сделаю экскурсию, покажу, как тут все устроено, отвечу на твои вопросы. Я говорю, у меня один главный вопрос есть, можно сразу? Почему только сорок тысяч, почему нельзя большему числу людей дать неслучайную жизнь? Бог смотрит на меня, как будто я задал максимально идиотский вопрос, и говорит, ну как, ровно на столько хватает моего компьюта [1]. И тут я проснулся.

[1] вычислительных мощностей (профессиональный сленг)
avva: (Default)
Вас попросили дать оценку, когда будет выполнено задание, которое должно с вероятностью 90% занять один день, и 10% - два месяца. Ваш ответ?
avva: (Default)
Как же я раньше этого не понимал? Конечно же, "у верблюда два горба, потому что жизнь - борьба" это пословица про важность избыточности (redundancy), и опасность единой точки отказа (single point of failure).
avva: (Default)
Много думал о двух статьях о программировании, которые недавно прочитал - об объектно-ориентированном программировании и не только.

William R. Cook. On Understanding Data Abstraction, Revisited: https://www.cs.utexas.edu/~wcook/Drafts/2009/essay.pdf

Jonathan Aldrich. The Power of Interoperability: Why Objects Are Inevitable:
https://www.cs.cmu.edu/~aldrich/papers/objects-essay.pdf

Статья Кука посвящена разграничению двух похожих, но фундаментально разных понятий, которые (согласно ему) часто путают - Абстрактных Типов Данных (АТД) и объектов. Статья Алдрича развивает эту тему, но в основном аргументирует, что "объекты" - намного более важное понятие, чем "АТД", и что повсеместное использование объектов в современных языках программирования и больших проектах не случайно.

Я сначала прочитал статью Кука (2009), много с ней спорил в голове, сформулировал какой-то ответ, а потом обнаружил к некоторому замешательству, что по большей части этот ответ содержится в статье Алдрича (2014). Тем не менее хочу попробовать упорядочить свои мысли на эти темы.

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

Абстрактный Тип Данных - это когда объявляется новый тип данных, доступ к которым возможен не через операторы языка, типа + или -, а исключительно через набор функций, которые обрабатывают скрытую репрезентацию этих данных. Самый естественный пример, на мой взгляд (не тот, что приводит Кук) - это BigInt. Опытные программисты наверняка встречались с имплементациями целых чисел неограниченной длины в разных языках. Обычно мы используем BigInt через некий API, который дает нам возможность создать такое число с обычным целым значением, делать с ними арифметические операции, печатать, вынимать значение по частям, итд. Функции, которые имлементируют этот API, опираются на какое-то внутреннее представление, например через массив целых чисел, но мы подробности этого представления не знаем и обычно знать не хотим.

В языке типа C такой АТД, как BigInt, скорее всего будет воплощен как структура и набор функций для манипуляции ее содержимым. Но в C++, Java итд. это будет класс, каждый объект которого будет конкретным большим числом: хранить внутри себя репрезентацию и давать возможность вызвать методы Add, Multiply, Create итд.

Обратите внимание на две важных особенности типичного АТД. Во-первых, функции, работающие с ATD, всегда имеют доступ к внутренней репрезентации, в том числе больше, чем у одного значения. Если я вызываю number1.Multiply(number2), то код метода Multiply может смотреть на внутреннюю репрезентацию как "своего" number1, так и "дополнительного" number2, и использовать их для эффективного умножения. Это так несмотря на то, что, казалось бы, number2 - другой объект. Во-вторых, функция Multiply обычно есть только одна, и я незаинтересован в том, чтобы их было много разных, работающих с разными репрезентациями. Даже если по какой-то причине мне надо использовать одновременно два разных пакета BigInt одновременно, я не могу ожидать, что number1.Multiply(number2) будет работать, если number1 и number2 принадлежат к разным АТД.

С другой стороны, что такое "объекты", по Куку? С его точки зрения, главной особенностью объектно-ориентированного программирования следует считать связку интерфейс-объект. Интерфейс - это набор деклараций функций с какой-то общей целью, а объект - это определенное воплощение всех этих функций, упакованное в общую оболочку, через которую это конкретное воплощение функций всегда можно вызвать. Для примера, в языке Go интерфейс Reader состоит из одной функции:

Read(p []byte) (n int, err error)

Во время работы программы на Go одновременно могут существовать множество объектов с разными имплементациями интерфейса Reader.

Файлы имплементируют функцию Read одним способом, буферы памяти другим, синхронизированные каналы третьим итд. итп. Переменная типа Reader может хранить объект файла или буфера или канала, и когда через нее вызовут Read(), исполнится правильный код. Для Кука это и есть главное в объектах, а, например, классы или наследование - это второстепенное и необязательное (в Go и нет классов, собственно).

Обратите внимание на полную противоположность ситуации с АТД, где была одна функция Multiply, которая имела доступ к внутренней репрезентации данных любого значения. Тут у нас есть полным-полно разных функций Read, и разные объекты не имеют доступа к "внутренней памяти" друг друга. Скажем, два объекта-файла не более родные друг другу, чем файл и буфер; все равно все, что они могут делать друг с другом, это вызывать Read(). "Внутренняя память" - скажем, дескриптор файла или положение в буфере - вообще неважна для Кука и не является частью объекта (хоть де-факто в популярных языках программирования это не так). Объект состоит из функций, которые на худой конец всегда можно сделать замыканиями, хранящими контекст и пристраивающими его к коду настоящей функции. Наглядным проявлением этого подхода является то, что интерфейс (в языках, где он явно определяется) состоит только из методов, а не из данных - в отличие от класса.

В языках типа C++/Java и АДТ и "объект" по Куку будут объектами, но типичное использование их будет совсем разным. АДТ можно отличить по тому, что их используют через классы, не через интерфейсы. Действительно, если представить на секунду, что BigInt был бы интерфейсом, а SpecificBigInt - конкретным классом-воплощением, то совершенно непонятно, как метод Multiply этого класса будет умножать SpecificBigInt на просто BigInt - у него нет доступа к внутренней репрезентации второго множителя (в некоторых языках техническую сложность воплощения класса можно решить с помощью механизма multiple dispatch, но фундаментальная проблема разных репрезентаций остается).

По Куку, наблюдается красивая дуальность: АТД и объекты реализуют разные способы "абстракции", т.е. как спрятать от себя конкретные детали программистской работы. В случае АТД мы прячем данные (во внутреннюю репрезентацию), но то, какая функция вызывается, остается известным и детерминированным. В случае объектов мы работаем с известными данными (теми, что передаем методам объектов - "внутренняя память" тут не считается, она деталь имплементации), но какая именно функция вызывается - неизвестно, в разных случаях разная. В 1970-х было несколько языков, которые давали прямой доступ к абстракции АТД, но потом взлет популярности объектов пресек эту тенденцию, и вместо этого АТД воплощают сегодня с помощью чего-то типа классов.

Меня эта дуальность не очень впечатляет. Разница между АТД и объектами в смысле Кука действительно есть и очень важна, но для того, чтобы уложить их в симметрично-дуальные ячейки, Кук нивелирует до состояния неважной детали данные, сидящие внутри объекта, а это противоречит тому, как программисты обычно думают об объектах. Скорее я бы сказал, что АТД являются важным частным примером абстракции, а объекты в смысле Кука - т.е. взаимозаменяемые воплощения интерфейсов, опирающиеся на внутренние данные, общие для всех функций данного объекта - являются намного более общим механизмом, краеугольным камнем современных сложных программ. Примерно то же говорит в своей статье Алдрич, и об этом - в следующей записи. Продолжение следует.
avva: (Default)


Для программистов. Сегодня час искал баг в старом коде (задолго до меня написан). Код рассчитывает, на каком месте на экране - в пикселях - поставить цель, чтобы она была на заданном угловом расстоянии от центра экрана, с точки зрения наблюдателя на расстоянии ровно в метр от экрана точно по центру. Известны размеры экрана в пикселях и в миллиметрах.

Это чистая и очень простая тригонометрия. Но у меня получается на один пиксель разница с тем, что код раньше посчитал и записал в лог. Никак не могу понять, откуда этот пиксель берется. Округление там в другую сторону. Вычисления все в плавающей точке. Из-за того, как структурирован код и передача данных между компонентами, вычисления проводятся немного муторно, по сути лишний раз делается тангенс/арктангенс в середине. Может, это как-то накапливает ошибку? - но вроде не должно настолько.

Я напихал всюду отладочные сообщения, перезапустил, сравнил все с ручным пересчетом... и наконец нашел в самом последнем месте, которое и в голову не приходило. См. картинку.
avva: (Default)
Программисту, который обычно не пишет графические интерфейсы, но тут вот нужно, начиная со второго дня надо платить надбавку за вредность. Еще пока не решил, какую и в каких единицах она измеряется, но надо.
avva: (Default)
Вот уже много лет я успешно сопротивляюсь попыткам вселенной заставить меня выучить язык Rust. Но блогпост "References are like jumps", возможно, сможет переломить наконец мою лень и инстинктивную неприязнь.

Хороший и интересный пост - о том, как разные языки и подходы пытались и пытаются решить проблему shared mutable state.

Если вы знаете Rust или даже работаете на нем, что можете сказать за или против?
avva: (Default)
Вот уже полгода я работаю над технологией отслеживания взгляда (eye-tracking). Каждый день работаю с снимками от камер, которые нацелены на левый и правый глаза, в разных экспериментах. Все эти снимки надо хранить, обрабатывать, проверять на них разные алгоритмы итд.

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

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

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

Вот такая проблема.

April 2025

S M T W T F S
   1 2 3 45
6 7 89 10 11 12
1314 15 1617 1819
2021 22 23242526
27282930   

Syndicate

RSS Atom

Most Popular Tags

Style Credit

Expand Cut Tags

No cut tags
Page generated Apr. 23rd, 2025 11:14 am
Powered by Dreamwidth Studios