Давным-давно, в 2006 году, я, Нил Форд, придумал термин «многоязычное программирование» (polyglot programming) и употребил его у себя в блоге. Сама концепция, обозначаемая этим термином, не нова (по крайней мере, не моложе Unix, а возможно и старше – я просто окрестил ее новым выражением). Тот пост в блоге был посвящен феномену, который некоторые называют «ренессанс языков программирования». Многоязычное программирование сводится к использованию языков, ориентированных на решение конкретных задач, но комбинируемых в одном и том же контексте. В результате получаются более рациональные решения разнообразных проблем. Чтобы вы могли по достоинству оценить эту концепцию, я сделаю экскурс в недавнюю историю механизмов абстрагирования, которые сегодня применяются разработчиками для решения различных задач. А потом я попытаюсь обрисовать, как такие механизмы будут развиваться в будущем.
Исторический экскурс
В течение последних 20 или около того лет подавляющее большинство приложений было написано с применением объектно-ориентированных языков. Одно из основных достоинств объектной ориентации заключается в простоте переиспользования кода. Разумеется, на такое переиспользование работают основные характеристики объектно-ориентированных языков: инкапсуляция, полиморфизм и эффективное абстрагирование. Но разработчикам все равно сложно добиться крупномасштабного переиспользования, имея в распоряжении такие мелкие (атомарные) элементы. Поэтому приходится задействовать два подхода, при которых объектно-ориентированные возможности используются в качестве «кирпичиков». Речь о компонентах и фреймворках. Компоненты исключительно хорошо подходят для визуального переиспользования. Сначала элементы управления VBX, а затем и ActiveX продемонстрировали, что можно создать динамичную экосистему из переиспользуемых компонентов. А вот реализовать невизуальные функции уже сложнее. Разработчики Windows не раз пытались подступиться к невизуальной области, достаточно просто перечислить аббревиатуры связанных с этим проектов: COM, COM+, MTS, BizTalk и т.д. И это только на платформе .NET. Мир Java воспринял многообещающие идеи MTS и далеко зашел по длинной и темной дороге, имя которой – Enterprise Java Beans. Тем не менее, все эти технологии, призванные обеспечить переиспользование «в промышленном масштабе», далеки от совершенства (поэтому, поиски в этой сфере продолжаются). Работа компонентов завязана на определенной физической экологии. Инструменты должны «понимать», как отображаются инспекторы свойств, событийные механизмы, а также как организуется управление жизненным циклом компонентов. В этой экосистеме существует и другая великолепная реализация идеи переиспользования: фреймворки. Оказывается, разработчики то и дело занимаются многоязычным программированием, даже не осознавая этого. Вам приходилось писать программы, которые обращаются к базе данных? А веб-приложения? Вполне возможно – а значит, вы уже знакомы с многоязычным программированием. Фреймворки в наше время стали наиболее популярным механизмом переиспользования. Фреймворк состоит из множества взаимосвязанных классов, разделяющих общий контекст. Наиболее распространенным, пожалуй, является фреймворк .NET, в его состав входит несколько более мелких фреймворков (ADO.NET, ASP.NET и т.д.). За пределами Microsoft существует ряд второстепенных фреймворков, в частности, log4net, nHibernate, iBatis.net, nVelocity и т.д. Конечно, все это давно известные вещи. Если вы .NET-разработчик, то вы настолько долго имеете дело с подобным стилем абстрагирования, что уже и не замечаете это. Но в основе такого механизма абстрагирования лежит идея о том, что может быть один истинный язык, на котором можно решить любые проблемы. Таким «единственно верным» языком во фреймворке .NET является, в зависимости от обстоятельств, C# или Visual Basic. Эти универсальные языки позволяют взаимодействовать с механизмами переиспользования компонентов и фреймворков. Такие методы применимы при работе с многими приложениями, которые приходится писать, но и у них есть свои недостатки. Идея о возможности создания единственного языка программирования, способного решить все (или почти все) проблемы не учитывает того, насколько разнообразны проблемы, которые приходится решать разработчикам. Если дополнять определенный язык программирования специальными фреймворками, то удается справиться со сложными абстракциями. Но, в таком случае, просто маскируется тот факт, что выбранный вами язык плохо подходит для решения стоящей перед вами проблемы. Объектная ориентация – это эффективный и при этом довольно универсальный механизм абстрагирования, поскольку он обеспечивает иерархическую организацию сущностей. Большая часть мира (с точки зрения) разработчика иерархична, либо может быть вплетена в определенную иерархию. Но разработчики снова и снова углубляются в работу со сквозными функциональностями (cross-cutting concern): транзакции, логирование, безопасность и т.д. А такие функции плохо вписываются в существующие иерархии. Эта идея проиллюстрирована на рисунке 1. В результате, возникла концепция аспектно-ориентированной разработки.Истоки аспектно-ориентированного программирования
Эта концепция была впервые описана в 1997 году в статье, написанной Грегором Кичалесом и др. в журнале «Proceedings of the European Conference on Object-Oriented Programming», выпуск 1241. Рис. 1. Для решения некоторых регулярно возникающих проблем требуются сквозные функциональности Для читателей, не знакомых с аспектно-ориентированным программированием, поясню, что аспекты помогают вплетать код в существующие иерархии. Аспекты обладают специфическим синтаксисом (который отличается от синтаксиса как C#, так и Visual Basic). Этот синтаксис описывает срезы (pointcut). Срезы – это точки, в которых можно инжектировать (внедрять) код прямо в скомпилированные артефакты (для этого применяется специальный компилятор). Аспекты позволяют определить весь код логирования в одном месте, а аспектный компилятор инжектирует байт-код в ваш скомпилированный код. Кроме того, здесь добавляется весь код, необходимый для организации логирующих вызовов. Рассмотрим следующий простой класс:public class MyClass {
public int ProcessString(String s,
out string outStr) {
// ...
}
}
Если вы хотите добавить логирующий вызов, определяющий, когда метод начинает выполняться, и когда его выполнение останавливается, то придется вручную запрограммировать эту операцию примерно так:
public int ProcessString(String s,
out string outStr) {
log.debug("entering ProcessString
method");
// ...
log.debug("exiting ProcessString
method");
}
}
Вместо этого можно добавить аспект, позволяющий перехватывать конкретный метод (этот метод вызывает определенный класс).
using DotNetGuru.AspectDNG.MetaAspects;
using DotNetGuru.AspectDNG.Joinpoints;
public class AspectsSample{
[AroundCall("*
MyClass::ProcessString(*)")]
public static object
Interceptor(JoinPoint jp) {
log.debug("entering ProcessString");
object result = jp.Proceed();
log.debug("exiting ProcessString");
return result;
}
}
Согласитесь, это более удобно, чем писать вручную весь логирующий код во всех тех местах, где он нужен. В то же время этот пример показывает, что абстракции базового языка (C# или Visual Basic) позволяют решить не любую проблему.
И это хорошо. Вероятно, мы никогда не сможем создать язык программирования, который подойдет в любой ситуации. Пожалуй, стоит уже бросить такие попытки. Microsoft разработала общеязыковую среду исполнения (CLR), в которой может использоваться несколько языков с общим целевым промежуточным представлением (IL). Почему бы не развить эту возможность, если уже и ранее нам приходилось комбинировать несколько языков при решении общей проблемы?
Многоязычное программирование сегодня
На самом деле разработчики то и дело занимаются многоязычным программированием, даже не осознавая этого. Вам приходилось писать программы, которые обращаются к базе данных? А веб-приложения? Вполне возможно – а значит, вы уже знакомы с многоязычным программированием. Согласитесь, C# + SQL + JavaScript > 1! Но мы делаем такие вещи, даже не задумываясь: это совершенно естественная часть процесса разработки. Почему бы не написать весь код доступа к данным на C# и обойтись вообще без реляционной базы данных? Вообще, можно написать целое приложение в одном неструктурированном файле или в XML-документе (хотя, постойте, ведь XML – это уже другой язык). Но оказывается, что реляционные базы данных – это инструмент, который очень удобно иметь под рукой, так как в них применяется особый механизм абстрагирования, специально заточенный под работу с большими объемами данных. Операции, ориентированные на работу со множествами данных, обладают собственными выгодными свойствами. Поэтому создается специальное программное обеспечение (серверы баз данных), написанное на собственном языке (SQL), приспособленное для выполнения такой рутинной работы. Подобная ситуация возникает и с JavaScript. Он может вызывать полярные чувства, но обойтись без него нельзя, так как он стал своеобразным «лингва-франка» для самых разных браузеров (лингва-франка – смесь языков, обеспечивающая общение представителей разных народов в многоязычном регионе. – прим. пер.). А еще он обладает специальными функциями, позволяющими создавать интерактивные веб-страницы. На самом деле, берусь утверждать, что многоязычные решения – еще более универсальные, чем можно заключить из следующих разделов. Каждый конфигурационный XML-файл – это отдельный микроязык. В каждом таком файле есть определения типа документа (DTD) или схемы, определяющие грамматику такого языка. Просто так получилось, что все эти языки используют общий синтаксис (XML). Например, и английский, и французский язык используют латиницу, но имеют разный грамматический строй и лексический состав. С такой точки зрения сегодняшние среды разработки уже нашпигованы разнообразными языками. Но большинство из этих языков – просто подпорки для ошибочного мнения о том, что максимально эффективное программирование должно осуществляться на каком-то одном универсальном языке, единственно верном и правильном. Довольно часто ситуация обстоит иначе.Многоязычное программирование в реальной жизни
Допустим, нам требуется написать настольное приложение, в котором будет замысловатый многопоточный элемент-планировщик. Такую программу можно написать целиком на C#. Но строгая типизация C#, пожалуй, только помешает вам при создании пользовательского интерфейса. Интерфейс, возможно, лучше написать на VB, где типизация более слабая. Тем не менее, именно планировщик – самая сложная часть работы. На C# сложно написать качественный многопоточный типобезопасный код. Эта проблема характерна не только для C#: написать хороший многопоточный код сложно на любом императивном языке. Но функциональные языки значительно лучше приспособлены для решения таких задач. Императивный язык относится к своеобразной «языковой семье», алгоритмической по своей природе: строки кода выполняются, в основном, сверху вниз, и вы описываете каждую часть операции, которую необходимо выполнить. Обычно в императивных языках разделяемое состояние хранится в переменных. Очевидно, что в наше время наиболее активно используются языки именно такого типа. С другой стороны, функциональные языки моделируются на базе математики. Функции в функциональном языке действуют «по-математически» (на самом деле, в строго функциональном языке можно получить формальные доказательства того, что функция работает правильно). В принципе, в функциональных языках нет изменяемых состояний, а если они и встречаются, то различия между изменяемыми и неизменяемыми состояниями специально подчеркиваются. Когда сам язык подталкивает вас к работе с неизменяемыми состояниями, то становится проще писать многопоточные приложения. Не приходится специально заниматься синхронизацией блоков кода, так как вы не работаете с разделяемыми состояниями, требующими синхронизации. Почему я затронул здесь тему функциональных языков? Потому что существует F# – новоиспеченный член мира .NET. Он появился в исследовательском центре Microsoft как язык, производный от функционального языка OCaml. F# во многом заимствует синтаксис OCaml, а также имеет некоторые дополнительные черты, благодаря которым он хорошо работает в среде CLR. Вы можете вызывать методы CLR, передавать параметры и вообще взаимодействовать с остальным миром .NET из вашего кода на F#. Но написать на функциональном языке целое приложение довольно сложно, и на это есть несколько причин. Во-первых, стандартный стиль разработки на них требует избегать использования переменных с разделяемым состоянием. Сложно писать приложения, выполняющие типичные задачи (например, ввод/вывод), если нельзя изменить значение переменной. Разумеется, в F# существуют свои варианты решения таких задач, но то, что легко написать на C#, будет сложно написать на F#. Но верно и обратное: что сложно написать на C#, легко сделать на F#. И здесь мы подходим ко второй причине, по которой не стоит писать приложения целиком на F#. Для разработчиков, привыкших к императивным языкам, сложно полностью переключиться на функциональные языки. И вот здесь во всей красе проявляется многоязычное программирование. Если взглянуть на мир с точки зрения многоязычного программирования, то мы даже не будем пытаться написать приложение полностью на F#. Напротив, в случае с приведенным выше примером сложного многопоточного планировщика, у вас получится решение из трех проектов, каждый из которых пишется на своем языке. На C# мы напишем ту часть приложения, которая организует поток задач (в терминологии MVC это «контроллер»). Большая часть модели также будет написана на C# (все, кроме самого механизма-планировщика). А вот сложный многопоточный планировщик будет реализован уже на F#. Ведь на этом языке проще писать многопоточный код – именно потому что сам язык оптимизирован под его создание. Наконец, представление (View) будет написано на Visual Basic, так как здесь строгая типизация будет ослаблена, и мы сможем быстрее разработать легкий пользовательский интерфейс приложения.Практическое многоязычное программирование
Итак, наиболее очевидная польза многоязычного программирования заключается в применении языков, лучше приспособленных для решения конкретных задач. Аналогично тому, как сегодня разработчики используют SQL для обработки рутинных задач, связанных с базой данных, так в недалеком будущем могут появиться приложения, отдельные части которых написаны полностью на функциональных языках. В настоящее время как минимум одна крупная фирма на Уолл-Стрит, работающая с электронными финансами, пишет все свои приложения на OCaml. Ее руководство считает, что так они приобретают конкурентное преимущество над аналогичными компаниями, работающими на рынке. Но, на самом деле, они тоже замкнулись на «единственном верном языке» (в их случае это OCaml) и вынуждены расплачиваться за то, что пытаются написать на функциональном языке пользовательские интерфейсы, которые проще писать на императивных языках. Когда разработчики привыкнут писать многоязычные программы, то такие программы будут казаться не менее естественными, чем приложения, взаимодействующие с базой данных. В наши дни одна из основных сложностей, связанных с написанием приложений для работы с базой данных, заключается в пресловутом рассогласовании интерфейсов (impedance mismatch) между объектно-ориентированными языками и языком SQL, ориентированным на работу с множествами. Без малого миллиарды долларов тратятся сегодня на решение этой проблемы, но до сих пор в нашем распоряжении есть, в лучшем случае, посредственные решения. Мой друг Тед Ньюард любит говорить по этому поводу: «Объектно-реляционное отображение в информатике чем-то напоминает вьетнамскую войну. Сначала вы посылаете несколько консультантов, потом других консультантов. И, наконец, внезапно обнаруживаете, что уже давно ведете окопную войну, которой не видно конца!». Эта метафора красиво излагает суть стоящей перед нами проблемы. Последней попыткой справиться с ней стал «Entity Framework» (здесь под «фреймворком» понимается контейнер для переиспользуемого кода). Но сложность объектно-реляционного отображения обусловлена двумя разнородными проблемами. Первая заключается в передаче информации через машинные границы. Для этого приходится работать со специальными форматами (обычно применяется либо адаптер бинарного кода для базы данных, либо XML). Передача информации через машинные границы – всегда трудоемкий и сложный процесс. Но, к счастью, эта проблема уже в основном решена. Вторая проблема заключается в том, что объектно-реляционное отображение концептуально отличается от объектно-ориентированного программирования. Объектно-ориентированные языки работают с иерархиями объектов, а SQL оперирует множествами. Последняя попытка решить эту проблему связана с использованием особой разновидности многоязычного программирования. Речь об особом предметно-ориентированном языке, который называется «LINQ». Он упрощает переход между этими двумя принципиально разными стилями абстрагирования. Наличие общеязыковой среды исполнения частично решает обе эти проблемы. Разработчики языков из Microsoft сумели сгладить многие отличия в абстрагировании, существующие между функциональным языком F# и другими языками CLR. Во многом это удалось благодаря созданию унифицированного кода на промежуточном языке (IL). И здесь же находится другая причина, по которой приходится иметь дело с менее сложной проблемой, чем обработка объектно-реляционного отображения. Многоязычное программирование подразумевает, что весь код компилируется в общее промежуточное представление (например, в IL). А значит, вам не приходится преодолевать машинные границы, и вы можете пользоваться преимуществами разделяемых типов, определяемых в промежуточном языке. Одна из основных проблем, относящихся к многоязычному программированию, заключается в необходимости отладки многоязычных решений. Именно в таких случаях нам пригодится Visual Studio – обычный контейнер для всех .NET-языков. Поскольку вся работа протекает на промежуточном языке, вы можете скомпилировать код – и начинать работу на F#, а заканчивать уже на C# или наоборот. На самом деле, интеллектуальные инструменты способствуют такому стилю разработки (и это – одна из причин, по которым такой стиль разработки не прижился ранее). В распоряжении у современных разработчиков есть многогранные среды, в которых не составляет труда писать код сразу на нескольких языках.Многоязычное программирование и языки предметных областей
Феномен языков предметных областей (они же – предметно-ориентированные языки или DSL) и, в особенности, текстовые языки, в настоящее время активно исследуется. DSL-языки предназначены для решения некоторых из проблем, которые можно решать и средствами многоязычного программирования. Такие методы сводятся к построению абстракций, оптимально приспособленных для решения конкретной проблемы. Тем не менее, языки DSL являются исключительно специализированными, создаются в рамках предметной области на основе конкретных практических случаев. Например, возвращаясь к описанной выше проблеме, можно написать предметно-ориентированный язык, который будет реализовывать функции планировщика в вашем приложении. Ниже приведен пример простого предметно-ориентированного языка, который является внутренним DSL, написанным на базе C# (внутренний DSL – это предметно-ориентированный язык, целиком написанный на базе другого языка). В компании ThoughtWorks нам как-то раз потребовалось написать приложение, которое должно было выдавать подробные характеристики железнодорожных вагонов (в целях тестирования). Сначала мы написали вот такой код:ICar car = new Car();
IMarketingDescription desc =
new MarketingDescription();
desc.Type = "Box";
desc.Subtype = "Insulated";
desc.Length = 50.5;
desc.Ladder = "Yes"
desc.LiningType = Lining.Cork;
desc.Description = desc;
Но бизнес-аналитикам этот код не понравился. Когда мы показали им код, они даже не попытались его читать, поскольку он слишком напоминал язык C#, а вникать в тонкости бизнес-аналитики не пожелали. Итак, мы переписали код следующим образом:
ICar car = Car.describedAs()
.Box
.Insulated
.Includes(Equipment.Ladder)
.Has(Lining.Cork);
Мы не применяли никаких особых магических фреймворков и расширений языка C#. Вместо того, чтобы создавать стандартные свойства, мы просто сделали свойства “Get” , которые вызывали побочные «мутации» и создавали методы для установки значений (каждый из таких методов возвращал this). Добавьте несколько красивых отступов – и у вас получится значительно более удобочитаемый код. Это очень простой пример, но бывают и более сложные случаи. На самом деле, LINQ – это просто внутренний предметно-ориентированный язык, обрабатывающий операции запроса структурированных данных.
Многоязычное программирование и использование DSL – это скорее взаимно-дополняющие, а не антагонистичные подходы. Ничего не мешает вам написать DSL как внутренний предметно-ориентированный язык (то есть, язык, полностью построенный на синтаксисе другого языка), воспользовавшись при этом функциональным языком, например, F#. Достаточно вспомнить хотя бы Scala – еще один функциональный язык, работающий на виртуальной машине Java. В свою очередь, язык .NET. включает ряд таких свойств, которые способствуют созданию DSL внутри языка.
Релоцировались? Теперь вы можете комментировать без верификации аккаунта.