Strange problem with repeat()

0 views
Skip to first unread message

Christoph Zwerschke

unread,
Apr 26, 2008, 6:35:55 AM4/26/08
to MochiKit
I'm somewhat at a loss with the following.
Why does Test 1 display the newlines, but Test 2 does not?

------

<html><head><title>MochiKit Test</title>
<script src="MochiKit.js" type="text/javascript"></script>
</head><body>
<h2>Test 1</h2><p id="test1"/>
<h2>Test 2</h2><p id="test2"/>
<script type="text/javascript">
text ="This\nis\na\ntest";
text = text.split('\n');
test1 = zip(text, [BR(), BR(), BR(), BR()]);
appendChildNodes("test1", test1);
test2 = zip(text, repeat(BR()));
appendChildNodes("test2", test2);
</script>
</body></html>

------

machineghost

unread,
Apr 26, 2008, 6:19:41 PM4/26/08
to MochiKit
First off, I'm not going to say RTFM or anything, but seriously the
docs are you friend ;-) If you take a look at the documentation for
zip:
"zip(p, q, ...):
Returns an array where the n-th element is an array of the n-th
elements from each of the arrays p, q, ..."

The last words are the most significant: "the arrays p, q,". So zip
needs arrays. But are your two arguments arrays? Let's use the
Firebug plug-in for Firefox (basically the best JS development tool in
existence) to find out. If you don't have Firebug you can use alert
or using the Mochikit logger instead.

// From the Firebug console
>>> "This\nis\na\ntest".split("\n")
["This", "is", "a", "test"]
>>> isArrayLike("This\nis\na\ntest".split("\n"))
true

So far so good, now on to argument #2:
>>> repeat(BR())
HTMLBRElement
>>> repr(repeat(BR()))
"repeat([object HTMLBRElement])"
>>> isArrayLike(repeat(BR()))
false

And you see your problem. But what is this HTMLBRElement thing? It's
an iterator, and we can see how it works by again using Firebug:
>>> keys(repeat(BR()))
["repr", "toString", "next"]

toString and repr we've already seen (both just result in
"repeat([object HTMLBRElement])"), but next() is more interesting:
>>> repeat(BR()).next()
<br>

There's the <br> you've been trying to get at this whole time. So
what's the moral of the story? Well first off, don't use the Iter
library functions with non-Iter functions unless they were
specifically designed to do so; instead, use them with other Iter
library functions like "list":
>>> zip(text, list(repeat(BR(), text.length)));
[["This", br], ["is", br], ["a", br], ["test", br]]

I think appendChildNodes handles flattening that mess for you, but if
not just use the function. Also, note "text.length" in that code;
without it you script will take an infinite loop (list will keep
calling repeat(BR()).next() over and over expecting it to stop, but it
won't because you didn't define a limit).

Hope that helped.

Jeremy

Christoph Zwerschke

unread,
Apr 26, 2008, 7:59:30 PM4/26/08
to MochiKit
machineghost schrieb:

>zip(text, list(repeat(BR(), text.length)));
> [["This", br], ["is", br], ["a", br], ["test", br]]

Right, this works, since you're using an array here. This is basically
the same as my Test 1.

Thanks for pointing out that zip should only be used with arrays, not
with iterators. I did not expect that the docs for zip need to be taken
so literally, and I'm still not sure that this is the problem. For
instance, the following works as expected:

zip(text, repeat("*"));

And if you replace zip with izip (which *is* an Iter library function),
the Test 2 does not work either. As I understand the difference between
zip and izip is that izip returns an iterator instead of an array, but I
would expect of an Iter lib function that it also takes iterators as
input, and the documentation does not claim otherwise here.

The following does not work, either:

