Чим менше, тим краще — про можливості мов програмування

Багато мов програмування включають у себе надлишкові можливості. Розвиток мови включає в себе роботу по їх вилученню.

Існує багато мов програмування, і нові продовжують з'являтися все час. Краще вони тих, що вже існували раніше? Очевидно, що на це питання неможливо відповісти, поки не буде дано чітке визначення, що таке «краще» щодо мов програмування.

Якщо ви подивитеся на історичні тренди, то помітите один із шляхів зробити кращий мова програмування — визначити яку-небудь надлишкову можливість у вже існуючому мовою і спроектувати нову мову без неї.

«Досконалість досягається не тоді, коли нема чого додати, а тоді, коли нічого прибрати»

Антуан де Сент-Екзюпері

У цій статті ви побачите кілька прикладів можливостей різних мов програмування, які вже загально визнані надмірними і ще кілька інших, які мають ті ж риси і можуть коли-небудь бути віднесені до тієї ж групи.


Безмежна кількість способів вистрілити собі в ногу

Коли були створені перші комп'ютери, програми для них повинні були писатися в машинних кодах або на мові ассембера. В машинних кодах ви можете висловити взагалі абсолютно все, що здатний виконати процесор, оскільки машинні коди строго відповідають набору інструкцій процесора. В машинних кодах можна писати коректні програми, але також можна написати нескінченну кількість некоректних програм, що включають ті, що будуть зависати, падати, або некоректно використовувати апаратне забезпечення, на якому вони працюють.

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

З використанням машинних кодів у вас є необмежена кількість способів створити унікальну некоректну програму. Ви можете виконати будь-яку команду процесора в будь-який момент. Набір коректних послідовностей команд — лише малу частину набору всіх можливих послідовностей інструкцій.

image

Перші програмісти швидко виявили, що написання програм у машинних кодах весьмо схильний до помилок, а крім того код виходить довге і погано читається. Одним із способів вирішення проблеми стало створення мови асемблера. Але насправді це мало чим допомогло: програми на асемблері були трохи більше человекочитаемы, але вони всього лише один до одного повторювали машинні інструкції, а значить набір потенційно некоректних програм, які можна було написати, залишався тим же.

Високорівневі мови

Після деякого періоду муки з машинними кодами і ассемблером програмісти придумали високорівневі мови програмування. Перші такі мови не дуже застосовуються сьогодні, але одним з прикладів цих «низькорівневих» високорівневих мов, який до цих пір в ходу, є мова С.

image

Мовою ви можете написати практично будь-яку валидну програму. Все ще є вагон і маленький візок написати неправильну програму, здатну, наприклад, впасти. Але, оскільки мова являє собою деяку абстракцію над машинним кодом — є і деякі набори машинних команд, які ми не можемо висловити мовою. Більшість з них являють собою невалідні програми, а значить абстракція, що відсікає їх — приносить користь.

Зверніть увагу, що сталося: перехід від машинного коду до високорівневих мов прибрав деякі можливості. В машинних кодах можна було написати що-завгодно, на высокоуровневом мовою можна написати вже не всі. Ми миримося з цим, оскільки плюси підходу переважують його мінуси.

GOTO

