Кодим на python по-функциональному: познаем силу функциональной парадигмы программирования

Содержание:

Языки функционального программирования

Основная статья: Язык функционального программирования

Наиболее известными языками функционального программирования являются:

  • Лисп — (Джон Маккарти, ) и множество его диалектов, наиболее современные из которых:

    • Scheme
    • Clojure
    • Common Lisp
  • Erlang — (Joe Armstrong, 1986

    Elixir

    ) функциональный язык с поддержкой процессов.

  • APL — предшественник современных научных вычислительных сред, таких как MATLAB.
  • ML (Робин Милнер, , из ныне используемых диалектов известны Standard ML и Objective CAML).
  • F# — функциональный язык семейства ML для платформы .NET
  • Scala
  • Miranda (Дэвид Тёрнер, , который впоследствии дал развитие языку Haskell).
  • Nemerle — гибридный функционально/императивный язык.
  • XSLT и XQuery
  • Haskell — чистый функциональный. Назван в честь Хаскелла Карри.

Ещё не полностью функциональные изначальные версии и Лиспа, и APL внесли особый вклад в создание и развитие функционального программирования. Более поздние версии Lisp, такие как Scheme, а также различные варианты APL поддерживали все свойства и концепции функционального языка.

Как правило, интерес к функциональным языкам программирования, особенно чисто функциональным, был скорее научный, нежели коммерческий. Однако, такие примечательные языки как Erlang, OCaml, Haskell, Scheme (после 1986) а также специфические R (статистика), Wolfram (символьная математика), J и K (финансовый анализ), и XSLT (XML) находили применение в индустрии коммерческого программирования. Такие широко распространенные декларативные языки как SQL и Lex/Yacc содержат некоторые элементы функционального программирования, например, они остерегаются использовать переменные. Языки работы с электронными таблицами также можно рассматривать как функциональные, потому что в ячейках электронных таблиц задаётся массив функций, как правило зависящих лишь от других ячеек, а при желании смоделировать переменные приходится прибегать к возможностям императивного языка макросов.

Стили программирования

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

# императивный стиль
target = []  # создать пустой список
for item in source_list  # для каждого элемента исходного списка
    trans1 = G(item)  # применить функцию G()
    trans2 = F(trans1)  # применить функцию F()
    target.append(trans2)  # добавить преобразованный элемент в список

Функциональная версия выглядит по-другому:

# функциональный стиль
# языки ФП часто имеют встроенную функцию compose()
compose2 = lambda A, B lambda x A(B(x))
target = map(compose2(F, G), source_list)

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

Более точно, существует четыре ступени развития функционального стиля, в порядке убывания роли данных в программах:

  • Рефал (для этой категории, представленной единственным языком, нет общепринятого названия);
  • Аппликативные (Лисп, ML, Tcl, Rebol);
  • Комбинаторные (APL/J/K, /);
  • Бесточечные (чистые конкатенативные) (Joy, Cat, Factor, подмножество PostScript).

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

Объектно-ориентированное программирование

Представители объектно-ориентированных языков: С++, Java, Python.

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

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

ООП призвано решать более сложные и объемные задачи по сравнению с директивным программированием.

В основе ООП лежат такие понятия как наследование, полиморфизм и инкапсуляция.

Инкапсуляция предполагает, что малозначащие детали объекта скрыты. Объект, получая какую-либо команду, сам «знает» как ее обработать исходя из того, к какому классу он принадлежит.

Все объекты являются экземплярами классов, которые по отношению друг к другу могут выступать в роли родитель-потомок. Дочерние классы наследуют свойства родительского. В случае, когда 100% наследование не требуется, выручает так называемый полиморфизм, который предполагает переопределение методов родительского класса в дочерних классах.

Полный поиск

Complete search (он же «грубая сила» или «рекурсивный откат») — метод решения задачи путем пересечения всего пространства поиска. Точнее на протяжении всего алгоритма мы отсекаем те части пространства поиска, которые, как мы считаем, не приведут к требуемому решению. На соревнованиях по спортивному программированию использование Complete Search скорее всего приведёт к превышению лимита времени (Time Limit Exceeded — TLE), однако, это хорошая стратегия для задач с небольшим объёмом входных данных.

Пример: Задача с 8 ферзями

