Почему функция имеет память в REBOL?

В rebol я написал эту очень простую функцию:

make-password: func[Length] [
    chars: "QWERTYUIOPASDFGHJKLZXCVBNM1234567890"
    password: ""
    loop Length [append password (pick chars random Length)]
    password
    ]

Когда я запускаю это несколько раз подряд, все становится действительно запутанным:

loop 5 [print make-password 5]

Дает (например) этот вывод:

  • TWTQW
  • TWTQWWWEWRT
  • ТВТQWWWEWRTQWWTW
  • TWTQWWWEWRTQWWTWQTTQQ
  • TWTQWWWEWRTQWWTWQTTQQTRRTT

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

Я этого не спрашивал!

Я хотел бы получить вывод, подобный следующему:

  • IPS30
  • DQ6BE
  • E70IH
  • XGHBR
  • 7ЛМН5

Как я могу достичь этого результата?


person Caridorc    schedule 19.09.2014    source источник
comment
@Joachim Нет, он ведет себя точно так же.   -  person Caridorc    schedule 19.09.2014


Ответы (3)


Хороший вопрос.

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

Например, из предложения @WiseGenius:

make-password: func[Length] [
    chars: "QWERTYUIOPASDFGHJKLZXCVBNM1234567890"
    password: copy ""
    loop Length [append password (pick chars random Length)]
    password
]

Взгляните на блок, содержащий append password.... Этот блок «отображен» там; как это на самом деле выглядит под капотом:

chars: **pointer to string! 0xSSSSSSS1**
password: copy **pointer to string! 0xSSSSSSS2**
loop Length **pointer to block! 0xBBBBBBBB**
password

Так работают все серии, когда они загружаются интерпретатором. Строки, блоки, двоичные файлы, пути, круглые скобки и т. д. Учитывая, что это «черепахи до конца», если вы будете следовать этому указателю, блок 0xBBBBBBBB внутренне:

append password **pointer to paren! 0xPPPPPPPP**

Одним из результатов этого является то, что на серию можно ссылаться (и, следовательно, «изображать») в нескольких местах:

>> inner: [a]

>> outer: reduce [inner inner]
[[a] [a]]

>> append inner 'b

>> probe outer
[[a b] [a b]]

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

Итак, вы заметили интересное следствие этого с функциями. Рассмотрим эту программу:

foo: func [] [
    data: []
    append data 'something
]

source foo

foo
foo

source foo

Это дает, возможно, неожиданный результат:

foo: func [][
    data: [] 
    append data 'something
]

foo: func [][
    data: [something something] 
    append data 'something
]

Мы вызываем foo пару раз, оказывается, что исходный код функции меняется по мере того, как мы это делаем. В некотором смысле это самомодифицирующийся код.

Если вас это беспокоит, в R3-Alpha есть инструменты для его атаки. Вы можете использовать PROTECT для защиты тел функций от изменений и даже создавать свои собственные альтернативы подпрограммам, таким как FUNC и FUNCTION, которые сделают это за вас. (PFUNC? PFUNCTION?) В Rebol версии 3 вы можете написать:

pfunc: func [spec [block!] body [block!]] [
    make function! protect/deep copy/deep reduce [spec body]
]

foo: pfunc [] [
    data: []
    append data 'something
]

foo

Когда вы запускаете это, вы получаете:

*** ERROR
** Script error: protected value or series - cannot modify
** Where: append foo try do either either either -apply-
** Near: append data 'something

Так что это заставляет вас копировать серию. Это также указывает на то, что FUNC — это просто функция! сама по себе, как и ФУНКЦИЯ. Вы можете сделать свои собственные генераторы.

Это может сломать вам мозг, и вы можете начать кричать, что «это не самый разумный способ писать программы». Или, может быть, вы скажете: «Боже мой, он полон звезд». Реакции могут быть разными. Но это довольно фундаментальный «трюк», который приводит систему в действие и придает ей невероятную гибкость.