У 1968-му році Эдсгар Дейкстра опублікував свою знамениту роботу про шкоду оператора goto. У ній він висловив думку, що сама концепція використання оператора goto порочна, а програми були б значно краще без goto. Це породило суперечка, що розтягнувся на десятиліття, але сьогодні ми прийшли до розуміння того, що Дейкстра був прав. Многи популярні мови програмування сьогодні не мають оператора goto (наприклад, Java або Javascript.

image

Оскільки goto виявився абсолютно непотрібним оператором, то його викидання з мови не зменшує кількість валідних програм, які ви можете написати цією мовою. Але воно зменшує кількість валідних
Ви вловили патерн? Приберіть щось непотрібне — і ви отримуєте удосконалення. В цьому немає нічого нового — Robert C. Martin говорив про це багато років тому.

Ви не можете прибирати з мови просто перші-ліпші функції — в цьому випадку ви можете втратити можливість написати деякі валідні програми.

image

Але ви можете прибирати деякі функції без шкоди для мови.

Виключення

Сьогодні всі погоджуються з тим, що помилки повинні оброблятися деяким механізмом обробки виняткових ситуацій. По крайней мене, ми погоджуємося з тим, що одних лише кодів помилок не достатньо. Нам потрібно щось серйозніше, але в теж час зберігає баланс корисності і продуктивності.

Проблема з винятками в наших мовах програмування — це те, що вони по суті є замаскованим GOTO. А ми вже з'ясували, що використання GOTO — це погано.

Кращим підходом може бути використання деякого композитного типу, збирає в собі інформацію про успіх виконання деякого блоку коду, або помилки в ньому.

Покажчики

Як зазначив Robert C. Martin, в старих мовах програмування начебто З і С++ ви можете маніпулювати покажчиками, але як тільки ви вводите поняття поліморфізму — чисті покажчики вам більше не потрібні. У Java немає покажчиків, Javascript немає покажчиків. В С# є покажчики, але вони потрібні лише в рідкісні випадках, на зразок прямого виклику WinApi-функцій.

Всі ці мови довели, що вам не потрібні покажчики для того, щоб передати що-небудь куди-небудь за посиланням. Від покажчиків можна позбутися.

Числові типи

Більшість строго типізованих мов програмування дають вам можливість вибрати між кількома числовими типами: 16-бітове ціле число, 32-бітове ціле, 32-бітне беззнакове ціле, дробове з плаваючою крапкою, і т. д. Це мало сенс в 1950-их, але рідко буває важливо сьогодні. Ми витрачаємо багато часу на мікро-оптимізації, зразок правильного вибору числового типу, втрачаючи загальну картину. сказав Дуглас Крокфорд: «В Javascript є лише один числовий тип, що є відмінною ідеєю. Шкода тільки, що це неправильний числовий тип».

image

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

Нульові покажчики

Нульові покажчики — одна з найбільш незрозумілих понять у мовах програмування. Немає нічого поганого в тому, що деяке значення може бути встановлено, а може бути відсутнім. Така концепція є в багатьох мовах програмування. У Хаскеле вона називається Maybe, F# це option, в T-SQL це null. Що спільного у всіх цих мовах — це те, що дана можливість опціональна. Ви можете оголосити значення як «nullable», але за замовчуванням воно таким не є.

Однак, за помилки Тоні Хоара, яку він сам визнав і оцінив в мільярд доларів, у багатьох мовах є нульові покажчики: C, C++, Java, C#. Проблема не в концепції «нульового покажчика», а в тому, що будь-покажчик за замовчуванням може бути нульовим, що робить неможливим зробити відмінність між випадками, в яких null — очікуване і валидное значення покажчика і тими ситуаціями, коли це дефект.

Створіть мовою без нульових покажчиків і вам ніколи не доведеться замислюватися про генерації або обробці помилок доступу за нульового покажчика.

image

Якщо Тоні Хоар прав у своїй оцінки збитку від цього бага в мільярд доларів, то позбавлення від нульового покажчика прямо зараз може зберегти нам усім багато грошей і в майбутньому. Через існування повних по Тьюрингу мов програмування без нульових покажчиків (на кшталт згаданих T-SQL, Хаскеля і F#) ми знаємо, що можемо виразити будь-яку валідність програму без даної концепції, а значить її усунення прибере величезний пласт помилок.

Зміна значень змінних

Однією з центральних концепцій в процедурному, імперативний і об'єктно-орієнтованому стилі є те, що ви можете змінити значення змінної по ходу роботи вашої програми. Це і є причина, чому це називається «змінної». Це здається логічним і інтуїтивно правильним, адже процесор містить регістри, а все, що ви робите по ходу програми — так це записуєте туди дані, виконуєте команди і прочитаєте результати. Це логічно і з іншого боку: більшість програми призначені для того, щоб якось змінити стан зовнішнього світу — записати дані у базу, відіслати email, вивести картинку на екран, роздрукувати документ і т. д.

Проте, виявилося що зміна значень змінних — величезне джерело помилок у програмному забезпеченні. Уявіть, наприклад, ось таку строчку коду на С#:

var r = this.mapper.Map(rendition);


Коли метод Map повертає значення — був параметр rendition модифікований? Якщо ви прямуєте деяким принципам — він не мав би змінитися. Але єдиний спосіб переконатися в цьому — переглянути код методу Map. А що, якщо він передає це значення далі? Раптом якийсь з них змінить його? І робитиме це лише у деяких випадках. Налагодження сильно ускладнитися. В мові С# (та і в Java, і в Javascript) немає нічого для запобігання цієї проблеми.

У складній програмі з довгим стеком викликів взагалі неможливо сказати щось певне про коді, оскільки в будь-якому методі може змінитися будь-що, а як тільки у вас будуть десятки\сотні змінних проекті — ви більше не зможете тримати їх стан в голові. «Прапор isDirty змінювався? Де? А це якось пов'язано з customerStatus?».

Давайте уявимо собі, що змінювати змінні була прибрана з мови програмування:

image

Більшість мов програмування не йдуть на такий радикальних крок, але Хаскель (при своїй повне по Тьюрингу) демонструє, що цілком можливо написати будь-яку програму без явного зміни значень змінних.

В цьому місці багато людей можуть заперечити, що Хаскель сліжком складний і неінтуітивний, але для мене ці аргументи схожі на аргументацію ортодоксів з приводу захисту оператора goto. Якщо ви довго покладалися на goto, то вам рано чи пізно доведеться вивчити нові способи виражати код без його допомоги. Точно так само, якщо ви звикли покладатися на концепцію змінюваних змінних, вам доведеться навчитися моделювати те ж саме поведінка без них.

Порівняння посилань

В об'єктно-орієнтованих мови начебто C# або Java операцією за замовчуванням для посилальних типів є порівняння посилань. Якщо дві змінних вказують на один і той же адресу пам'яті — вони вважаються рівними. Якщо дві змінних вказують на два різних блоку пам'яті (нехай навіть заповнених ідентичними даними) — вони не вважаються рівними. Це не інтуїтивно і породжує баги.

Що, якщо прибрати з мови порівняння посилань?

image

Брати і порівнювати об'єкти за їх вмістом.

Я не впевнений до кінця в цій ідеї, але з мого досвіду, коли ви що-з чим порівнюєте, то хочете знати одно чи зміст, а не дорівнює посилання. Можливо, порівняння посилань знадобиться для якоїсь рідкісної оптимізації — на цей випадок ми можемо зберегти функцію порівняння посилань де-небудь в стандартній бібліотеці. Але не як поведінка за умовчанням.

Спадкування

Навіть сьогодні успадкування зустрічається нам скрізь. Вже 20 років тому Банда Чотирьох радила нам віддавати перевагу композицію спадкоємства. Немає нічого такого, що ви можете зробити за допомогою наслідування і не можете зробити за допомогою композиції та інтерфейсів. Зворотне твердження невірне для мов з одиночним спадкуванням: є речі, які ви можете зробити за допомогою інтерфейсів (зразок реалізувати більше одного), але не можете зробити за допомогою спадкування. Композиція — це надмножество успадкування.

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

Інтерфейси

Багато строго типізовані мови (наприклад, Java і С#) мають інтерфейси, які там є механізмом реалізації поліморфізму. Таким чином ви можете об'єднати різні операції разом, як методи інтерфейсу. Проте, одне з наслідків застосування SOLID полягає в тому, що ви повинні віддавати перевагу інтерфейси, що позначають якусь одну роль, а не інтерфейси, що включають набори методів. Логічний висновок з цього — кожен інтерфейс повинен мати рівно один метод. У цьому випадку сама назва та визначення інтерфейсу ставати вже зайвим — нас цікавить тільки описувана їм операція, її параметри і результат. А для вираження цього у нас є інші засоби — делегати в С#, лямбды в Java.

В цьому немає нічого нового або страшного. Функціональні мови використовували функцію як базову одиницю композиції багато років.

Згідно мого досвіду, ви можете змоделювати всі, що завгодно з допомогою інтерфейсів з одним методом, що автоматично означає можливість змоделювати те ж саме і без самих інтерфейсів. Повторюся, тут немає ніяких «разючих відкриттів», функціональні мови всі так і роблять — і залишаються повні по Тьюрингу.

Відображення

Якщо ви коли-небудь займалися мета-програмування .NET або Java, ви, швидше за все, знаєте, що таке відображення. Це набір API і можливостей мови програмування або платформи, що дозволяє вам отримувати інформацію про код і виконувати його.

Мета-програмування — незамінний інструмент, так що шкода буде його втрачати. Однак, відображення — не єдиний спосіб скористатися мета-програмуванням. Деякі мови програмування гомоиконны, а значить програми на цих мовах являють собою структуровані дані, які самі по собі можуть служити джерелом інформації або об'єктом маніпуляцій. Подібні мови не вимагає відображення як окремої мовної можливості, оскільки мета-програмування вже зашито в мову іншим способом.

Іншими словами: відображення існує лише як засіб для мета-програмування. Якщо мета-програмування можна буде застосовувати через гомоиконность, це зробить відображення непотрібної можливістю мови.

Циклічні залежності

Хоча нульові покажчики і є найбільшим джерелом проблем в коді, є ще одна проблема, що призводить до аналогічних масштабів проблем в плані підтримки динамічної кодової бази — пов'язаність. Один з аспектів проблеми зі зв'язаністю — циклічні залежності. У мовах зразок C# або Java циклічних залежностей неможливо уникнути вбудованими засобами мови.

Ось одна з моїх помилок, яку я знайшов лише тому, що почав цілеспрямовано її шукати: в одному на перший погляд непоганому модулі AtomEventStore був інтерфейс IXmlWritable:

public interface IXmlWritable
{
void WriteTo(XmlWriter xmlWriter, IContentSerializer serializer);
}


Метод WriteTo приймає аргументом IContentSerializer, який оголошено ось так:

public interface IContentSerializer
{
void Serialize(XmlWriter xmlWriter, object value);

XmlAtomContent Deserialize(XmlReader xmlReader);
}


Зверніть увагу, що Deserialize() повертає XmlAtomContent. Як же визначений XmlAtomContent? А ось так:

public class XmlAtomContent : IXmlWritable


Дивіться, він реалізує IXmlWritable — і ось вона, циклічна залежність, яка вимагає вираження IXmlWritable через самого себе!

Я постійно перевіряю код на подібні речі, але ось ця помилка зуміла прокрастися повз мене. У мові F# (і, по-моєму, в OCaml) такий код навіть не скомпилировался б! Хоча F# і дозволяє вводити невеликі циклічні залежності на рівні модулів за допомогою ключових слів and і rec, у вас не може утворитися циклічної залежності випадково. Вам потрібно явно висловити своє бажання на створення такого зв'язку — і навіть в цьому випадку ви не можете перетнути кордон одного модуля або бібліотеки.

Яка чудова захист від щільно пов'язаного коду! Приберіть можливість випадкового створення циклічних залежностей — і отримаєте найкращий мову.

image

Дане питання навіть досліджувався «в полі»: Scott Wlaschin аналізував реальні проекти на C# F# і виявив, що проекти на F# мають менше циклічних залежностей, ніж проекти на C#. Це дослідження пізніше продовжила у своїй роботі Evelina Gabasova.

Висновки

У даній статті я спробував показати, як можна зробити мову програмування краще, прибравши з нього деяку функціональність. Приберіть надлишкову фічу — і ви залишитеся з повним по Тьюрингу мовою програмування, здатною викликати все, що завгодно (ну, майже все), але мають менше можливостей вистрілити собі в ногу.

Можливо, ідеальний мову програмування, це мова без:

  • GOTO
  • Виключення
  • Покажчики
  • Множинні числові типи
  • Нульові покажчики
  • Зміна значень змінних
  • Порівняння посилань
  • Спадкування
  • Інтерфейси
  • Відображення
  • Циклічні залежності


Я перерахував всі можливі надмірні можливості? Швидше за все немає, а значить у розробників нових мов є відмінна можливість придумати ще кращий мову, прибравши з нього що-небудь ще!
Джерело: Хабрахабр

0 коментарів

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