Нам нужно расположить на шахматной доске 8 ферзей так, чтобы ни один ферзь не нападал на другого. В наиболее простом решении нам придётся перебрать 64 млрд комбинаций и выбрать 8–4 млрд возможных расстановок. Также неплохой вариант — поставить каждого ферзя в отдельную колонну, что сводит число возможностей к 8⁸ — ~17 млн. Но лучше всего поставить каждого ферзя в отдельный ряд и в отдельную колонну. Это приведёт к 8! — 40 тыс. возможных комбинаций. В приведённой ниже реализации мы предполагаем, что каждый ферзь занимает отдельный столбец, и вычисляем номер строки для каждого из 8 ферзей.

Для TC = 8 и начальной позиции ферзя в (a, b) = (1, 1), приведённый выше код выводит следующее:

Он указывает, что всего возможно 4 расстановки, принимающих начальное положение ферзя в (r = 1, c = 1).

Использование рекурсии позволяет легче выделить пространство поиска в сравнении с итерационным решением.

Парадигмы

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

Императивное программирование

«Сначала делаем это, потом это, затем вот это»

Языки: Почти все

Абсолютно понятная любому программисту парадигма: «Человек дает набор инструкций машине».
С императивной парадигмы все начинают учить/понимать программирование.

Функциональное программирование

«Считаем выражение и используем результат для чего-нибудь еще».

Языки: Haskell, Erlang, F#

Абсолютно непонятная начинающему программисту парадигма. Мы описываем не последовательность состояний (как в императивной парадигме), а последовательность действий.

Объектно-ориентированное программирование

«Обмениваемся сообщениями между объектами, моделируя взаимодействия в реальном мире».

Языки: Почти все

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

Логическое программирование

«Отвечаем на вопрос поиском решения».

Языки: Prolog

Логическое программирование – довольно специфическая штука, но, в то же время, интересная и интуитивно понятная.
Достаточно простого примера:

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

Функциональное программирование противопоставляют императивному.

Императивное программирование подразумевает последовательность изменений состояния программы, а переменные служат для хранения этого состояния.

Функциональное программирование, наоборот, предусматривает последовательность действий над данными. Это сродни математике – мы долго пишем на доске формулу f(x), а потом подставляем x и получаем результат.

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

На практике: функциональное программирование на JavaScript

Функциональное программирование охватывает приведенные выше концепции следующими способами:

  • Основные функции реализованы с использованием чистых функций без побочных эффектов.
  • Данные неизменяемы.
  • Функциональные программы не имеют состояния.
  • Императивный код контейнера управляет побочными эффектами и выполняет декларативный, чистый код ядра.

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

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

Код доступен для запуска здесь — JSFiddle: Functional Programming with JavaScript. Он разбит на понятные, декларативные функции с четким назначением. Если мы пройдем по нему и прочитаем комментарии, то никаких дополнительных разъяснений кода не потребуется. Каждая функция ядра — модульная и зависит только от ее входных данных (чистая). Последняя функция обрабатывает ядро ​​для генерации общих выходных данных. Функция — нечистый контейнер, выполняющий ядро ​​и управляющий побочными эффектами. Мы использовали , принимающую другие функции в качестве аргументов для поддержания функционального стиля.

Чего следует избегать

Я либо допустил, либо позволил допустить все эти ошибки в коде, над которым работал. Это всего лишь советы.

  • Избегайте refs. Конечно, иногда они необходимы, но если вы чувствуете, что подбираетесь к refs, это часто означает, что вы делаете что-то неправильно. (В документации React указаны некоторые уместные способы использования refs).
  • Попытка манипулировать DOM напрямую (это очень тесно связано с Refs)
  • Вместо возвращения функций, которые рендерят компонент, лучше возвращайте функции, которые возвращают необходимую информацию для отображения компонента. В первом случае мы указываем, что делать (отобрази именно это), а во втором -возвращаем некоторую информацию (используйте эту информацию, чтобы что-то сделать).
  • Общение между siblings, а не через компоненты. Постарайтесь общаться только с другими компонентами через props.
  • Используйте чистые функциональные компоненты где это возможно. Поскольку эти компоненты не имеют методов жизненного цикла, они требуют, чтобы вы полагались на декларативный, основанный на props подход. Они также могут повысить производительность.

Зависимые типы

Примеры языков: Idris, Agda, Coq.