(Примечание: ветвь Rebol3 Ren-C принципиально сделала так, чтобы функция тела — и исходные серии в целом — заблокированы по умолчанию. Если вам нужна статическая переменная в функции, вы можете сказать foo: func [x <static> accum (copy "")] [append accum x | return accum], и функция будет накапливать состояние в accum между вызовами.)

Я также предлагаю обратить пристальное внимание на то, что на самом деле происходит при каждом запуске. Пока вы не запустите функцию foo, данные не имеют значения. Что происходит каждый раз, когда мы выполняем функцию, и оценщик видит SET-WORD! за которым следует значение серии, он выполняет присвоение переменной.

data: **pointer to block! 0xBBBBBBBB**

После этого назначения у вас будет две ссылки на существующий блок. Во-первых, это его существование в структуре кода, которая была установлена ​​во время ЗАГРУЗКИ, еще до того, как функция была запущена. Вторая ссылка — это та, которая была сохранена в переменной данных. Именно через эту вторую ссылку вы изменяете эту серию.

Обратите внимание, что данные будут переназначаться каждый раз при запуске функции. Но переназначается на одно и то же значение снова и снова... этот исходный указатель блока! Вот почему вам нужно КОПИРОВАТЬ, если вы хотите получать новый блок при каждом запуске.

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

a: b: c: 10

Это просто оценщик нажимает a: как SET-WORD! символ и говоря: «Хорошо, давайте свяжем переменную a в ее контексте привязки с тем, что производит следующее полное выражение». b: делает то же самое. c: делает то же самое, но достигает терминала из-за целочисленного значения 10... а затем тоже вычисляет значение 10. Таким образом, это выглядит как множественное присваивание.

Так что просто помните, что исходный экземпляр последовательного литерала висит в загруженном исходном коде. Если оценщик когда-нибудь соберется сделать такое УСТАНОВЛЕННОЕ СЛОВО! или SET, он позаимствует указатель на этот литерал в источнике, чтобы вставить его в переменную. Это изменяемая ссылка. Вы (или созданные вами абстракции) можете сделать его неизменным с помощью PROTECT или PROTECT/DEEP, и вы можете сделать его не-ссылочным с помощью COPY или COPY/DEEP.


Примечание по теме

Некоторые утверждают, что вы никогда не должны писать copy []... потому что (а) у вас может появиться привычка забывать писать КОПИЮ, и (б) вы создаете неиспользованную серию каждый раз, когда вы сделай это. Этот «пустой шаблон серии» выделяется, должен быть просканирован сборщиком мусора, и никто никогда его не трогает.

Если вы пишете сделайте блокировку! 10 (или любого другого размера, который вы хотите предварительно выделить блоку), вы избегаете проблемы, сохраняете серию и предлагаете подсказку по размеру.

person HostileFork says dont trust SE    schedule 20.09.2014
comment
Похоже, ваш код не работает: он дает Script Error: function expected body argument of type: block (я использую REBOL 2.7 View). - person Caridorc; 21.09.2014
comment
@Caridorc Rebol3 переопределил значение FUNCTION, чтобы оно стало тем, что Rebol2 назвал FUNCT (то есть в соответствии с Red, а также). Я склоняюсь к функции Rebol3, которая автоматически собирает SET-WORD! элементы в теле как параметры /local, чтобы было более элегантным именем и лучшим поведением по умолчанию для новых пользователей, поэтому я предпочитаю вводить его вместо примитива более низкого уровня FUNC. Но я изменил его на FUNC в своем примере на случай, если этот пример рассматривается как в Rebol2, так и в Rebol3. - person HostileFork says dont trust SE; 21.09.2014
comment
@Caridorc Обратите внимание, что теперь у вас есть баллы, необходимые для доступа к чату Rebol и Red SO - person HostileFork says dont trust SE; 21.09.2014
comment
@Memophenon спрашивает Подсказка о размере всегда меня озадачивала. Есть ли какие-нибудь рекомендации, что выбрать здесь, если вы понятия не имеете об окончательной величине? Я чувствую, что кодирование бессмысленной информации в коде — это BadThing(tm), и я не сторонник непоследовательности. либо. Вот почему я поддержал это предложение. Если вы хотите, вы можете создать небольшой помощник с именем make-block, который не принимает никаких параметров по умолчанию, а затем make-block/capacity с целочисленным уточнением. - person HostileFork says dont trust SE; 17.10.2014

