Без перебора с возвратом. Часть 2

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

Предположим у вас есть нечто подобное: (*)

 namespace XYZ.DEF
{ 
    public class GHI {} 
} 
namespace QRS.DEF.GHI 
{ 
    public class JKL { } 
} 

... в другом файле ...

 using QRS; 
namespace TUV  
{
    using XYZ;
    namespace ABC
    {
        namespace DEF
        {
            class GHI { }
            class MNO : DEF.GHI.JKL { }
        }
    }
} 

И теперь вам нужно определить базовый тип для MNO.

Без перебора с возвратом мы скажем «Ближайшим контейнером, в котором содержится член DEF, является пространство имен ABS, таким образом, DEF означает ABC.DEF». Таким образом, GHI – это ABC.DEF.GHI. А это значит, что JKL – это ABC.DEF.GHI.JKL, которого не существует, поэтому выдается ошибка. Разработчику придется исправить ошибку, указав имя типа, которое позволит понять, что означает DEF.

А что бы мы получили, если бы использовали перебор с возвратом? Мы бы получили ошибку и начали искать заново. Содержит ли XYZ DEF? Да. Содержит ли он GHI? Да. Содержит ли он JKL? Нет. Ищем снова. Содержит ли QRS DEF.GHI.JKL? Да.

Это работает, но можем ли мы логически понять, что это именно то, что подразумевал пользователь?

Кто, черт возьми, может знать это в подобной ситуации? У нас есть несколько подходящих вариантов, которые оказываются неподходящими позднее. И то, что мы пришли к желаемому ответу, блуждая в темноте, является крайне подозрительным. А может быть существует еще один вариант поиска и именно его имел в виду пользователь. Мы не можем знать этого, не проверив все варианты, и, как уже говорилось, это может потребовать большого объема работы.

В этом случае не корректно выполнять перебор с возвратом несколько раз в попытке найти все худшие варианты на каждом шаге поиска. В этом случае правильнее сказать «приятель, лучшее, что мы смогли найти – это какая-то бессмыслица; дай мне что-нибудь более однозначное, пожалуйста».

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

Но проблема в том, что это требование противоречивое. Компилятор не может одновременно налагать строгие правила, которые позволят четко определить подозрительный код, как некорректный и будет компилировать безумный код с помощью эвристик, определяющих, «что я имел в виду на самом деле», когда вы напишите что-то, что компилятор правильно трактует как нечто неоднозначное или неверное.

Я могу продолжать рассматривать места, где мы могли бы применить поиск назад, но где мы этого не сделали. Например, вывод типов методов, всегда либо выполняется, либо нет; в C# он никогда не выполняет поиск назад (**). Но я думаю, что на этом я закончу. Но я хочу сказать, что в одном месте мы используем поиск назад, это касается разрешения перегрузки во вложенных лямбда-выражениях. Я писал об этом здесь.

(*) Читая это, помните, что в языке C# “namespace XYZ.DEF {}” эквивалентно “namespace XYZ { namespace DEF {} }”

(**) В отличие, скажем, от F#

Оригинал статьи