Статически Типизированное Метапрограммирование? - PullRequest
21 голосов
/ 26 ноября 2011

Я думал о том, что я упустил бы при переносе некоторого кода Python на статически типизированный язык, такой как F # или Scala; библиотеки могут быть заменены, краткость сопоставима, но у меня есть много кода на Python, который выглядит следующим образом:

@specialclass
class Thing(object):
    @specialFunc
    def method1(arg1, arg2):
        ...
    @specialFunc
    def method2(arg3, arg4, arg5):
        ...

Там, где декораторы делают огромное количество: заменяя методы вызываемыми объектами на состояние, дополняя класс дополнительными данными и свойствами и т. Д. Хотя Python позволяет динамическое метапрограммирование обезьяньего патча где угодно, когда угодно, кем угодно, я нахожу, что по сути, все мое метапрограммирование выполняется на отдельной «фазе» программы. i.e.:

load/compile .py files
transform using decorators
// maybe transform a few more times using decorators
execute code // no more transformations!

Эти фазы в основном совершенно разные; Я не запускаю никакого кода прикладного уровня в декораторах и не выполняю никаких функций ниндзя replace-class-with-other-class или replace-function-with-other-function в основном коде приложения. Хотя «динамическая» сущность языка говорит о том, что я могу делать это где угодно, я никогда не берусь заменять функции или переопределять классы в основном коде приложения, потому что он очень быстро сходит с ума.

Я, по сути, выполняю одну перекомпиляцию кода перед тем, как начать его выполнение.

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

load/parse application files 
load/compile transformer
transform application files using transformer
compile
execute code

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

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

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

РЕДАКТИРОВАТЬ: Одним из примеров использования будет полностью универсальный декоратор кэширования. В питоне это будет:

cacheDict = {}
def cache(func):
    @functools.wraps(func)
    def wrapped(*args, **kwargs):
        cachekey = hash((args, kwargs))
        if cachekey not in cacheDict.keys():
            cacheDict[cachekey] = func(*args, **kwargs)
        return cacheDict[cachekey]
    return wrapped


@cache
def expensivepurefunction(arg1, arg2):
    # do stuff
    return result

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

public Thingy wrap(Object O){ //this probably won't compile, but you get the idea
    return (params Object[] args) => {
        //check cache
        return InvokeWithReflection(O, args)
    }
}

Но все литья полностью убивают безопасность типа.

РЕДАКТИРОВАТЬ: Это простой пример, где сигнатура функции не меняется. В идеале то, что я ищу, могло бы изменить сигнатуру функции, изменив входные параметры или тип вывода (например, состав функции), сохраняя при этом проверку типа.

Ответы [ 5 ]

10 голосов
/ 26 ноября 2011

Очень интересный вопрос.

Некоторые моменты, касающиеся метапрограммирования в Scala:

  • В Scala 2.10 будут происходить события в Отражение в Scala

  • Существует работа по преобразованию источника в источник (макросы), которую вы ищете: scalamacros.org

  • Java обладает самоанализом (посредствомотражение api), но не допускает самостоятельной модификации.Однако вы можете использовать инструменты для поддержки этого (например, javassist ).Теоретически вы можете использовать эти инструменты в Scala для достижения большего, чем просто интроспекция.

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

5 голосов
/ 27 ноября 2011

Итак, мой вопрос: возможно ли это?

Есть много способов добиться того же эффекта в статически типизированных языках программирования.

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

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

Семейство языков ML (метаязык), включаяСтандарты ML, OCaml и F # были специально разработаны для метапрограммирования.Следовательно, они, как правило, имеют отличную поддержку для лексирования, анализа, переписывания, интерпретации и компиляции.Тем не менее, F # - наиболее удаленный член этого семейства, и ему не хватает зрелых инструментов, которыми могут пользоваться такие языки, как OCaml (например, camlp4, ocamllex, dypgen, menhir и т. Д.).F # имеет частичную реализацию fslex, fsyacc и библиотеки комбинаторов парсеров, вдохновленных Haskell, которая называется FParsec .

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

