Что такое контекст программирование
Перейти к содержимому

Что такое контекст программирование

  • автор:

Контекст

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

Операнд1 знак операнд2

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

Операнд1 ? операнд2 : операнд3

Операции используются для формирования выражений, которые вычисляются в процессе выполнения программы. Синтаксически выражение представляется в виде последовательности операндов и знаков операций, которая интерпретируется компилятором в определенную последовательность выполнения операций с учетом их старшинства — правил, определяющих, какая операция должна выполняться первой, второй и т. д. Для изменения порядка выполнения операций служат круглые скобки. Результатом выполнения выражения является некоторое вычисленное значение. Это и есть основное предназначение выражения — вычислять в программе некоторое значение на основе других известных значений. В языке Perl определено несколько десятков операций, причем некоторые операции являются обычными с точки зрения любого языка — умножение, деление, вычитание и сложение целых и вещественных чисел, конкатенация строк и т. д. — и для их записи используется общепринятый синтаксис, а другие операции, например, операция чтения входного потока <>, представляют вызовы определенных в языке стандартных функций или являются действиями с внешними данными. Все операции языка можно разбить на три большие группы: скалярные, списковые и не принадлежащие к первым двум, причем в зависимости от используемых подтипов скалярных операндов (числа или строки) некоторые скалярные операции вычисляются по разным алгоритмам. Каждая операция формирует для своих операндов определенный контекст, в котором они должны вычисляться, причем контекст одного операнда операции может зависеть от типа другого операнда, как в случае, например, с операцией присваивания. Поэтому, прежде чем переходить к подробному изучению операций языка Perl, мы расширим наше представление о контексте — понятии, широко используемом в этом языке.

Контекст

Все вычисления в языке Perl осуществляются в определенном контексте, который влияет на интерпретацию результатов вычисления выражений и на процесс вычисления операций в них. Контекст можно представить себе как некоторое окружение, в котором выполняется операция и интерпретируется ее значение. Мы уже знаем два основных контекста: скалярный и списковый. Идея контекста заключается в том, что если в данном месте выполнения некоторой операции требуется скалярное значение, то операция вернет именно скаляр, а если список (несколько значений) — то именно список и будет результатом ее выполнения, даже если он будет представлен единственным скалярным значением. Это очень похоже на то, как употребляются некоторые английские слова, у которых единственное и множественное число не различаются и имеют одинаковое написание, например «sheep» (овца) или «fish» (рыба). Единственный способ понять, употреблены они в единственном или множественном числе, — это контекст предложения. С точки зрения современных концепций языков программирования реализация контекста для операций Perl осуществляется с помощью механизма перегрузки: в зависимости от того, какой тип данных операция должна вернуть в соответствующем контексте, для ее выполнения вызываются разные функции. Операция в зависимости от контекста, в котором она вычисляется, может определять контекст вычисления своих операндов. Например, операция substr выделения подстроки (чуть позже мы узнаем, что каждая стандартная функция Perl может рассматриваться как операция, используемая в выражениях) «создает» для своих операндов скалярный контекст. Поэтому операция чтения из входного потока , 0, 1); А это приведет к тому, что она будет читать только одну строку из входного потока, реализуя скалярный контекст для своего операнда. Эта же операция чтения из стандартного потока , употребленная в списковом контексте, создаваемом операцией sort сортировки списка, будет создавать списковый контекст для своего операнда, что приведет к чтению всех вводимых пользователем строк, пока не встретится символ конца файла (Ctrl+Z):

sort();
ВНИМАНИЕ Осуществляет ли операция перегрузку в зависимости от контекста использования, можно узнать, обратившись к документации, поставляемой с интерпретатором языка.

