Где пункты в списках

В чем разница между следующими двумя формулами?

cp [] = [[]]
cp (xs:xss) = [x:ys | x <- xs, ys <- cp xss]
----------------------------------------------
cp [] = [[]]
cp (xs:xss) = [x:ys | x <- xs, ys <- yss]
              where yss = cp xss

Пример вывода: cp [[1,2,3],[4,5]] => [[1,4],[1,5],[2,4],[2,5],[3,4],[3,5]]

В соответствии с мышлением функционально с Haskell (стр. 92) вторая версия является "более эффективным определением... [который] гарантирует, что cp xss вычисляется только один раз", хотя автор никогда не объясняет, почему. Я бы подумал, что они эквивалентны.

Ответ 1

Два определения эквивалентны в том смысле, что они обозначают одно и то же значение, конечно.

Оперативно они различаются в режиме совместного использования при оценке по требованию. jcast уже объяснил почему, но я хочу добавить ярлык, который не требует явно обесцвечивания понимания списка. Правило: любое синтаксическое выражение в позиции, где оно может зависеть от переменной x, будет перерасчитываться каждый раз, когда переменная x привязана к значению, даже если выражение фактически не зависит от x.

В вашем случае в первом определении x находится в области видимости, в которой отображается cp xss, поэтому cp xss будет переоцениваться для каждого элемента x xs. Во втором определении cp xss появляется за пределами x, поэтому он будет вычислен только один раз.

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

  • Компилятор не обязан придерживаться оперативной семантики оценки по требованию, только денотационная семантика. Таким образом, он может вычислять вещи в несколько раз (плавающие) или больше раз (плавающие), чем вы ожидали бы, основываясь на приведенном выше правиле.

  • В целом это неправда, что лучше использовать общий доступ. В этом случае, например, это, вероятно, не лучше, потому что размер cp xss растет так же быстро, как объем работы, который он затратил для его вычисления в первую очередь. В этой ситуации стоимость чтения значения из памяти может превышать стоимость пересчета значения (из-за иерархии кеша и GC).

Ответ 2

Ну, наивное обезжиривание было бы:

cp [] = [[]]
cp (xs:xss) = concatMap (\x -> concatMap (\ ys -> [ x:ys ]) (cp xss)) xs
----------------------------------------------
cp [] = [[]]
cp (xs:xss) = let yss = cp xss in concatMap (\x -> concatMap (\ ys -> [ x:ys ]) yss) xs

Как вы можете видеть, в первой версии вызов cp xss находится внутри лямбда. Если оптимизатор не перемещает его, это означает, что он будет переоцениваться каждый раз, когда вызываемая функция \x -> concatMap (\ ys -> [ x:ys ]) (cp xss) вызывается. Сбрасывая это, мы избегаем повторного вычисления.

В то же время у GHC есть пропуск оптимизации для вычисления дорогостоящих вычислений из таких циклов, поэтому он может автоматически преобразовать первую версию во вторую. В вашей книге говорится, что вторая версия "гарантирует", чтобы вычислить значение cp xss только один раз, потому что, если выражение дорогостоящее для вычисления, компиляторы, как правило, очень не решаются встроить его (преобразование второй версии обратно в первую).