Складна проблема комьютерных наук

… це, звичайно ж, іменування сутностей. І я говорю не тільки про імена змінних або нових технологій, немає. Ми не можемо домовитися навіть про самих базових термінах.

Тисяча діалектів
Чи знаєте ви, що специфікація мови програмування часто згадує термін «об'єкт»? Ні, це не об'єкт в тому розумінні, як він описується в ООП — об'єкт Із визначається як «блок даних в середовищі виконання, вміст якого може представляти деяке значення». В цьому розумінні об'єкта має сенс говорити про, наприклад, «об'єкт типу 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-bit і 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 і розмова швидко перейшов до обговорення і критики інших альтернативних назв. Так що іменування сутностей — справді найважливіша проблема в комп'ютерних науках.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.