Большинство перегружаемых операций «требуют» вычисления своих операндов в том контексте, в котором они сами выполняются. Несколько иначе ведет себя операция присваивания, которая создает контекст для правого операнда в зависимости от типа левого операнда. Если последний является скалярной переменной, то правый операнд вычисляется в скалярном контексте, а если левый операнд представлен массивом, то это предписывает вычислять правый операнд в списковом контексте. Скалярный контекст можно подразделить на числовой, строковый и безразличный. Однако ни одна операция не может определить, вычисляется ли она в одном из перечисленных контекстов, так как «переключение» между этими контекстами осуществляется самим интерпретатором — при необходимости строки преобразуются в числовые значения и наоборот. Операция может только «понять», вычисляется ли она в скалярном или списковом контексте. Иногда вообще безразлично, возвращается ли операцией число или строка. Подобное происходит в операции присваивания. Здесь не важен тип присваиваемого значения — переменная или элемент массива, определяемые в левой части операции присваивания, просто принимают скалярный подтип возвращаемого значения операции правой части. Такой контекст называется безразличным. Для выполнения некоторых операторов и операций Perl требуется трактовать результаты вычисления выражений как булевы значения «истина» или «ложь». Такое поведение предоставляет булевый контекст — специальный тип скалярного контекста, в котором скалярная величина трактуется как «истина», если только она не равна пустой строке «», строке «0» или числу 0 (целому или вещественному). Перечисленные значения, и только они, могут представлять в программе Perl «ложь». Строки «00», «0.0», «0e0» и т. п. рассматриваются в булевом контексте как «истина». Этот контекст появляется в условных операторах и простых операторах с модификаторами, о которых речь пойдет в главе 5. Другим специфическим типом скалярного контекста является void-контекст. Он не только не заботится о подтипе возвращаемого значения — скалярный он или числовой, — но ему и не надо никакого возвращаемого значения. Этот контекст появляется при вычислении выражения без побочного эффекта, то есть когда не изменяется никакая переменная программы. Например, следующие выражения вычисляются в void-контексте:

$n; "string";

Этот контекст можно обнаружить, если установить ключ -w компилятора perl. Тогда при вычислении предыдущих операторов вы получите предупреждающие сообщения следующего вида:

Useless use of a variable in void context at ex04-01.pl line 2. Useless use of a constant in void context at ex04-01.pl line 3. (Бесполезное использование переменной (константы) в void-контексте 
в строке 2 (3)программы ex04-01.pl)

Если первое из представленных выражений заменить на выражение с побочным эффектом, например $n++;, то первое предупреждение отображаться не будет.

что такое контекст js

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

const car =  brand: 'BMW', model: 'X5', startEngine: function ()  console.log(`Запускаем двигатель $this.brand> $this.model>`); >, >; car.startEngine(); // Запускаем двигатель BMW X5 

В данном примере car — это объект, у которого есть свойства brand и model , а также метод startEngine , который выводит в консоль сообщение о запуске двигателя. Обратите внимание на использование ключевого слова this в методе startEngine .

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

Если мы попытаемся вызвать метод startEngine в контексте другого объекта, например truck , то мы получим другой результат:

const truck =  brand: 'Volvo', model: 'FH16', >; car.startEngine.call(truck); // Запускаем двигатель Volvo FH16 

Здесь мы использовали метод call для изменения контекста выполнения метода startEngine . Метод call позволяет вызвать функцию с указанным контекстом, который в данном случае указывается как объект truck . Теперь, когда мы вызываем startEngine в контексте объекта truck , значения свойств brand и model будут заменены на значения объекта truck , и мы получим другой результат.

Это простой пример, который демонстрирует, как работает контекст в JavaScript. Контекст позволяет функциям и методам иметь доступ к свойствам объектов и другим переменным в текущем исполнении кода. Изменение контекста выполнения функции с помощью методов call , apply и bind может быть полезным в различных сценариях программирования, когда нужно использовать функцию в контексте другого объекта.

Контекстное программирование

Контекстное программирование – широкое понятие, связанное с настройкой программ по автоматическому управлению PPC-рекламы. Одним из результатов применения контекстного программирования является повышение эффективности PPC-рекламы.

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

Контекстное программирование.jpg

Одна из систем управления контекстной рекламой