list(izip(text, repeat(BR()));

So I'm still puzzled.

> I think appendChildNodes handles flattening that mess for you, but if
> not just use the function.

Yes, as I already wrote, you don't need to flatten since this is done
implicitly. My Test 1 works nicely, without flattening.

-- Christoph

machineghost

unread,
Apr 27, 2008, 5:20:59 PM4/27/08
to MochiKit
My apologies; after checking the code it turns out that zip CAN take
an iterator as an argument (guess the docs need updating ...). It
turns out the problem is with both of our understanding of repeat (I
really should have looked at the code in the first place, but I was
lazy):
repeat: function (elem, /* optional */n) {
var m = MochiKit.Base;
if (typeof(n) == 'undefined') {
return {
repr: function () {
return "repeat(" + m.repr(elem) + ")";
},
toString: m.forwardCall("repr"),
next: function () {
return elem;
}
};
}
... and the rest doesn't matter, because in your example n IS
undefined. So as you can see, all repeat does is give you back an
object that has a method which, when called, returns the original
object you passed it in the first place. Why does this matter?
Consider the following:
>>> var a = zip(text, repeat(BR()));
>>> console.log(a);
[["This", br], ["is", br], ["a", br], ["test", br]]
>>> appendChildNodes("test2", a);

If you actually do that in Firebug, and try clicking on the brs,
you'll see something rather strange: all four BRs, when clicked on,
will take you to the same place in the DOM, and that place is the one
<br> that's actually inside test2, at the end. Which brings up the
question, why is there even one BR there anyway; if zip worked there
should be four, otherwise there should be none.

Imagine if you did this instead:
<div id="test1"/>
<div id="test2"/>
<div id="test3"/>
<script>
var a = P({}, "hi");function(x) { return [x, BR()];}
$("test1").appendChild(a);
$("test2").appendChild(a);
$("test3").appendChild(a);
</script>

Would you get this?
hi
hi
hi
Actually no, because there is only ever one P with the word "hi" in
it, and his name "a". So when you add "a" to test1, he's in test1.
Then you add him to test2; now he's no longer in test1. Add him to
test 3, and he's no longer in test2 or test1. No matter how many
times you append him to the DOM, there's only ever one "a", and you'll
only ever see one "hi" on the screen.

Now let's refactor your code a bit:
var a = BR();
var test2 = zip(text, repeat(a));
appendChildNodes("test1", test2);

And suddenly everything makes sense. All you're doing is sticking "a"
after each peice of text, but since "a" can only live in one place, he
keeps getting moved to the most recent append. The last append
happens to the last word, so the final resting place of "a" is at the
end of test2 (which is where, if you use firebug, you can see that the
one <br> ends up).

So what's the solution then? Make a new BR every time, like so:
var brAdder = function(x) { return [x, BR()]; }
appendChildNodes("test2", map(brAdder, text));

Hope that helps,
Jeremy


P.S. To be honest, the repeat function doesn't seem very useful to me,
unless you want to repeat a primitive; however, I noticed it used a
few times within other Mochikit functions, so I imagine it has a
purpose that isn't about making lots of references to the same
individual HTML element ;-)

Christoph Zwerschke

unread,
Apr 27, 2008, 7:40:48 PM4/27/08
to MochiKit
machineghost schrieb:

> It turns out the problem is with both of our understanding of repeat

Ah, yes! Thanks for your analysis, now everything makes sense.

I could use the excuse that I usually program in Python, not in
Javascript, but in fact repeat() does not behave differently there ;-)

To summarize, the Test2 code based on two subtle misconceptions. First,
the implicit idea that in the expression repeat(BR()) the BR() is either
evaluated on every repetition, or copies of the BR() object are created
on every repetition, which both is of course not the case. Second, the
idea that you don't need to care whether elements you're adding to the
DOM are already used elsewhere ;-)

Your solution

var brAdder = function(x) { return [x, BR()]; }
appendChildNodes("test2", map(brAdder, text));

is nice. Alternatively, the Test2 code be fixed as follows:

appendChildNodes("test2", izip(text, imap(BR, repeat())));

Just to make use of repeat() against these odds...

-- Christoph

Per Cederberg

unread,
Apr 28, 2008, 3:16:34 AM4/28/08
to MochiKit
An alternative would be to call cloneNode() for each DOM node before
appending to the DOM tree. Here is a small untested function to
illustrate:

function appendChildNodeClones(parent, /* ... */) {
var args = flattenArguments(arguments);
for (var i = 1; i < args.length; i++) {
if (args[i] != null && typeof(args[i].cloneNode) == "function") {
parent.appendChild(args[i].cloneNode(true));
} else {
appendChildNodes(parent, args[i]);
}
}
}

/Per

Reply all
Reply to author
Forward
0 new messages