Макросы

Пример готового макроса в Julia:

julia> @time 1+1
  0.000009 seconds (130 allocations: 7.719 KB)
2

, он принимает код и оборачивает его в измерение времени выполнения.

Еще пример макроса: r"[a-z]" он создает регулярное выражение.

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

В документации долго и нудно описывается структура выражений, как она представляется в виде AST-дерева, но простые вещи можно делать уже сейчас.

Пример 1.

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

macro s(e::Expr)
  :( s::AbstractString->$e )
end

Здесь видно, как макрос ожидает e типа Expr. Такая штука :(что-то внутри) создает quoted-expression (оковыченное выражение). А внутри него e подставляется так: $e. Это похоже на интерполяцию переменных в строках. Только внутри строк интерполируемая переменная вычисляется в ее значение, а в макросе значением выражения является само выражение, типа Expr, не вычисленное, в том виде, как оно передано в макрос.

Теперь создадим с помощью нашего макроса функцию:

f1 = @s length(s)

, здесь мы вызвали макрос s и передали ему выражение length(s). Он не стал пытаться вычислить длину какой-то там переменной s уже сейчас, а создал новое выражение - функцию s::AbstractString->length(s), а мы ее присвоили переменной f1.
Теперь можно делать так:

f1("asd")
3 # длина строки "asd"

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

@s length(s)

то же самое, что

s::AbstractString->length(s)

Правда, чтобы использовать макрос внутри других выражений, нужно обернуть параметры скобками:

["a","1","b","2"] |> xs->filter( @s(ismatch(r"\d", s)), xs )
2-element Array{String,1}:
 "1"
 "2"

Пример 2.

macro iif(e1, e2, e3)
       :(if $e1
           $e2
        else
           $e3
        end )
end

Макрос принимает три аргумента любого типа. Если первый аргумент вычисляется в true, то макрос выполняет и возвращает результат вычисления второго, иначе - третьего. Аргументы могут быть как выражениями

@iif 1>1 println("1") println("2")
2

так и значаниями

julia> @iif true "yes" "no"
"yes"

Это аналог тернарного оператора e1? e2: e3. С помощью функций такое определить нельзя, потому что аргументы функций сперва вычисляются, а потом передаются в функцию.

Пример 3. Полезный.

Если определить макрос с именем, заканчивающимся на _str, например c_str, то вызывать его можно так: c"что-то".

Создадим макрос, возвращающий нам команду, вызывающую bash -с

macro c_str(s::AbstractString)
        `bash -c """$s"""`
       end

вызов:

c"ls | grep aa"

вернет:

`bash -c 'ls | grep aa'`

Используя макросы, нужно помнить, что они работают на этапе компиляции. Например, в Julia есть два варианта создания регулярных выражений : r"" и Regex(). Если регулярное выражение должно быть создано на основе значения переменной, то нужно использовать Regex().

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

Парсинг выражений

С темой макросов связана тема парсинга выражений.

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

Рассмотрим на примерах работу parse(). Она пробует распарсить строку и возвращает либо выражение Expr . Ниже приведены несколько вариантов (dump раскрывает внутреннюю структуру, того, что получилось). Как, видно, выражение имеет структуру. Вот этой структурой и манипулирут макросы:

julia> "1+1"|>parse|>dump

Expr

head: Symbol call

args: Array{Any}((3,))

1: Symbol +

2: Int64 1

3: Int64 1

typ: Any

Например, у выражения имеется поле head, которое обозначает, с чего начинается или что представляет собой выражение.

Вызов неправильно указанной функции (prin вместо print) не приводит к ошибке - распознается только структура:

julia> "prin(1+1)"|>parse|>dump
Expr
 head: Symbol call
  args: Array{Any}((2,))
   1: Symbol prin
   2: Expr
 head: Symbol call
  args: Array{Any}((3,))
   1: Symbol +
   2: Int64 1
   3: Int64 1
 typ: Any
typ: Any

Анонимная функция, такая простая с виду, раскрывается вот во что:

``julia> "x->x+1"|>parse|>dump Expr head: Symbol -> args: Array{Any}((2,)) 1: Symbol x 2: Expr head: Symbol block args: Array{Any}((2,)) 1: Expr head: Symbol line args: Array{Any}((2,)) 1: Int64 1 2: Symbol none typ: Any 2: Expr head: Symbol call args: Array{Any}((3,)) 1: Symbol + 2: Symbol x 3: Int64 1 typ: Any typ: Any typ: Any


Но если в строке что-то более простое ,например, число, то это - самое "более простое" и возвращается.

```julia> "1"|>parse|>dump
Int64 1

```julia> "a"|>parse|>dump Symbol a


При этом, если `parse()` что-то вернуло, то это гарантированно можно подставлять в `eval()` для вычисления.

```julia> "1"|>parse|>eval
1

Особый случай - в целом синтаксически правильное выражение, но незавершенное:

julia> "x->x+"|>parse|>dump
Expr
 head: Symbol incomplete
  args: Array{Any}((1,))
   1: String "incomplete: premature end of input"
typ: Any

Если выражение нельзя интерпретировать, бросается исключение:

julia> "+++"|>parse|>dump
ERROR: ParseError("\"++\" is not a unary operator")
in #parse#310(::Bool, ::Bool, ::Function, ::String, ::Int64) at ./parse.jl:184
in (::Base.#kw##parse)(::Array{Any,1}, ::Base.#parse, ::String, ::Int64) at ./<missing>:0
in #parse#311(::Bool, ::Function, ::String) at ./parse.jl:194
in |>(::String, ::Base.#parse) at ./operators.jl:350

Как бы мы анализировали что происходит, если бы текст программы проходил сначала через наши руки, и мы бы хотели как-то обрабатывать ошибки парсинга? Вот функция, которая получает строку, производит ее парсинг в блоке try ... catch и, если все нормально - возвращает результат. В случае ошибки парсинга - она бросает ошибку, подставляя в текст сообщения исходную строку. В случае если выражение не полно - ошибка с текстом исходной строки. Функция catch_stacktrace() - отдает стек вызовов.

function myparse( str::AbstractString)         # -- принимаем строковый параметр
 try ex = parse( str)                          # -- пробуем распарсить
  typeof( ex) == Expr &&                       # -- если тип - Expr
   ex.head == :incomplete &&
    error("Incomplete expression '$str'")
  return ex
 catch e error("Bad expression '$str': $e. \n\n$(catch_stacktrace())") end
end

results matching ""

    No results matching ""