Контекстное (или в другом варианте контекстно-ориентированное программирование) может использовать различные языки. Но в любом случае оно должно опираться на ряд общих принципов. Эти принципы включают учет поведенческих изменений и контекст этих изменений. Любая информация, которая поддается вычислительным операциям, может образовывать часть контекста. От этого зависят поведенческие отклонения. Поведенческие изменения образуют «слои», которые можно активировать или деактивировать, то есть контролировать в явном виде. Все эти возможности позволяют создавать специальные приложения, включая приложения для управления PPC-рекламой.

PPC-реклама – это один из самых популярных видов онлайн-рекламы. Она применяется в различных поисковых системах, например в Google, Bing и Yahoo. Такая реклама появляется на страницах поисковой выдачи. Рекламодатель платит за клик по его объявлению (PPC – Pay per Click) для рекламы на страницах результатов поиска. Однако для эффективности такой рекламы необходима правильная работа систем контекстной рекламы. Это и позволяет обеспечить контекстное программирование.

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

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

Введение в контекстно-ориентированное программирование на Kotlin

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

Несколько слов о разрешении функций

Как хорошо известно, существует три основных парадигмы программирования (примечание Педанта: есть и другие парадигмы):

  • Процедурное программирование
  • Объектно-ориентированное программирование
  • Функциональное программирование

Объектно-ориентированный стиль программирования ограничивает области видимости функций. Функции не глобальны, вместо этого они являются частью классов, и могут быть вызваны только для экземпляра соответствующего класса (примечание Педанта: некоторые классические процедурные языки имеют модульную систему и, значит, области видимости; процедурный язык != С).

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

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

Функциональный подход не приносит чего-то принципиально нового в плане разрешения функций. Обычно функционально-ориентированные языки имеют лучшие правила для разграничения областей видимости (примечание педанта: еще раз, C — это еще не все процедурные языки, есть такие, в которых области видимости хорошо разграничены), которые позволяют проводить более скрупулезный контроль видимости функций на основе системы модулей, но кроме этого, разрешение проводится во время компиляции основываясь на типе аргументов.

Что такое this?

В случае объектного подхода, при вызове метода у объекта, у нас есть его аргументы, но кроме того мы имеем явный (в случае Python) или неявный параметр, представляющий экземпляр вызываемого класса (здесь и далее все примеры написаны на Kotlin):

class A < fun doSomething()< println("Этот метод вызывается на $this") >>

Вложенные классы и замыкания все несколько усложняют:

interface B < fun doBSomething() >class A< fun doASomething()< val b = object: B< override fun doBSomething()< println("Этот метод вызывается на $this внутри $") > > b.doBSomething() > >

В данном случае есть два неявных this для функции doBSomething — один соответствует экземпляру класса B, а другой возникает от замыкания экземпляра A. То же самое происходит в намного более часто встречающемся случае лямбда-замыкания. Важно отметить, что this в данном случае работает не только как неявный параметр, но и как область или контекст для всех функций и объектов, вызываемых в лексической области определения. Так что метод doBSomething на самом деле имеет доступ к любым, открытым или закрытым, членам класса A, так же как и к членам самого B.

А вот и Kotlin