Скорее всего, вы привыкли к системам типов в таких языках, как C или Java, где при проверке компилятор определяет, является ли переменная целым числом, списком или строкой. Но что, если бы компилятор смог проверить переменную на соответствие типам «положительное целое число», «список длины 2» или «строка-палиндром»?

Этот вопрос приводит к рассмотрению языков программирования, в которых реализована поддержка зависимых типов, то есть возможность определять типы, проверяющие значения переменных на стадии компиляции. За примером можно обратиться к Scala и подключаемой библиотеке shapeless, которая из экспериментального проекта за несколько лет выросла до относительно стабильного инструмента.

Объявим структуру , содержащую значения ,  и :

Код выше создает переменную , для которой определение типа указывает не только на то, что это с целыми числами внутри, но и на то, что это длины 3. Компилятор может использовать данную информацию для выявления ошибок.

Используем метод для попарного сложения значений:

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

Библиотека shapeless довольно привлекательна, но существуют гораздо более мощные решения, например, язык Idris, в котором типы являются объектами первого класса, и, соответственно, система зависимых типов реализована элегантнее и чище. Ради интереса можете посмотреть сравнительную презентацию Scala vs Idris: Dependent Types, Now and in the Future.

Различные методы формальной верификации разрабатываются довольно давно, но часто оказываются слишком громоздкими для использования в стандартном программировании. Система зависимых типов в языках, подобных Idris, может оказаться полезной для поиска более простых и практичных решений, которые при этом сильно улучшат реализацию концепции «система типов для поиска ошибок». Несмотря на определенные ограничения, при правильном подходе это может стать хорошим инструментом для статических проверок.

История

Первыми императивными языками были машинные инструкции (коды) — команды, готовые к исполнению компьютером сразу (без каких-либо преобразований). В дальнейшем были созданы ассемблеры, и программы стали записывать на языках ассемблеров. Ассемблер — компьютерная программа, предназначенная для преобразования машинных инструкций, записанных в виде текста на языке, понятном человеку (языке ассемблера), в машинные инструкции в виде, понятном компьютеру (машинный код). Одной инструкции на языке ассемблера соответствовала одна инструкция на машинном языке. Разные компьютеры поддерживали разные наборы инструкций. Программы, записанные для одного компьютера, приходилось заново переписывать для переноса на другой компьютер. Были созданы языки программирования высокого уровня и компиляторы — программы, преобразующие текст с языка программирования на язык машины (машинный код). Одна инструкция языка высокого уровня соответствовала одной или нескольким инструкциям языка машины, и для разных машин эти инструкции были разными. Первым распространённым высокоуровневым языком программирования, получившим применения на практике, стал язык Fortran, разработанный Джоном Бэкусом в 1954 году. Fortran является компилируемым языком программирования, позволяет использовать именованные переменные, составные выражения, подпрограммы и многие другие элементы, распространённые в императивных языках. Для упрощения выражения математических алгоритмов в конце 1950‑х годов был разработан язык Algol. В дальнейшем Algol послужил базой для написания операционных систем для некоторых моделей компьютеров. Языки COBOL (1960 год) и Basic (1964 год) стали первыми языками, разработчики которых пытались сделать языки похожими на английский язык. В 1970-х годах Никлаус Вирт разработал язык Pascal. Денис Ритчи создал язык C. В 1978 году команда разработчиков из фирмы Honeywell начала разработку языка Ada, через четыре года опубликовала требования для его работы; спецификация языка увидела свет в 1983 году, была обновлена в и -2006 годах.

В 1980-х годах возрос интерес к объектно-ориентированному программированию (ООП). В 1980 году сотрудники исследовательского центра Xerox PARC на основе языка Smalltalk, разработанного Аланом Кэем в 1969 году, создали язык Smalltalk-80. Бьерн Страуструп на основе языка C и по образу и подобию языка Simula (предположительно, первого в мире ООП-языка, разработанного ещё в 1960-х годах) разработал язык C++. Первая реализация C++ была создана в 1985 году. В 1987 году Ларри Уолл выпустил язык Perl и интерпретатор для него. В 1990 году Гвидо ван Россум выпустил язык Python. В 1994 году в фирме Sun Microsystems был разработан язык Java. В 1995 году Расмус Лердорф разработал язык PHP. Язык Ruby был выпущен в 1995 году. Язык C# был выпущен в 2002 году вместе с программной платформой (фреймворком) .NET Framework, поддерживающей несколько языков.