4 голосов
/ 26 ноября 2011

Не зная почему вы делаете это, трудно понять, является ли такой подход правильным в Scala или F #.Но игнорируя это на данный момент, это, безусловно, возможно достичь в Scala, по крайней мере, хотя и не на уровне языка.

Плагин компилятора предоставляет вам доступ к дереву и позволяет выполнять все виды манипулирования этимtree, все полностью проверено по типу.

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

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

2 голосов
/ 29 ноября 2011

Вас могут заинтересовать системы преобразования программ от источника к источнику (PTS) .

Такие инструменты анализируют исходный код, генерируя AST, а затем позволяют определять произвольные анализы и / или преобразования в коде, в конечном итоге восстанавливая исходный код из модифицированного AST.

Некоторые инструменты обеспечивают синтаксический анализ, построение дерева и навигацию по AST с помощью процедурного интерфейса, например ANTLR . Многие из более современных динамических языков (Python, Scala и т. Д.) Имеют встроенные библиотеки синтаксического анализатора, и даже Java (плагины компилятора) и C # (открытый компилятор) догоняют эту идею.

Но в основном эти инструменты обеспечивают только процедурный доступ к AST. Система с поверхностным синтаксическим переписыванием позволяет вам выразить «если вы видите this , измените его на , что », используя шаблоны с синтаксисом языка (ов), которым манипулируют. К ним относятся Stratego / XT и TXL .

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

Наш инструментарий реинжиниринга программного обеспечения DMS - это PTS, обеспечивающий все перечисленные выше возможности ( Life After Parsing ), за определенную плату при настройке его на ваш конкретный язык или DSL, который мы стараемся улучшить, предоставив эти готовые для основных языков . [DMS обеспечивает явную инфраструктуру для построения / управления таблицами символов, управления и потока данных; это использовалось для реализации этих механизмов для Java 1.8 и полного C ++ 14].

DMS также использовался для определения meta-AOP , инструментов, позволяющих создавать системы AOP для произвольных языков и применять AOP-подобные операции.

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

Вы можете увидеть пример спецификации DSL и манипуляции с правилами переписывания «источник-источник» с синтаксисом поверхности, сохраняющими семантику, в этом примере , который определяет и манипулирует алгеброй и исчислением с использованием DMS. Я отмечаю, что этот пример прост, чтобы сделать его понятным; в частности, на нем не представлено ни одного оборудования для анализа потоков, которое предлагает DMS.

0 голосов
/ 07 июля 2016

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

Мне нужно то же самоесделать R API доступными в мире безопасных типов.Таким образом, мы перенесем богатый научный код из R в (безопасный) мир Scala.

Обоснование

  1. Сделать возможным документирование аспектов API бизнес-предметной области через Specs2 (см. https://etorreborre.github.io/specs2/guide/SPECS2-3.0/org.specs2.guide.UserGuide.html;, сгенерированный из кода Scala).Think Domain Driven Design применяется в обратном направлении.

  2. Используйте языковой подход к задачам, стоящим перед SparkR, который пытается объединить Spark с R.

См. https://spark -summit.org / восток-2015 / функциональность и производительность-улучшение-sparkr-and-его-application / для попыток улучшить то, как это в настоящее время делается в SparkR.См. Также https://github.com/onetapbeyond/renjin-spark-executor для упрощенного способа интеграции.

С точки зрения решения этой проблемы мы могли бы использовать Renjin (интерпретатор на основе Java) в качестве движка времени выполнения, но использовать StrategoXT Metaborg для анализа R и создания строго типизированного Scala.API (как вы описали).

StrategoTX (http://www.metaborg.org/en/latest/) - самая мощная из известных мне платформ разработки DSL. Позволяет объединять / встраивать языки, используя технологию синтаксического анализа, которая позволяет создавать языки (более длинная история).

...