Странное поведение страстного желания - PullRequest
6 голосов
/ 14 января 2020

Естественно ленивый

D:\>6e "my @bar = 'a', 'b', 'c'; sub foo( @b ) { my $bar = 0; gather loop { print "*"; take ($bar, @b[$bar]); $bar++; last if $bar > 2; } }; .print for foo( @bar )"
*(0 a)*(1 b)*(2 c)

Пока что так и ожидалось. А теперь давайте будем нетерпеливы.

D:\>6e "my @bar = 'a', 'b', 'c'; sub foo( @b ) { my $bar = 0; eager gather loop { print "*"; take ($bar, @b[$bar]); $bar++; last if $bar > 2; } }; .print for foo( @bar )"
***(3 a)(3 b)(3 c)

С какой стати цифры c имеют все значения "3"? Как будто, я не знаю, закрытие исчезло в пух иллоги c. take-rw тоже не режет.

D:\>6e "my @bar = 'a', 'b', 'c'; sub foo( @b ) { my $bar = 0; eager gather loop { print "*"; take-rw ($bar, @b[$bar]); $bar++; last if $bar > 2; } }; .print for foo( @bar )"
***(3 a)(3 b)(3 c)

Я могу смягчить

D:\>6e "my @bar = 'a', 'b', 'c'; sub foo( @b ) { my $bar = 0; eager gather loop { print "*"; take-rw ($bar + 0, @b[$bar]); $bar++; last if $bar > 2; } }; .print for foo( @bar )"
***(0 a)(1 b)(2 c)

Но зачем мне это нужно?

Редактировать: Итак, вопросы сводятся к,

>6e "my $bar = 0; .say for gather { take ($bar,); $bar++; take ($bar,) }"
(0)
(1)

>6e "my $bar = 0; .say for eager gather { take ($bar,); $bar++; take ($bar,) }"
(1)
(1)

До сих пор не объяснено, почему существует разница в поведении в eager против lazy случае.

Ответы [ 3 ]

6 голосов
/ 14 января 2020

Холли принял ответ, который процитировал два комментария Брэда. Я сознательно сохранил это вкратце.

Но вы читаете это. 1 Так что я буду go в обратном направлении с этим ответом.

($bar,...) это список, содержащий $bar, а не $bar значение

Поведение, показанное в вопросе, действительно касается контейнеров и значений и того, как литералы списка хранят контейнеры, а не их значения 1 :

my $bar = 0;
my $list = ($bar,);
say $list[0];       # 0
$bar = 1;
say $list[0];       # 1

Лень против нетерпеливого аспекта - это просто вещи, которые делают то, что они все должны делать. Подчеркивая этот первый пункт может быть достаточно, чтобы вдохновить вас сосредоточиться на контейнерах против ценностей. И, возможно, это поможет вам быстро понять, что пошло не так в коде Холли. Но, возможно, нет. 1 Так что я продолжу.

Что происходит в ленивом случае?

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

В коде Холли требования for l oop значения.

При первом обращении к for l oop требуется значение из выражения lazy. Это оборачивается и требует значения из выражения gather 'd. Последний затем вычисляет до take, к этому времени он создает список, первым элементом которого является контейнер $bar. Этот список является результатом take.

Затем .print печатает этот первый список. На момент печати $bar все еще содержит 0. (Первое увеличение $bar еще не произошло.)

Во второй раз вокруг for l oop внутренняя структура управления, включающая take (loop), имеет вид вновь вошел. Первое, что происходит, это то, что $bar увеличивается впервые. Затем проверяется (и не выполняется) условие выхода l oop, поэтому начинается второй раз вокруг l oop. Еще один список создан. Тогда это take d.

Когда печатается второй список, его первый элемент, который является контейнером $bar, печатается как 1, а не 0, потому что в этот момент, будучи увеличенный, $bar теперь содержит 1.

(Если Холли написал код, который удерживал первый список, и напечатал этот первый список снова сейчас , только что напечатав второй список, они обнаружили бы, что первый список также теперь печатается с 1, больше не 0. Потому что все take d списки имеют такой же $bar контейнер как их первый элемент.)

