Придумают же! Самые крутые фишки языков программирования
Всё самое интересное: от расширений до постепенной типизации.
Альберто Блинчиков для Skillbox Media
Джереми Грифски
(Jeremy Grifski)
об авторе
Программист-энтузиаст. Получает докторскую степень в области инженерных наук, хочет стать преподавателем. Любит писать о коде. Сайт автора: The Renegade Coder.
Я давно пишу серию статей «Примеры программ на каждом языке». За это время я успел пощупать с полсотни языков программирования и хочу поделиться их самыми прикольными фишками.
Примечание переводчика
Интересных особенностей так много, что мы решили растянуть удовольствие и разбили перевод на две статьи. В этой части:
А вторая часть статьи ждёт вас тут.
Методы расширения
Одна из самых прикольных фич, которые я для себя открыл, — это расширения. Впервые я столкнулся с ними, когда кодил Hello World на Kotlin. Конечно, в такой простой программе они не пригодились, зато я узнал, что они существуют.
Расширения позволяют добавлять новые методы к существующим классам, не расширяя сами классы.
Представьте, что нам очень нравится класс String в Java, но мы хотим сделать его ещё лучше, добавив новый метод. Единственный способ сделать это — создать свой класс, который расширяет String:
Примечание переводчика
Создать такой класс не получится, потому что String в Java — финальный класс, его нельзя расширять.
Между тем в Kotlin достаточно написать метод, который напрямую расширяет класс String:
Теперь каждый раз, когда мы создаём экземпляр String, мы можем обратиться к его методу mutate, как будто это обычный публичный метод класса String.
Конечно, у расширений есть и недостатки. Представьте, что произойдёт, если к стандартным методам класса String добавят метод с тем же именем mutate. Наверняка в вашей программе возникнет какая-нибудь хитрая ошибка. Но не думаю, что такие совпадения случаются часто.
Так или иначе, я не придумал ничего лучше, чем использовать расширения для быстрого прототипирования. Расскажите мне, если у вас есть идеи получше.
Макросы
Другая классная фишка языков программирования — макросы. Я столкнулся с ними, когда писал Hello World на Rust. Ведь вывод в консоль на Rust реализован именно в виде макроса.
Вообще, макросы — это понятие из области метапрограммирования. Они позволяют расширять язык напрямую — добавляя правила к дереву абстрактного синтаксиса. Такие правила создаются с помощью сопоставления с образцом (pattern matching).
Объяснение получилось довольно мутным — поэтому рассмотрим пример на Rust:
Этот код взят из исходников самого Rust. Как видите, в макросе print используется одно сопоставление с образцом, которое принимает любое число аргументов и пытается перед выводом привести их к подходящему виду.
Если сопоставление с образцом даётся вам трудновато, возможно, стоит получше изучить регулярные выражения — у них похожий синтаксис.
Вы наверняка заметили, что работать с макросами не так-то просто. Их трудно писать и так же сложно отлаживать. В документации по Rust макросы и вовсе называют крайним средством, использовать которое можно редко и только по рецепту :)
Автоматические свойства
Я узнал о них, когда разбирался с C# (см. статью «Hello World на C#»).
Автоматические свойства (automatic properties) — это, по сути, сокращения для геттеров и сеттеров в объектно-ориентированных языках, то есть синтаксический сахар.
Допустим, у нас есть класс Person и мы хотим добавить в него поле name: у людей есть имена, так что всё логично. Вот как это будет выглядеть на Java:
Теперь, если мы захотим заполнить имя, то нам, вероятно, придётся написать общедоступный метод (с модификатором public). Тогда мы сможем обновить приватное поле. Именно такие методы чаще всего называют сеттерами — ведь они устанавливают (set) свойство объекта. Однако официально они называются мутаторами (mutator methods).
На Java код для создания мутатора выглядит так:
Мы написали уже шесть строк кода, но даже не можем получить значение переменной name вне класса. Чтобы это сделать, нужно написать геттер, или метод доступа:
В языках с поддержкой автоматических свойств можно просто выкинуть эти шесть строк бойлерплейта. Вот полный аналог нашего джавишного класса на C#:
С автоматическими свойствами мы пишем всего одну строку кода для каждого поля, которое хотим открыть другим классам, — и это здорово! Без такой фичи нам пришлось написать шесть строк.
Опциональные цепочки
Наш новый герой — необязательное связывание, или цепочки опциональных вызовов (optional chaining). Я впервые столкнулся с ними, когда писал Hello World на Swift. Для приветствия миру эта фича не пригодилась, но познакомиться с ней было интересно.
Понять, что такое цепочки опциональных вызовов, нам помогут необязательные переменные. Для начала разберёмся с ними.
В Swift переменные не могут быть пустыми. Иными словами, они не могут хранить значение NIL — по крайней мере, напрямую. И это отлично, потому что так мы уверены, что любая переменная содержит какое-то значение.
Конечно, иногда нам необходимо передать в переменную NIL. К счастью, Swift позволяет это сделать с помощью необязательных переменных. Они оборачивают реальное значение (в том числе NIL) в контейнер, из которого это значение потом можно будет извлечь:
В этом примере мы объявляем необязательную строковую переменную и присваиваем ей значение «Hello, World!». Мы знаем, что в переменной лежит строка, и можем безо всяких проверок на NIL извлечь её и вывести на печать.
Примечание переводчика
С помощью символа ? после типа String мы поясняем компилятору, что printString — не просто строковая переменная, а опциональная (необязательная). То есть в ней может лежать строковое значение или пустое значение (NIL).
С помощью символа ! мы извлекаем значение из такой переменной и выводим его на печать.
Конечно, извлекать значение вот так, безо всяких проверок, — плохая практика. Здесь я пренебрёг этим правилом, чтобы не усложнять пример.
Концепцию необязательных значений также применяют к вызовам методов и полям. В этом случае речь как раз идёт о цепочках опциональных вызовов. Представьте, что у нас есть длинная цепочка вызовов методов:
В этом примере мы получаем из командной строки какое-то строковое значение и делим его на отрезки, ограниченные дефисами (-). Потом берём пятый из этих отрезков-строк и получаем из него седьмой по счёту символ. Если вызов хотя бы одного из трёх методов завершится неудачно, вся наша программа обрушится.
С необязательным связыванием мы можем перехватить значение NIL в любом месте цепочки и обработать его. Тогда вместо сбоя мы получим значение important_char, равное NIL. Как по мне, это гораздо лучше, чем иметь дело с пирамидой смерти (the pyramid of doom).
Примечание переводчика
Когда аргументами функций становятся другие функции, внутри которых тоже есть функции, может получиться что-то такое (пример на псевдокоде):
Код функций принято записывать со смещением вправо — так проще понять, какой текст к какой функции относится. В итоге код действительно напоминает пирамиду.
Если уровней вложенности очень много, то вершина пирамиды уезжает далеко вправо. Читать, а тем более поддерживать такой код — смерти подобно :)
Лямбда-выражения
Без лямбда-выражений этот список был бы неполным. Лямбда-выражения — не новая концепция (смотрите «Hello World на Lisp»), они даже старше компьютеров. И всё же их продолжают добавлять в современные языки программирования — даже в такие проработанные и устоявшиеся, как Java (в Java лямбды поддерживаются с 8-й версии; она вышла в 2014 году. — Пер.).
Честно говоря, сам я впервые услышал про лямбда-выражения три или четыре года назад, когда изучал Java. Тогда я толком не понял, что в них интересного, — да и не особенно пытался узнать.
Однако пару лет спустя я начал писать на Python, а в нём куча библиотек с открытым исходным кодом, которые вовсю используют лямбда-выражения. Так что в какой-то момент мне всё-таки пришлось с ними подружиться.
Если вы не слышали о лямбдах, то, возможно, знаете хотя бы об анонимных функциях. Лямбды — это почти то же самое, но с одним отличием: их можно использовать как данные. А точнее — упаковать лямбда-выражение в переменную, а потом обращаться с ним как с обычными данными. Например:
Тут мы создали функцию, сохранили её в переменную и вызвали, как любую другую обычную функцию. Фактически можно даже создать функцию, которая будет возвращать другие функции — то есть динамически генерировать функции.
Круто!
Постепенная типизация (gradual typing)
Если вы хоть немного программировали, то, вероятно, знакомы с двумя основными видами типизации: статической и динамической. Только не путайте эти понятия с явной и неявной типизацией, а ещё с сильной и слабой типизацией. Это всё абсолютно разные вещи.
Примечание переводчика
При статической типизации тип переменной известен и проверяется во время компиляции, а при динамической — определяется во время выполнения в зависимости от значения переменной.
При явной типизации задаётся тип каждой переменной. Это частный случай статической типизации. При неявной типизации типы указывать не обязательно.
При сильной типизации преобразуются один в другой только совместимые между собой типы. Например, в сильно типизированном языке нельзя без явных преобразований передать в подпрограмму строку, если параметр подпрограммы объявлен с числовым типом.
Слабо типизированные языки относятся к совместимости типов более свободно. Например, в JavaScript можно складывать строку с числом. Ошибок не будет, просто число преобразуется в строку.
Пересечение между статической и динамической типизацией называется постепенной (gradual) типизацией. И для меня это одна из самых крутых фишек в языках программирования. Пользователь сам указывает, когда ему нужна статическая типизация, а по умолчанию действует динамическая.
Во многих языках постепенная типизация реализуется через объявления типов (как в явно типизированных языках):
Это функция на Python, у которой явно указаны типы входного и выходного параметров (у обоих тип float. — Пер.). Но можно обойтись и без объявления типов:
Теперь ничто не помешает передать в эту функцию вообще всё что угодно. Однако первый вариант реализации позволяет статически проверять типы — встроенными в IDE инструментами статического анализа или какими-то внешними утилитами. Я бы сказал, что это беспроигрышный вариант.
Хоть я и выбрал для иллюстрации Python, но впервые столкнулся с постепенной типизацией, когда писал Hello World на языке Hack. Похоже, Facebook* и правда решил улучшить систему типов в PHP.
Что дальше
В следующем выпуске вас ждут множественная диспетчеризация, неизменяемость значений и даже встроенный ассемблер. А пока можно узнать больше про лямбда-выражения, виды типизации, языки программирования Python и Java.