Подтвердить что ты не робот

Статически типизированное метапрограммирование?

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

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

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

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

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

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

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

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

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

В то время как функции более высокого порядка могут выполнять некоторые из этих или objects-with-functions-inside, могут сделать некоторые из них, 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)
    }
}

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

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

4b9b3361

Ответ 1

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

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

  • В scala 2.10 будут развиваться события scala

  • Есть работа в преобразовании источника в исходное (макросы), которое вы ищете: scalamacros.org

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

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

Ответ 2

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

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

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

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

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

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

Ответ 3

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

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

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

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

Ответ 4

Возможно, вас заинтересует системы преобразования исходных программ (PTS).

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

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

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

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

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

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

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

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

Ответ 5

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

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

Обоснование

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

  • Возьмите ориентированный на язык подход к задачам, с которыми сталкивается SparkR, который пытается совместить Spark с R.

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

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

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