Как я могу написать функцию, которая будет проверять, является ли возвращаемый тип статически доступным для каждого вызываемого метода? - PullRequest
5 голосов
/ 24 сентября 2019

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

1 Ответ

11 голосов
/ 24 сентября 2019

Один из способов сделать это (если ваше тело функции чистое) - это сгенерированная функция .Например, предположим, что рассматриваемая функция была

f(x) = x + (rand(Bool) ? 1.0 : 1)

Вместо этого мы можем написать

_f(x) = x + (rand(Bool) ? 1.0 : 1)
@generated function f(x)
    out_type = Core.Compiler.return_type(_f, Tuple{x})
    if !isconcretetype(out_type)
        error("$f($x) does not infer to a concrete type")
    end
    :(_f(x))
end

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

julia> f(1.0)
2.0

julia> f(1)
ERROR: f(Int64) does not infer to a concrete type
Stacktrace:
 [1] error(::String) at ./error.jl:33
 [2] #s28#4(::Any, ::Any) at ./REPL[5]:4
 [3] (::Core.GeneratedFunctionStub)(::Any, ::Vararg{Any,N} where N) at ./boot.jl:524
 [4] top-level scope at REPL[8]:1

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

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

using MacroTools: splitdef, combinedef

strip_type_asserts(ex::Expr) = ex.head == :(::) ? ex.args[1] : ex
strip_type_asserts(s) = s

macro checked(fdef)
    d = splitdef(fdef)

    f = d[:name]
    args = d[:args]
    whereparams = d[:whereparams]

    d[:name] = gensym()
    shadow_fdef = combinedef(d)

    args_stripped = strip_type_asserts.(args)

    quote
        $shadow_fdef
        @generated function $f($(args...)) where {$(whereparams...)}
            d = $d
            T = Tuple{$(args_stripped...)}
            shadowf = $(d[:name])
            out_type = Core.Compiler.return_type(shadowf, T)
            sig = collect(T.parameters)
            if !isconcretetype(out_type)
                f = $f
                sig = reduce(*, (", $U" for U in T.parameters[2:end]), init="$(T.parameters[1])")
                error("$f($(sig...)) does not infer to a concrete type")
            end
            args = $args
            #Core.println("statically inferred return type was $out_type")
            :($(shadowf)($(args...)))
        end
    end |> esc
end

Теперьв ответе мы просто должны аннотировать определение функции с помощью @checked:

julia> @checked g(x, y) = x + (rand(Bool) ? 1.0 : 1)*y
f (generic function with 2 methods)

julia> g(1, 2.0)
3.0

julia> g(1, 2)
ERROR: g(Int64, Int64) does not infer to a concrete type
Stacktrace:
 [1] error(::String) at ./error.jl:33
 [2] #s28#5(::Any, ::Any, ::Any) at ./REPL[11]:22
 [3] (::Core.GeneratedFunctionStub)(::Any, ::Vararg{Any,N} where N) at ./boot.jl:524
 [4] top-level scope at REPL[14]:1

Редактировать: В комментариях было указано, что я нарушаю одно из«правила» использования сгенерированных функций здесь, потому что то, что происходит во время компиляции в сгенерированной функции, может быть автоматически отменено, если кто-то переопределит функцию, на которую опирается функция @checked.Например:

julia> g(x) = x + 1;

julia> @checked f(x) = g(x) + 1;

julia> f(1) # shouldn't error
3

julia> g(x) = rand(Bool) ? 1.0 : 1
g (generic function with 1 method)

julia> f(1) # Should error but doesn't!!!
2.0

julia> f(1)
2

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

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

...