Определение функции. Оператор def

В языке программирования Python функции определяются с помощью оператора def. Рассмотрим код:

def countFood():
    a = int(input())
    b = int(input())
    print("Всего", a+b, "шт.")

Это пример определения функции. Как и другие сложные инструкции вроде условного оператора и циклов функция состоит из заголовка и тела. Заголовок оканчивается двоеточием и переходом на новую строку. Тело имеет отступ.

Ключевое слово def сообщает интерпретатору, что перед ним определение функции. За def следует имя функции. Оно может быть любым, также как и всякий идентификатор, например, переменная. В программировании весьма желательно давать всему осмысленные имена. Так в данном случае функция названа «посчитатьЕду» в переводе на русский.

После имени функции ставятся скобки. В приведенном примере они пустые. Это значит, что функция не принимает никакие данные из вызывающей ее программы. Однако она могла бы их принимать, и тогда в скобках были бы указаны так называемые параметры.

После двоеточия следует тело, содержащее инструкции, которые выполняются при вызове функции. Следует различать определение функции и ее вызов. В программном коде они не рядом и не вместе. Можно определить функцию, но ни разу ее не вызвать. Нельзя вызвать функцию, которая не была определена. Определив функцию, но ни разу не вызвав ее, вы никогда не выполните ее тела.

История

Своим современным значением в научно-технической области термин «парадигма» обязан, по-видимому, Томасу Куну и его книге «Структура научных революций». Кун называл парадигмами устоявшиеся системы научных взглядов, в рамках которых ведутся исследования. Согласно Куну, в процессе развития научной дисциплины может произойти замена одной парадигмы на другую, при этом старая парадигма ещё продолжает некоторое время существовать и даже развиваться благодаря тому, что многие её сторонники оказываются по тем или иным причинам неспособны перестроиться для работы в другой парадигме.

Термин «парадигма программирования» впервые применил в 1978 году Роберт Флойд в своей лекции лауреата премии Тьюринга. Флойд отмечает, что в программировании можно наблюдать явление, подобное парадигмам Куна, но, в отличие от них, парадигмы программирования не являются взаимоисключающими: если прогресс искусства программирования в целом требует постоянного изобретения и усовершенствования парадигм, то совершенствование искусства отдельного программиста требует, чтобы он расширял свой репертуар парадигм.

Таким образом, по мнению Роберта Флойда, в отличие от парадигм в научном мире, описанных Куном, парадигмы программирования могут сочетаться, обогащая инструментарий программиста.

Важно отметить, что парадигма программирования не определяется однозначно языком программирования; практически все современные языки программирования в той или иной мере допускают использование различных парадигм. Так на языке Си, который не является объектно-ориентированным, можно работать в соответствии с принципами объектно-ориентированного программирования, хотя это и сопряжено с определёнными сложностями; функциональное программирование можно применять при работе на любом императивном языке, в котором имеются функции (для этого достаточно не применять присваивание)

Чистые функции и ленивый компилятор

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

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

Как мы уже выяснили, в функциональной парадигме можно распоряжаться функциями как угодно. Но больше всего выгоды мы получаем, когда пишем «чистые функции». Чистая функция – это функция без побочных эффектов, а значит, она не зависит от своего окружения и не изменяет его состояния.

Применение чистых функций дает нам ряд преимуществ:

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

Для увеличения производительности в ФП также используются ленивые вычисления. Яркий пример:

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

В результате несколько «степеней свободы» получает не только программист, но и компилятор.

Функциональное и логическое программирование

Представители функциональных языков: List, Haskell.

Представитель логических языков: Prolog.

Декларативная программа заявляет (декларирует), что должно быть достигнуто в качестве цели. Важным является точная формулировка задачи. Программист не задает алгоритм для ее решения.

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

В логическом программировании программы выражены в виде формул математической логики, и решение задачи достигается путем вывода логических следствий из них.

Императивное vs. декларативное программирование

Ровно год назад мы хорошо подискутировали с одним человеком о том, что часто введение дополнительных ограничений в язык программирования дает больше свободы. Так лозунг «нет goto!» привел к структурному программированию, «код в модули!» — к модульному, «код в классы!» — к ООП. Следующее на очереди — функциональное программирование с лозунгом «функция это тоже аргумент и результат» и «все функции чистые». Часто функциональные программирование отождествляют с декларативным, противопоставляя его традиционному императивному. Пол года назад я понял, что до сих пытаюсь писать в императивном стиле даже с использованием функциональных нововведений.

