Почему Dialyzer не находит этот код неверным?

Я создал фрагмент ниже на основе этого урока. Последние две строки (feed_squid(FeederRP) а также feed_red_panda(FeederSquid)) явно нарушают определенные ограничения, но Dialyzer находит их в порядке. Это довольно обидно, потому что это именно тот тип ошибок, который я хочу уловить с помощью инструмента, выполняющего статический анализ.

В уроке есть объяснение:

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

В чем причина такого поведения? Я понимаю, что философия успешной типизации заключается в том, чтобы "никогда не плакать, волк", но в текущем сценарии Dialyzer явно игнорирует намеренно определенные спецификации функций (после того, как он видит, что функции были вызваны правильно ранее). Я понимаю, что код не приводит к сбою во время выполнения. Могу ли я как-то заставить Dialyzer всегда серьезно относиться к спецификациям моей функции? Если нет, есть ли инструмент, который может это сделать?

-module(zoo).
-export([main/0]).

-type red_panda() :: bamboo | birds | eggs | berries.
-type squid() :: sperm_whale.
-type food(A) :: fun(() -> A).

-spec feeder(red_panda) -> food(red_panda());
            (squid) -> food(squid()).
feeder(red_panda) ->
    fun() ->
            element(random:uniform(4), {bamboo, birds, eggs, berries})
    end;
feeder(squid) ->
    fun() -> sperm_whale end.

-spec feed_red_panda(food(red_panda())) -> red_panda().
feed_red_panda(Generator) ->
    Food = Generator(),
    io:format("feeding ~p to the red panda~n", [Food]),
    Food.

-spec feed_squid(food(squid())) -> squid().
feed_squid(Generator) ->
    Food = Generator(),
    io:format("throwing ~p in the squid's aquarium~n", [Food]),
    Food.

main() ->
    %% Random seeding
    <<A:32, B:32, C:32>> = crypto:rand_bytes(12),
    random:seed(A, B, C),
    %% The zoo buys a feeder for both the red panda and squid
    FeederRP = feeder(red_panda),
    FeederSquid = feeder(squid),
    %% Time to feed them!
    feed_squid(FeederSquid),
    feed_red_panda(FeederRP),
    %% This should not be right!
    feed_squid(FeederRP),
    feed_red_panda(FeederSquid).

2 ответа

Немного сворачивая пример, у меня есть две версии:

Первый, который Dialyzer может поймать:

-module(zoo).
-export([main/0]).

-type red_panda_food() :: bamboo.
-type squid_food()     :: sperm_whale.

-spec feed_squid(fun(() -> squid_food())) -> squid_food().
feed_squid(Generator) -> Generator().

main() ->
    %% The zoo buys a feeder for both the red panda and squid
    FeederRP = fun() -> bamboo end,
    FeederSquid = fun() -> sperm_whale end,
    %% CRITICAL POINT %%
    %% This should not be right!
    feed_squid(FeederRP),
    %% Time to feed them!
    feed_squid(FeederSquid)

Тогда тот без предупреждений:

    [...]
    %% CRITICAL POINT %%
    %% Time to feed them!
    feed_squid(FeederSquid)
    %% This should not be right!
    feed_squid(FeederRP).

Диализатор предупреждает о версии, которую он может поймать:

zoo.erl:7: The contract zoo:feed_squid(fun(() -> squid_food())) -> squid_food() cannot be right because the inferred return for feed_squid(FeederRP::fun(() -> 'bamboo')) on line 15 is 'bamboo'
zoo.erl:10: Function main/0 has no local return

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

Для версии, которую он не ловит, Dialyzer предполагает, что feed_squid/1 тип аргумента fun() -> bamboo это супертип fun() -> none() (закрытие, которое приведет к сбою, которое, если не вызывается в течение feed_squid/1, по-прежнему является действительным аргументом). После того, как типы были выведены, Dialyzer не может знать, действительно ли в функции вызывается переданное закрытие или нет.

Диализатор по-прежнему выдает предупреждение, если опция -Woverspecs используется:

zoo.erl:7: Type specification zoo:feed_squid(fun(() -> squid_food())) -> squid_food() is a subtype of the success typing: zoo:feed_squid(fun(() -> 'bamboo' | 'sperm_whale')) -> 'bamboo' | 'sperm_whale'

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

ВНИМАНИЕ: ГЛУБОКАЯ ЭЗОТЕРНАЯ ЧАСТЬ СЛЕДУЕТ!

Причина, по которой ошибка сообщается в первом случае, но не во втором, связана с прогрессом локального уточнения модуля. Изначально функция feed_squid/1 имеет успех набрав (fun() -> any()) -> any(), В первом случае функция feed_squid/1 сначала будет уточнено только с FeederRP и обязательно вернусь bamboo немедленно опровергнуть спецификацию и прекратить дальнейший анализ main/0, Во втором, функция feed_squid/1 сначала будет уточнено только с FeederSquid и обязательно вернусь sperm_whale затем уточняется обоими FeederSquid а также FeederRP и вернуться sperm_whale ИЛИ ЖЕ bamboo, Когда тогда позвонил с FeederRP ожидаемое возвращаемое значение при успешном наборе sperm_whale ИЛИ ЖЕ bamboo, Спецификация тогда обещает, что это будет sperm_whale и Dialyzer принимает это. С другой стороны, аргумент должен быть fun() -> bamboo | sperm_whale с точки зрения успеха, это fun() -> bamboo так что остается только с fun() -> bamboo, Когда это проверено по спецификации (fun() -> sperm_whale Диализатор предполагает, что аргумент может быть fun() -> none(), Если вы никогда не вызываете переданную функцию в течение feed_squid/1 (то, что система типов Dialyzer не хранит в качестве информации), и вы обещаете в спецификации, что вы всегда будете возвращать sperm_whale, Все отлично!

То, что может быть "исправлено", так это то, что система типов может быть расширена, чтобы отмечать, когда замыкание, которое передается в качестве аргумента, фактически используется в вызове, и предупреждать в случаях, когда единственным способом "выжить" некоторая часть вывода типа является быть fun(...) -> none(),

(Обратите внимание, я немного размышляю здесь. Я не читал код диализатора подробно).

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

Впрочем, это не один из таких случаев:)

Проблема двоякая. Тип успеха говорит: "Если эта функция завершается нормально, каков ее тип?". В приведенном выше нашем feed_red_panda/1 функция завершается для любого совпадения аргумента fun (() -> A) для произвольного типа A, Мы могли бы позвонить feed_red_panda(fun erlang:now/0) и это тоже должно работать. Таким образом, наши два вызова функции в main/0 не вызывает проблемы. Они оба заканчиваются.

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

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

Можно представить себе будущий вариант диализатора, который учитывает тот факт, что каждый колл-сайт может обрабатываться индивидуально. Поскольку диализатор также со временем меняется, возможно, он сможет справиться с этой ситуацией в будущем. Но в настоящее время это одна из ошибок, которая проскользнет.

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

Другие вопросы по тегам