Semantics
Programming
Functional Programming
November 2013 29

«Чем это сделать?»: поиск API — методики и проблемы

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

Этот факт находит отражение и в изменении учебных курсов. Сассман, автор SICP, самого известного курса по программирование, сказал: " инженерное дело в середине 90-ых, а уж тем более в 2000-ых сильно отличается от инженерного дела 80-ых. В 80-ых хорошие программисты проводили много времени в размышлениях, а потом писали немного кода, который работал. Код работал близко к «железу», даже Scheme — все было прозрачно на всех стадиях. Как с резистором, достаточно посмотреть на цветную маркировку, чтобы узнать номинальную мощность, допустимые отклонения, сопротивление и V=IR — это все, что нужно знать. 6.001 был задуман как курс для обучения инженеров тому, как из маленьких кубиков, в которых они досконально разбираются, посредством простых техник составлять сложные конструкции, которые делают то, что от них хотят. Но программирование сейчас далеко не то же самое. Теперь вы ковыряетесь в непонятной или несуществующей документацией для софта, даже неизвестно, кем написанного. Вы должны досконально исследовать библиотеки, чтобы узнать, как они работают, пробовать разные исходные данные и смотреть, как реагирует код. Это в корне иная работа, и для нее требуется иной курс обучения."

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

В статически типизированных о том, что делает функция можно догадаться по ее типу. И наоборот — зная, что должна делать функция, предположить ее тип (сигнатуру). Например, функция извлечения элемента из списка должна иметь тип
[a] -> a
— здесь «a» не определенный в данный момент тип элемента, а "[a]" — тип списка из элементов типа «a». Поисковики по типам есть (для некоторых языков) и сильно упрощают жизнь разработчика — это ocamlfind для OCaml, Hoogle и Hayoo для Haskell, Scalex для Scala.

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

Ted Kaehler, сотрудник Алона Кая (автора языка Smalltalk) предлагает более радикальные методики поиска. Один из них — написать тест для этой функции и запустить его на всех существующих. Звучит пугающе, но лично мне часто приходилось искать метод из Java-библиотеки, вызывая все, что показалось подходящим по названию из Scala REPL. А все, что можно автоматизировать, должно быть автоматизировано. :-).

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

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

Все знают, что онтология — это точная спецификация концептуализации. Но что такое спецификация и концептуализация знают не многое. Я к их числу не отношусь, по тому попробую описать как я это понимаю.

Онтология более или менее формально описывает предметную область — какие в ней бывают объекты и какие между ними бывают отношения. Есть множество языков для онтологий, но наибольшее распространение получил стандарт W3C RDF (для более сложных случаев OWL).

В RDF все задается «тройками» субъект-предикат-объект. F1.R is_a RFunction. «F1.R» есть R-функция. Для описания троек W3C пытался навязать XML, но вовремя одумался и разработал человеческий синтаксис.

RDF часто называют графом, а базы данных, способные его обрабатывать относят к графовым. Основным языком общения с таким базами является SPARQL — достаточно удачный гибрид SQL и Prolog. Типичный запрос представляет их себя шаблон фрагмента графа, который требуется найти. В таком виде не сложно сформулировать достаточно экзотические условия — например «найти функцию из доступной под GPLv3 библиотеки, результат которой можно передать на вход функции с именем createPDF». Это несколько сложнее, чем поиск по шаблону типа, описанный в начале статьи, но гораздо гибче.

Можно хранить более подробную информацию об аргументах функции, которая плохо описывается типами. Например, что у метода executeSelect класса org.w3.banana.SparqlEngine первый аргумент — строка с запросом SPARQL select. Найти такую функцию из всех получающих строковый аргумент без такой аннотации было бы не легко. Кроме поисковика этой информацией мог бы пользоваться верификатор кода (аналог lint из C) и IDE для подсветки синтаксиса.
Хотя иногда подобную информацию умудряются засунуть в тип, но это мало помогает в поиске. Вот как это сделано в OCaml (на уровне компилятора)
# (fun a -> Printf.sprintf a 1 ; a) "%d";;
- : (int -> string, unit, string) format  = <abstr>
Понятно, что что-бы найти функцию подстановки целого в строковый шаблон, ни кто не будет искать функцию с типом
(int -> string, unit, string) format -> int -> string


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

Современные языки очень разные и термины одного могут отображаться на другой нетривиальным образом. Например, в некоторых языках есть понятие «класс типов» — совокупность типов, к которым есть общий интерфейс (в отличие от интерфейсов ООП, эти интерфейсы не являются частью типа, а живут отдельно). Например функция
max :: Ord a => a -> a -> a
ждет два параметра одинакового типа a, возвращает результат того же типа и этот тип должен принадлежать классу Ord (то, что можно сравнивать). То есть ее можно вызвать с двумя целыми и получить целое, или с двумя действительными и получить действительное. А с целым и действительным — нельзя.

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

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

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

Как говорится, в каждой библиотеке сидит DSL и просится наружу. Если с тем, как найти синус в Smalltalk мы более-менее разобрались, то как найти loop в Common Lisp пока совершенно не понятно.

+9
5.3k 22
Comments 4
Top of the day