Код
#статьи

Придумают же! Самые крутые фишки языков программирования

Всё самое интересное: от расширений до постепенной типизации.

Альберто Блинчиков для Skillbox Media

Джереми Грифски

(Jeremy Grifski)


об авторе

Программист-энтузиаст. Получает докторскую степень в области инженерных наук, хочет стать преподавателем. Любит писать о коде. Сайт автора: The Renegade Coder.


Ссылки


Я давно пишу серию статей «Примеры программ на каждом языке». За это время я успел пощупать с полсотни языков программирования и хочу поделиться их самыми прикольными фишками.

Примечание переводчика

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

А вторая часть статьи ждёт вас тут.

Методы расширения

Одна из самых прикольных фич, которые я для себя открыл, — это расширения. Впервые я столкнулся с ними, когда кодил Hello World на Kotlin. Конечно, в такой простой программе они не пригодились, зато я узнал, что они существуют.

Расширения позволяют добавлять новые методы к существующим классам, не расширяя сами классы.

Представьте, что нам очень нравится класс String в Java, но мы хотим сделать его ещё лучше, добавив новый метод. Единственный способ сделать это — создать свой класс, который расширяет String:

public class StringPlusMutation extends String {
  public String mutate() {
    // добавляем код для изменений
  }
}

Примечание переводчика

Создать такой класс не получится, потому что String в Java — финальный класс, его нельзя расширять.

Между тем в Kotlin достаточно написать метод, который напрямую расширяет класс String:

fun String.mutate(){
  // добавляем код для изменений
}

Теперь каждый раз, когда мы создаём экземпляр String, мы можем обратиться к его методу mutate, как будто это обычный публичный метод класса String.

Конечно, у расширений есть и недостатки. Представьте, что произойдёт, если к стандартным методам класса String добавят метод с тем же именем mutate. Наверняка в вашей программе возникнет какая-нибудь хитрая ошибка. Но не думаю, что такие совпадения случаются часто.

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

Макросы

Другая классная фишка языков программирования — макросы. Я столкнулся с ними, когда писал Hello World на Rust. Ведь вывод в консоль на Rust реализован именно в виде макроса.

Вообще, макросы — это понятие из области метапрограммирования. Они позволяют расширять язык напрямую — добавляя правила к дереву абстрактного синтаксиса. Такие правила создаются с помощью сопоставления с образцом (pattern matching).

Объяснение получилось довольно мутным — поэтому рассмотрим пример на Rust:

macro_rules! print {
    ($($arg:tt)*) => ($crate::io::_print(format_args!($($arg)*)));
}

Этот код взят из исходников самого Rust. Как видите, в макросе print используется одно сопоставление с образцом, которое принимает любое число аргументов и пытается перед выводом привести их к подходящему виду.

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

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

Автоматические свойства

Я узнал о них, когда разбирался с C# (см. статью «Hello World на C#»).

Автоматические свойства (automatic properties) — это, по сути, сокращения для геттеров и сеттеров в объектно-ориентированных языках, то есть синтаксический сахар.

Допустим, у нас есть класс Person и мы хотим добавить в него поле name: у людей есть имена, так что всё логично. Вот как это будет выглядеть на Java:

public class Person {
  private String name;
}

Теперь, если мы захотим заполнить имя, то нам, вероятно, придётся написать общедоступный метод (с модификатором public). Тогда мы сможем обновить приватное поле. Именно такие методы чаще всего называют сеттерами — ведь они устанавливают (set) свойство объекта. Однако официально они называются мутаторами (mutator methods).

На Java код для создания мутатора выглядит так:

public setName(String name) {
  this.name = name;
}

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

public getName() {
  return this.name;
}

В языках с поддержкой автоматических свойств можно просто выкинуть эти шесть строк бойлерплейта. Вот полный аналог нашего джавишного класса на C#:

public class Person
{
  public string Name { get; set; }
}

С автоматическими свойствами мы пишем всего одну строку кода для каждого поля, которое хотим открыть другим классам, — и это здорово! Без такой фичи нам пришлось написать шесть строк.

Опциональные цепочки

Наш новый герой — необязательное связывание, или цепочки опциональных вызовов (optional chaining). Я впервые столкнулся с ними, когда писал Hello World на Swift. Для приветствия миру эта фича не пригодилась, но познакомиться с ней было интересно.

Понять, что такое цепочки опциональных вызовов, нам помогут необязательные переменные. Для начала разберёмся с ними.

В Swift переменные не могут быть пустыми. Иными словами, они не могут хранить значение NIL — по крайней мере, напрямую. И это отлично, потому что так мы уверены, что любая переменная содержит какое-то значение.

Конечно, иногда нам необходимо передать в переменную NIL. К счастью, Swift позволяет это сделать с помощью необязательных переменных. Они оборачивают реальное значение (в том числе NIL) в контейнер, из которого это значение потом можно будет извлечь:

var printString: String?
printString = "Hello, World!"
print(printString!)

В этом примере мы объявляем необязательную строковую переменную и присваиваем ей значение «Hello, World!». Мы знаем, что в переменной лежит строка, и можем безо всяких проверок на NIL извлечь её и вывести на печать.

