Hello, all:

I was composing a node for SOPW, and spent the better part of an hour on it, when all of a sudden, I knew the proper answer. So I thought I'd post it anyway, but in Meditations rather than SOPW, just to amuse my fellow monks. The text between the two lines is what I had at the moment I realized what the answer was:


Hello, all--

I'm trying to figure out the best way to tell if a variable is read-only or not. I can do it by using eval and trapping the error after the fact, but that doesn't quite feel like the correct solution. I could dig around in Devel::Peek or constant.pm and see how hard it would be to determine it a priori, but I haven't monkeyed around with XS yet. Or there could simply be a better method that I've not been able to find.

I've tried a couple Google searches, and Super Search as well (various combinations of "read-only", "readonly", "alias", "@_", "Modification of a read-only"). But if there's a good way to determine the readonly status of a variable, I've not stumbled across it.

Note: This question is XY problem related, as I'm simply trying to test a function. My original goal was to write a trim subroutine to trim leading and trailing blanks from a list of strings, as I get tired of typing the same old thing. So I created:

sub trim { for (@_) { s/^\s+//; s/\s+$//; } }

It works nicely, but in my test cases, I get the "Modification of a read-only value attempted" error because the first test case is:

subtest 'trim' => sub { is("leading", trim(" leading"), "trim: leading"); ... more test cases ... };

Since I want to operate on strings in place, it's obviously a brain-dead test case

I know that it's due to aliasing of @_/$_, but that's intentional, as I want trim to operate on strings in place. Ideally, there would be an is_readonly() function that I could use like:

sub trim { for (@_) { next if is_readonly($_); s/^\s+//; s/\s+$//; } }

For now, I simply changed my test case to:

subtest 'trim' => sub { my @test = (" leading", "trailing ", " both "); trim(my $tmp = " leading"); is("leading", $tmp, "trim: leading"); trim($tmp = "trailing "); is("trailing", $tmp, "trim: trailing"); trim($tmp = " both "); is("both", $tmp, "trim: leading & trailing"); trim(@test); my $out = "'".join("', '",@test)."'"; is("'leading', 'trailing', 'both'", $out, "trim: list"); done_testing(); };

So, does anyone have a suggestion for me? I expect that the correct answer is to use eval, but I'm not sure.


The reason it took the better part of an hour is that I wanted to be sure I didn't post too much or too little code, and it takes a little time to refine a question properly.

So, I was writing a (to be commented-out) test for using an in-line literal, when I had the revelation: The behavior I want is for it to die with that error message. Otherwise, I'm using the function incorrectly, and I don't want to hide that from myself. So rather than having my code bugs prominent, I was was heading towards a way to make my code fail silently. D'oh!

Also, if I find myself frequently encountering that error, then I probably ought to make trim not operate in place, but instead simply return a list of values.

So while I'm still (slightly) interested in the answer to the question I posed, I'm not sure that it would be good (for me) to have the is_readonly function, as I might be tempted to misuse it in some similar future situation.

...roboticus

Replies are listed 'Best First'.
Re: Amusing note on my own stupidity (longish)
by Arunbear (Prior) on Jan 12, 2010 at 16:54 UTC
    For scalars, you can use the readonly function from Scalar::Util

    Update: There's even a String::Util module that provides a trim function.

      There's even Scalar::Readonly that allows you to modify readonly variables :)

      #!/usr/bin/perl use Scalar::Readonly 'readonly_off'; sub trim { for (@_) { readonly_off($_); s/^\s+//; s/\s+$//; } } for (' foo ', ' bar ') { trim($_); print ">>$_<<\n"; } __END__ >>foo<< >>bar<<
Re: Amusing note on my own stupidity (longish)
by ikegami (Patriarch) on Jan 12, 2010 at 17:21 UTC
    Sounds like you're asking the wrong question. If you need something that isn't readonly, copy it. You obviously want to have something that's isn't an lvalue when you accept readonly inputs, so you need to copy the inputs that aren't readonly.
Re: Amusing note on my own stupidity (longish)
by moritz (Cardinal) on Jan 14, 2010 at 09:18 UTC

    (I've written this two days ago and only hit the "preview" button, not "create". What a nice coincidence when replying to an amusing note on stupidity :-)

    I don't know the answer either for Perl 5 or Perl 6 (though assigning and check doesn't seem too wrong to me), but in Perl 6 you can use the rw-ness at least as a constraint in multi dispatch (if you think it's a wise idea):

    multi sub trim($str) { # return modified value } multi sub trim($str is rw) { # modify in place }

    (Not implemented yet :/ )

    I also have one comment which is totally unrelated.

    subtest 'trim' => sub { is("leading", trim(" leading"), "trim: leading");

    Test::More says the usage is is($got, $expected, $test_name, so while everything works fine when the passes, you'll get a confusing message when it fails:

    $ perl -we 'use Test::More tests => 1; is "leading", " leading", "trim +: leading"' 1..1 not ok 1 - trim: leading # Failed test 'trim: leading' # at -e line 1. # got: 'leading' # expected: ' leading' # Looks like you failed 1 test of 1.

    Perl 6 - links to (nearly) everything that is Perl 6.