И также третий список.

После того, как третий список был напечатан, for l oop требует четвертого go при gather. Это снова вводит loop в операторе после оператора take. $bar увеличивается в третий раз до 3, а затем срабатывает условие last if $bar > 2;, выходящее из l oop (и, таким образом, выражение является gather 'd и, в конечном итоге, весь оператор .print for ...) .

Что происходит в нетерпеливом случае?

Все выполнение gather завершено до любого ввода print.

В конце этого, конструкция for имеет последовательность из трех списков. Он еще не звонил .print. В третий раз вокруг loop в gather осталось $bar, содержащее 3.

Далее, .print вызывается в каждом из трех списков. $bar содержит 3, поэтому все они печатаются с 3 в качестве первого элемента.

Решение проблемы путем переключения на массив

Я думаю, что идиоматический c способ решения с этим можно переключиться с литерала списка на литерал массива:

[$bar, @bar[$bar]]
# instead of
($bar, @bar[$bar])

Это работает, потому что, в отличие от литерала list , литерал массива обрабатывает элемент, который является контейнером как r-значение 2 , т.е. он копирует значение , содержащееся в контейнере из этого контейнера, вместо хранения самого контейнера.

Просто так получается, что значение копируется в другой новый Scalar контейнер. (Это потому, что все элементы новых несобственных массивов являются fre sh Scalar контейнерами; это одна из главных вещей, которая отличает массив от списка.) Но эффект в этом контексте так же, как если бы значение было скопировано непосредственно в массив, потому что больше не имеет значения, что значение, содержащееся в $bar, изменяется по мере продолжения.

В результате получается, что первый элемент из трех массивов в итоге содержат, соответственно, 0, 1 и 2, три значения, которые содержались в $bar на момент создания каждого массива.

Решение проблемы переключившись на выражение

Как заметил Холли, написание $bar + 0 также сработало.

Фактически подойдет любое выражение, если оно не просто $bar сам по себе.

Конечно, выражение должно работать и возвращать правильное значение. Я думаю, что $bar.self должен работать и возвращать правильное значение независимо от того, какое значение $bar связано или назначено.

(Хотя это действительно немного странно читается; $bar.self это не $bar само по себе, если $bar привязано к контейнеру Scalar! Действительно, в еще более нелогичном повороте, даже $bar.VAR, который использует .VAR, метод, который "возвращает базовый элемент Scalar объект, если таковой имеется. " все равно заканчивается обработкой r-значением ! * *

Нужно ли обновлять do c?

Вышеприведенное является вполне логичным следствием:

  • Что такое Scalar с;

  • Что литералы списка делают с Scalar s;

  • Что означает ленивая против энергичной обработки.

Если do c слабый, то, вероятно, в его объяснении одного из последних двух аспектов. Похоже, что это в первую очередь литеральный список.

Синтаксис do c page содержит раздел о различных литералах, включая литералы Array, но не список литералов. do c * Списки, последовательности и массивы имеет раздел Список литералов (а не один в массивах) но здесь не упоминается, что они делают с Scalar с.

Предположительно, это заслуживает внимания.

Страница Списки, последовательности и массивы также содержит Ленивые списки раздел, который, возможно, может быть обновлен.

Собрав все вышеперечисленное, похоже, простейшим исправлением do c может быть обновление Lists, последовательности и массивы стр.

Сноски

1 В моей первой паре версий этого ответа ( 1 , 2 , я пытался заставить Холли задуматься о влиянии контейнеров на значения. Но это не помогло им и, возможно, не сработало и для вас. Если вы не знакомы с контейнерами Раку, подумайте о прочтении:

  • Контейнеры , официальный представитель c "Низкоуровневое объяснение контейнеров Raku".

2 Некоторые детали обсуждения в Википедии «значений l и r» не соответствуют Раку, но общий принцип тот же.

2 голосов
/ 15 января 2020

В Raku большинство значений являются неизменяемыми.

my $v := 2;
$v = 3;
Cannot assign to an immutable value

Чтобы сделать переменные, вы знаете, на самом деле переменные, есть вещь, называемая Скаляр .

По умолчанию $ переменные фактически связаны с новым Скалярным контейнером.
(Это имеет смысл, поскольку они называются скалярными переменными.)

my $v;
say $v.VAR.^name; # «Scalar»

# this doesn't have a container:
my $b := 3;
say $v.VAR.^name; # «Int»

Вы можно обойти этот скалярный контейнер.

my $v;

my $alias := $v;
$alias = 3;

say $v; # «3»
my $v;

sub foo ( $alias is rw ){ $alias = 3 }

foo($v);

say $v; # «3»

Вы можете даже обойти его в списке.

my $v;

my $list = ( $v, );

$list[0] = 3;

say $v; # «3»

Это основа поведение, которое вы видите.


Дело в том, что вы на самом деле не хотите обойти контейнер, вы хотите передать значение в контейнере.

Так что вы делаете это - деконтейнеризуйте его.

Есть несколько вариантов для этого.

$v.self
$v<>
$v[]
$v{}

Эти последние несколько имеют смысл только для Array или Ha sh контейнеров, но они также работают на Scalar контейнерах.

Я бы рекомендовал использовать $v.self или $v<> для decontainerization.

(Технически $v<> - это сокращение от $v{qw<>}, поэтому оно относится к Ha sh контейнерам, но, как представляется, существует консенсус, что $v<> может использоваться в целом для этой цели.)


Когда вы делаете $v + 0, вы создаете новый объект-значение, которого нет в контейнере Scalar .

Поскольку он не находится в контейнере, вместо контейнера передается само значение.


Вы не замечаете, что это происходит в ленивом случае, но это все еще происходит.

Просто вы напечатали текущее значение в контейнере до того, как оно будет изменено из-под вас.

my @bar = 'a', 'b', 'c';
sub foo( @b ) {
  my $bar = 0;
  gather loop {
    print "*";
    take ($bar, @b[$bar]);
    $bar++;
    last if $bar > 2;
  }
};

my \sequence = foo( @bar ).cache; # cache so we can iterate more than once

# there doesn't seem to be a problem the first time through
.print for sequence; # «*0 a*1 b*2 c»

# but on the second time through the problem becomes apparent
.print for sequence; # «3 a3 b3 c»

sequence[0][0] = 42;
.print for sequence; # «42 a42 b42 c»

say sequence[0][0].VAR.name; # «$bar»
# see, we actually have the Scalar container for `$bar`

Вы также заметите это, если будете использовать что-то вроде .rotor в результирующей последовательности.

.put for foo( @bar ).rotor(2 => -1);

# **1 a 1 b
# *2 b 2 c

Это все еще так же лениво, но генератор последовательности должен создать два значения, прежде чем вы напечатаете его в первый раз. Таким образом, вы заканчиваете печать того, что находится в $bar после второго take.
И во втором put вы печатаете то, что было в нем после третьего take.
В частности, обратите внимание, что число связанный с b изменен с 1 на 2.
(я использовал put вместо print, чтобы разделить результаты. Это будет то же самое поведение с print.)


Если вы удалите контейнер, вы получите значение, а не контейнер.
Так что тогда не имеет значения, что происходит с $bar за это время.

my @bar = 'a', 'b', 'c';
sub foo( @b ) {
  my $bar = 0;
  gather loop {
    print "*";
    take ($bar.self, @b[$bar]); # <------
    $bar++;
    last if $bar > 2;
  }
};

my \sequence = foo( @bar ).cache;

.print for sequence; # «*0 a*1 b*2 c»
.print for sequence; # «0 a1 b2 c»

# sequence[0][0] = 42; # Cannot modify an immutable List

# say sequence[0][0].VAR.name; # No such method 'name' for invocant of type 'Int'.


.put for foo( @bar ).rotor(2 => -1);

# **0 a 1 b
# *1 b 2 c
1 голос
/ 15 января 2020

Per @BradGilbert:

Это касается только контейнеров и значений.

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

См. ответы Брэда и Райфа для дальнейшего обсуждения.

...