Примечание переводчика

С помощью символа ? после типа String мы поясняем компилятору, что printString — не просто строковая переменная, а опциональная (необязательная). То есть в ней может лежать строковое значение или пустое значение (NIL).

С помощью символа ! мы извлекаем значение из такой переменной и выводим его на печать.

Конечно, извлекать значение вот так, безо всяких проверок, — плохая практика. Здесь я пренебрёг этим правилом, чтобы не усложнять пример.

Концепцию необязательных значений также применяют к вызовам методов и полям. В этом случае речь как раз идёт о цепочках опциональных вызовов. Представьте, что у нас есть длинная цепочка вызовов методов:

important_char = commandline_input.split('-').get(5).charAt(7)

В этом примере мы получаем из командной строки какое-то строковое значение и делим его на отрезки, ограниченные дефисами (-). Потом берём пятый из этих отрезков-строк и получаем из него седьмой по счёту символ. Если вызов хотя бы одного из трёх методов завершится неудачно, вся наша программа обрушится.

С необязательным связыванием мы можем перехватить значение NIL в любом месте цепочки и обработать его. Тогда вместо сбоя мы получим значение important_char, равное NIL. Как по мне, это гораздо лучше, чем иметь дело с пирамидой смерти (the pyramid of doom).

Примечание переводчика

Когда аргументами функций становятся другие функции, внутри которых тоже есть функции, может получиться что-то такое (пример на псевдокоде):

функция1(аргумент1, function (аргумент2, аргумент3) {
    функция2(аргумент4, function (аргумент5, аргумент6) {
        функция3(аргумент7, function (аргумент8, аргумент9) {
        });
    });
});

Код функций принято записывать со смещением вправо — так проще понять, какой текст к какой функции относится. В итоге код действительно напоминает пирамиду.

Если уровней вложенности очень много, то вершина пирамиды уезжает далеко вправо. Читать, а тем более поддерживать такой код — смерти подобно :)

Лямбда-выражения

Без лямбда-выражений этот список был бы неполным. Лямбда-выражения — не новая концепция (смотрите «Hello World на Lisp»), они даже старше компьютеров. И всё же их продолжают добавлять в современные языки программирования — даже в такие проработанные и устоявшиеся, как Java (в Java лямбды поддерживаются с 8-й версии; она вышла в 2014 году. — Пер.).

Честно говоря, сам я впервые услышал про лямбда-выражения три или четыре года назад, когда изучал Java. Тогда я толком не понял, что в них интересного, — да и не особенно пытался узнать.

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

Если вы не слышали о лямбдах, то, возможно, знаете хотя бы об анонимных функциях. Лямбды — это почти то же самое, но с одним отличием: их можно использовать как данные. А точнее — упаковать лямбда-выражение в переменную, а потом обращаться с ним как с обычными данными. Например:

increment = lambda x: x + 1
increment(5)  # вернёт 6

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

def make_incrementor(n):
  return lambda x: x + n

addFive = make_incrementor(5)
addFive(10)  # вернёт 15

Круто!

Постепенная типизация (gradual typing)

Если вы хоть немного программировали, то, вероятно, знакомы с двумя основными видами типизации: статической и динамической. Только не путайте эти понятия с явной и неявной типизацией, а ещё с сильной и слабой типизацией. Это всё абсолютно разные вещи.

Примечание переводчика

При статической типизации тип переменной известен и проверяется во время компиляции, а при динамической — определяется во время выполнения в зависимости от значения переменной.

При явной типизации задаётся тип каждой переменной. Это частный случай статической типизации. При неявной типизации типы указывать не обязательно.

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

Слабо типизированные языки относятся к совместимости типов более свободно. Например, в JavaScript можно складывать строку с числом. Ошибок не будет, просто число преобразуется в строку.

Пересечение между статической и динамической типизацией называется постепенной (gradual) типизацией. И для меня это одна из самых крутых фишек в языках программирования. Пользователь сам указывает, когда ему нужна статическая типизация, а по умолчанию действует динамическая.

Во многих языках постепенная типизация реализуется через объявления типов (как в явно типизированных языках):

def divide(dividend: float, divisor: float) -> float:
  return dividend / divisor

Это функция на Python, у которой явно указаны типы входного и выходного параметров (у обоих тип float. — Пер.). Но можно обойтись и без объявления типов:

def divide(dividend, divisor):
  return dividend / divisor

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

Хоть я и выбрал для иллюстрации Python, но впервые столкнулся с постепенной типизацией, когда писал Hello World на языке Hack. Похоже, Facebook* и правда решил улучшить систему типов в PHP.

Что дальше

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



* Решением суда запрещена «деятельность компании Meta Platforms Inc. по реализации продуктов — социальных сетей Facebook* и Instagram* на территории Российской Федерации по основаниям осуществления экстремистской деятельности».
Научитесь: Веб-разработчик с нуля до PRO Узнать больше
Понравилась статья?
Да

Пользуясь нашим сайтом, вы соглашаетесь с тем, что мы используем cookies 🍪

Ссылка скопирована