Kotlin дает нам совершенно новую «игрушку» – функции-расширения. (Примечание Педанта: на самом деле не такие уж новые, в C# они тоже есть). Вы можете определить функцию вроде A.doASomething() где угодно в программе, не только внутри A. Внутри этой функции у нас есть неявный this-параметр, называемый получателем (receiver), указывающий на экземпляр A на котором метод вызывается:

class A fun A.doASomthing() < println("Этот метод-расширение вызывается на $this") >fun main()

У функций-расширений нет доступа к закрытым членам их получателя, так что инкапсуляция не нарушается.

Следующая важная вещь, которая есть в Kotlin — блоки кода с получателями. Можно запустить произвольный блок кода используя что-нибудь в качестве получателя:

class A < fun doInternalSomething()<>> fun A.doASomthing()<> fun main() < val a = A() with(a)< doInternalSomething() doASomthing() >>

В этом примере обе функции можно было вызвать без дополнительного «a.» в начале, потому что функция with помещает весь код последующего блока внутрь контекста a. Это значит, что все функции в этом блоке вызываются так, как если бы они вызывались на (явно переданном) объекте a.

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

class B class A < fun B.doBSomething()<>> fun main() < val a = A() val b = B() with(a)< b.doBSomething() // будет работать >b.doBSomething() // не скомпилируется >

Важно, что здесь B получает некоторое новое поведение, но только когда находится в конкретном лексическом контексте. Функция-расширение является обычным членом класса A. Это значит, что разрешение функции делается статически на основе контекста, в котором она вызывается, но настоящая реализация определяется экземпляром A, передаваемым в качестве контекста. Функция может даже взаимодействовать с состоянием объекта a.

Контекстно-ориентированная диспетчеризация

В начале статьи мы обсудили разные подходы к диспетчеризации вызовов функций, и это было сделано не просто так. Дело в том, что функции-расширения в Kotlin позволяют работать с диспетчеризацией по-новому. Теперь решение о том, какая конкретно функция должна быть использована, основано не только на типе ее параметров, но и на лексическом контексте ее вызова. То есть то же самое выражение в разных контекстах может иметь разное значение. Конечно, с точки зрения реализации ничего не меняется, и у нас по-прежнему есть явный объект-получатель, который определяет диспетчеризацию для своих методов и расширений, описанных в теле самого класса (member extensions) — но с точки зрения синтаксиса, это другой подход.

Давайте рассмотрим, как контекстно-ориентированный подход отличается от классического объектно-ориентированного, на примере классической проблемы арифметических операций над числами в Java. Класс Number в Java и Kotlin является родительским для всех чисел, но в отличие от специализированных чисел вроде Double, он не определяет своих математических операций. Так что нельзя писать, например, так:

 val n: Number = 1.0 n + 1.0 // операция `plus` не определена в классе `Number` 

Причина здесь в том, что невозможно согласованно определить арифметические операции для всех числовых типов. К примеру, деление целых чисел отличается от деления чисел с плавающей точкой. В некоторых особых случаях пользователь знает, какой тип операций нужен, но обычно нет смысла определять такие вещи глобально. Объектно-ориентированным (и, на самом деле, функциональным) решением было бы определить новый тип-наследник класса Number, нужные операции в нем, и использовать его где необходимо (в Kotlin 1.3 можно использовать встраиваемые (inline) классы). Вместо этого, давайте определим контекст с этими операциями и применим его локально:

 interface NumberOperations < operator fun Number.plus(other: Number) : Number operator fun Number.minus(other: Number) : Number operator fun Number.times(other: Number) : Number operator fun Number.div(other: Number) : Number >object DoubleOperations: NumberOperations < override fun Number.plus(other: Number) = this.toDouble() + other.toDouble() override fun Number.minus(other: Number) = this.toDouble() - other.toDouble() override fun Number.times(other: Number) = this.toDouble() * other.toDouble() override fun Number.div(other: Number) = this.toDouble() / other.toDouble() >fun main() < val n1: Number = 1.0 val n2: Number = 2 val res = with(DoubleOperations)< (n1 + n2)/2 >println(res) > 

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

 fun NumberOperations.calculate(n1: Number, n2: Number) = (n1 + n2)/2 val res = DoubleOperations.calculate(n1, n2) 

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

Также стоит помнить, что контексты могут быть вложенными:

with(a) < with(b)< doSomething() >> 

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

Мощь явных корутин (coroutines)

Один из лучших примеров контекстно-ориентированного подхода использован в библиотеке Kotlinx-coroutines. Объяснение идеи можно найти в статье Романа Елизарова. Здесь я только хочу подчеркнуть, что CoroutineScope — это случай контекстно-ориентированного дизайна с контекстом, имеющим состояние. CoroutineScope играет две роли:

  • Он содержит CoroutineContext, который нужен для запуска корутин и наследуется когда запускается новая сопрограмма.
  • Он содержит состояние родительской корутины, позволяющее отменить ее в случае, если порожденная сопрограмма выкидывает ошибку.
suspend fun CoroutineScope.doSomeWork()<> GlobalScope.launch < launch< delay(100) doSomeWork() >> 

Здесь doSomeWork — это контекстная функция, но определенная за пределами ее контекста. Методы launch создают два вложенных контекста, которые эквивалентны лексическим областям соответствующих функций (в данном случае оба контекста имеют один и тот же тип, поэтому внутренний контекст затеняет внешний). Хорошей отправной точкой для изучения сопрограмм в Kotlin является официальное руководство.

DSL

Существует широкий класс задач для Kotlin, на которые обычно ссылаются как на задачи построения DSL (Domain Specific Language). Под DSL при этом понимается некоторый код, обеспечивающий дружественный пользователю построитель (builder) какой-то сложной внутри структуры. На самом деле использование термина DSL здесь не совсем корректно, т.к. в таких случаях просто используется базовый синтаксис Kotlin без каких-либо специальных ухищрений — но давайте все-таки использовать этот распространенный термин.

DSL-построители в большинстве случаев контекстно ориентированы. Например, если вы хотите создать HTML-элемент, надо в первую очередь проверить, можно ли добавлять этот конкретный элемент в данное место. Библиотека kotlinx.html достигает этого предоставлением основанных на контексте расширений классов, представляющих определенный тег. По сути, вся библиотека состоит из контекстных расширений для существующих элементов DOM.

Другой пример — построитель GUI TornadoFX. Весь построитель графа сцены устроен как последовательность вложенных контекст-построителей, где внутренние блоки отвечают за построение детей для внешних блоков или подстройку параметров родителей. Вот пример из официальной документации:

 override val root = gridPane < tabpane < gridpaneConstraints < vhGrow = Priority.ALWAYS >tab("Report", HBox()) < label("Report goes here") >tab("Data", GridPane()) < tableview < items = persons column("ID", Person::idProperty) column("Name", Person::nameProperty) column("Birthday", Person::birthdayProperty) column("Age", Person::ageProperty).cellFormat < if (it < 18) < style = "-fx-background-color:#8b0000; -fx-text-fill:white" text = it.toString() >else < text = it.toString() >> > > > > 

В этом примере лексическая область определяет свой контекст (что логично, т.к. он представляет раздел GUI и его внутреннее устройство), и имеет доступ к родительским контекстам.

Что дальше: множественные получатели

Контекстно-ориентированное программирование дает разработчикам Kotlin множество инструментов и открывает новый способ дизайна архитектуры приложений. Нужно ли нам что-то еще? Вероятно, да.

На данный момент разработка в контекстном подходе ограничена тем фактом, что нужно определять расширения, чтобы получить какое-то ограниченное контекстом поведение класса. Это нормально, когда речь идет о пользовательском классе, но что если мы хотим то же самое для класса из библиотеки? Или если мы хотим создать расширение для уже ограниченного в области поведения (например, добавить какое-то расширение внутрь CoroutineScope)? На данный момент Kotlin не позволяет функциям-расширениям иметь более одного получателя. Но множественные получатели можно было бы добавить в язык, не нарушая обратной совместимости. Возможность использования множественных получателей сейчас обсуждается (KT-10468) и будет оформлена в виде KEEP-запроса (UPD: уже оформлена). Проблема (или, может быть, фишка) вложенных контекстов — в том, что они позволяют покрыть большинство, если не все, варианты использования классов типов (type-classes), другой очень желанной из предложенных фич. Довольно маловероятно, что обе эти фичи будут реализованы в языке одновременно.

Дополнение

Хотим поблагодарить нашего штатного Педанта и любителя Haskell Алексея Худякова за его замечания по тексту статьи и поправки по моему достаточно вольному использованию терминов. Также благодарю Илью Рыженкова за ценные замечания и вычитку английской версии статьи.

  • kotlin
  • jetbrains research
  • программирование

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

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