По умолчанию эта запись не копирует значение строки "" в password. Вместо этого он устанавливает password для указания на ту строку, которая находится в блоке тела функции. Поэтому, когда вы выполняете append над password, вы фактически добавляете строку, на которую он указывает, которая находится в блоке тела вашей функции. Фактически вы изменяете часть блока тела функции. Чтобы увидеть, что происходит, вы можете использовать ?? для проверки вашей функции, чтобы посмотреть, что происходит с ней каждый раз, когда вы ее используете:

make-password: func[Length] [
    chars: "QWERTYUIOPASDFGHJKLZXCVBNM1234567890"
    password: ""
    loop Length [append password (pick chars random Length)]
    password
]

loop 5 [
    print make-password 5
    ?? make-password
]

Это должно дать вам что-то вроде:

TWTQW
make-password: func [Length][
    chars: "QWERTYUIOPASDFGHJKLZXCVBNM1234567890"
    password: "TWTQW"
    loop Length [append password (pick chars random Length)]
    password
]
TWTQWWEWRT
make-password: func [Length][
    chars: "QWERTYUIOPASDFGHJKLZXCVBNM1234567890"
    password: "TWTQWWEWRT"
    loop Length [append password (pick chars random Length)]
    password
]
TWTQWWEWRTQWWTW
make-password: func [Length][
    chars: "QWERTYUIOPASDFGHJKLZXCVBNM1234567890"
    password: "TWTQWWEWRTQWWTW"
    loop Length [append password (pick chars random Length)]
    password
]
TWTQWWEWRTQWWTWQTTQQ
make-password: func [Length][
    chars: "QWERTYUIOPASDFGHJKLZXCVBNM1234567890"
    password: "TWTQWWEWRTQWWTWQTTQQ"
    loop Length [append password (pick chars random Length)]
    password
]
TWTQWWEWRTQWWTWQTTQQTRRTT
make-password: func [Length][
    chars: "QWERTYUIOPASDFGHJKLZXCVBNM1234567890"
    password: "TWTQWWEWRTQWWTWQTTQQTRRTT"
    loop Length [append password (pick chars random Length)]
    password
]

Чтобы скопировать строку в password, а не указывать на нее, попробуйте следующее:

make-password: func[Length] [
    chars: "QWERTYUIOPASDFGHJKLZXCVBNM1234567890"
    password: copy ""
    loop Length [append password (pick chars random Length)]
    password
]
person WiseGenius    schedule 19.09.2014
comment
Ваш ответ правильный, но @HostileFork дал более глубокий ответ. - person Caridorc; 21.09.2014
comment
@Caridorc Я надеялся, что он это сделает. - person WiseGenius; 21.09.2014
comment
@Caridorc Рад, что вы нашли это полезным. Я немного расширил его, чтобы прояснить пару других вопросов. - person HostileFork says dont trust SE; 21.09.2014

Не имея достаточной репутации, чтобы комментировать ответ HostileFork, я реагирую таким образом. Речь идет о вашей «Связанной заметке», которая указывает мне на то, о чем я никогда не знал.

«Некоторые спорят» предполагает, что вы не среди них, но, тем не менее, вы заставили меня подумать, что мне лучше написать str: make string! 0 и blk: сделать блокировку! 0 теперь не только внутри functions. Подсказка по размеру меня всегда смущала. Есть ли какие-то рекомендации, что выбрать здесь, если вы понятия не имеете об окончательной величине? (Конечно, не меньше вашего минимального ожидания, а также не больше максимального.)

person Memophenon    schedule 12.10.2014