Макросы
Пример готового макроса в 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