Если представить программу в виде графа выполнений (узлы — это данные или операции, а переходы — это трансфер данных), то дополнительные ограничения к языку программирования — это всего лишь требования к виду графа. Эти требования помогают де юре исключить сложные ситуации или дают компилятору свободу в выборе способа исполнения программы. В идеальном функциональном языке, казалось бы, странный запрет «после присваивания переменная не может менять своего значения» приводит к отсутствию циклов в графе, то есть отсутствию зацикливания по определению.

Чистые функциональные языки типа F# существуют, но их область применения ограничена. Как минимум, функция ввода данных от пользователя не является чистой (результат вычисления зависит только от параметров). Она возвращает каждый раз разные значения, не зависящие от входных параметров. Поэтому речь идет лишь о поддержке функциональных возможностей в традиционных языках. Так в C# поддерживаются анонимные функции, лямбда-выражения, итераторы, LINQ. Анонимные функции и итераторы также доступны в PHP и JavaScript. Всех этих возможностей уже достаточно, чтобы привести часть программы к декларативному виду.

Итак две противоположности

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

Примером декларативного языка служит HTML, описывающий содержание страницы, а не способ её отображения на экране. Популярный сборщик пакетов Maven описывает лишь зависимости между пакетами, а не последовательность их установки. Язык запросов SQL описывает, что мы хотим достать из базы данных, как именно решает сервер. Декларативное описание задачи более наглядно и легче формулируется, так как мы чаще знаем, чего хотим, но не знаем как сделать.

Базовой операцией при декларативном подходе является операция над множеством или над всеми элементами множества. Причем для последнего случая порядок обработки не важен, так как обрабатывающая функция должна быть чистой. В C#, PHP, Java, Javascript множество реализуется как массив или последовательность, генерируемая итератором. Ниже пример сравнения двух подходов на C#

Подводя итог: функциональное реактивное программирование

FRP представляет собой написание действий, которые, используя чистые функции, реагируют на события и переводят состояние с предыдущего момента времени к следующему. FRP в реализации JavaScript не придерживается двух основных принципов FRP Конала Эллиота, но в абстрагировании от оригинальной концепции есть определенный смысл. JavaScript сильно зависит от побочных эффектов и императивного программирования, но мы, безусловно, можем использовать преимущества концепций FRP для улучшения нашего JS.

Наконец, рассмотрим эту цитату из первого издания Eloquent JavaScript: «Fu-Tzu написал небольшую программу, использующую глобальное состояние и сомнительные переплетения, и, прочитав ее, студент спросил:« Вы предупреждали нас против этих методов, но я нахожу их в вашей программе. Как такое могло случиться?». Фу-Цзы ответил: «Нет необходимости забирать водяной шланг, когда дом не горит». .

Дополнительную информацию о функциональном реактивном программировании можно найти на следующих ресурсах:

  • Функциональное реактивное программирование для начинающих
  • Функциональное реактивное заблуждение
  • Haskell — функциональное реактивное программирование
  • Создание реактивной анимации
  • Более элегантная спецификация для FRP
  • Elm — прощание с FRP
  • Ранние успехи и новые направления в функциональном реактивном программировании
  • Разрушение FRP
  • Rx не является FRP

Заключение

Мы закончим еще одной отличной цитатой из первого издания Eloquent JavaScript: «Студент долгое время сидел за своим компьютером, мрачно хмурился и пытался написать красивое решение сложной проблемы, но не мог найти правильный подход. Фу-Цу ударил его по затылку и крикнул: ‘Введите что-нибудь!’. Студент начал писать уродливое решение, и после того, как он закончил, он внезапно понял прекрасное решение».

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

Используя этот Справочник в качестве отправной точки, вы можете начать использовать представленные концепции и парадигмы программирования для повышения своего уровня владения JavaScript. Если по описанным темам что-либо еще неясно, пожалуйста, обратитесь к ссылкам в каждом разделе за дополнительными ресурсами. Позже мы рассмотрим новые концепции в следующей статье Справочника современных концепций JavaScript!

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *