Permalink
Find file
6a69e76 Jan 29, 2011
234 lines (179 sloc) 10.6 KB
=begin rant
More discussion, and examples of problems, at
* http://www.ruby-forum.com/topic/173380
* http://www.ruby-forum.com/topic/179303
* http://www.ruby-forum.com/topic/192218
* http://www.ruby-forum.com/topic/216873
For me, I absolutely hate all this encoding stuff in ruby 1.9, and I'll try
to explain why here.
* As a programmer, the most important thing for me is to be able to reason
about the code I write. Reasoning tells me whether the code I write is
likely to run, terminate, and give the result I want.
In ruby 1.8, if I write an expression like "s3 = s1 + s2", where s1 and s2
are strings, this is easy because it's a one-dimensional space.
s3 = s1 + s2
-----> -----> ----->
string string string
As long as s1 and s2 are strings, then I know that s3 will be a string,
consisting of the bytes from s1 followed by the bytes from s2. End of
story, move to the next line.
But in ruby 1.9, it becomes a multi-dimensional problem:
s3 = s1 + s2
enc^ enc^ enc^
| | |
| | |
+----> +----> +---->
string string string
The number possibilities now explodes. What are the possible encodings
that s1 might have at this point in the program? What are the possible
encodings that s2 might have at this point? Are they compatible, or will
an exception be raised? What encoding will s3 have going forward in the
next line of the program?
The reasoning is made even harder because the *content* of the strings is
also a dimension in this logic. s1 and s2 might have different encodings
but could still be compatible, depending on whether they are empty or
consist only of 7-bit characters, as well as whether they are tagged with
an ASCII-compatible encoding. The encoding of s3 also depends on all
these factors, including whether s1 is empty or s2 is empty.
Analysing a multi-line program then multiplies this further, as you need
to carry forward this additional state in your head to where it is next
used.
* Now try reasoning about a program which makes uses of strings returned by
library functions (core or third party), where those functions almost
never document what encoding the string will be tagged with. You need to
guess or test what encoding you get, and/or reason that the encoding
actually doesn't matter at this point in the program, because of what you
know about the encodings of other strings it will be combined with.
* Whether or not you can reason about whether your program works, you will
want to test it. 'Unit testing' is generally done by running the code with
some representative inputs, and checking if the output is what you expect.
Again, with 1.8 and the simple line above, this was easy. Give it any two
strings and you will have sufficient test coverage.
With 1.9, there is an explosion of test cases if you want to get proper
coverage: the number of different encodings and string contents
(empty/ascii/non-ascii) you expect to see for s1, multiplied by the same
for s2, plus testing the encoding of the results.
Third-party libraries need this sort of test coverage too, but generally
don't have it.
* Sane programming languages have a well-defined concept of equality.
If a == b then you can be fairly sure that foo(a) will do the same
as foo(b) [note 1]
Strings in ruby 1.9 don't work this way. You can have two strings which
'==' says are the same, and they are the same when you print or inspect
them, but they behave differently when used. For example see
http://www.ruby-forum.com/topic/476119#964668
http://www.ruby-forum.com/topic/658559
Here a user is taking a string returned by Sinatra (which tags it as
ASCII-8BIT) and passing it as a query argument to sqlite3-ruby. The
sqlite3-ruby query fails, even though the string contains only ASCII
characters, but the query works if given the same string tagged US-ASCII
or UTF-8.
You can argue weakly that this is a bug in Sinatra (for not tagging the
string UTF-8, which it could have done from the Content-Type:...encoding
header), or more strongly that this is a bug in sqlite3-ruby; but neither
library documents or tests its encoding-related behaviour, so you're
basically relying on undefined behaviour in your application.
Such problems can be a real nightmare to debug.
[note 1] Floating-point numbers sometimes break this rule. For example,
-0.0 == 0.0, but 1/-0.0 != 1/0.0
* Unless you take explicit steps to avoid it, the behaviour of a program
under ruby 1.9 may vary depending on what system it is run on. That is,
the *same* program running with the exact *same* data and the *same*
version of ruby can behave *differently* on different systems, even
crashing on one where it worked on the other. This is because, by
default, ruby uses values from the environment to set the encodings of
strings read from files.
Even on the same system, your program could work when run from an
interactive shell but fail when run from a daemon such as cron. See
http://www.ruby-forum.com/topic/211476#918885
It's possible to override ruby's policy on this, but it requires
remembering to use some incantations. If you accidentally omit one, the
program may still work on your system but not on someone else's.
* It's ridiculously complicated. string19.rb contains around 200 examples of
behaviour, and could form the basis of a small book. It's a +String+ for
crying out loud! What other language requires you to understand this
level of complexity just to work with strings?! The behaviour is full of
arbitrary rules and inconsistencies (like /abc/ having encoding US-ASCII
whilst "abc" having the source encoding, and some string methods raising
exceptions on invalid encodings and others not)
It's hard to define the aesthetics of "simple" and "clean" design,
but this certainly isn't.
* It's buggy as hell. I found loads of bugs just in the process of
documenting this. To me this two could imply two things:
- even Ruby's creators, who are extremely bright people, don't understand
their own rules sufficiently to implement them properly. In that case,
what chance do the rest of us have?
- very few people are actually using this functionality, in which case,
what's it doing as a core part of the language?
* Of course, it's very hard to categorise something as a "bug" if you don't
know what the intended behaviour is. Almost all the behaviour given in
string19.rb is undocumented. By that I mean: when I look at the
documentation for String#+, I expect at minimum to be told what it
requires for valid input (i.e. under what circumstances it will raise an
exception), and what the properties of the result are.
If Ruby has any ideas of becoming a standardised language, the ISO and
ANSI committees will laugh down the corridor until all this is formally
specified (and what I have written here doesn't come close)
* Even when I explicitly tag an object as "BINARY", Ruby tells me it's
"ASCII-8BIT". This may seem like a minor issue, but it annoys me
intensely to be contradicted by the language like this, when it is
so blatently wrong. All text is data; the converse is *not* true.
* It solves a non-problem: how to write a program which can juggle multiple
string segments all in different encodings simultaneously. How many
programs do you write like that? And if you do, can't you just have
a wrapper object which holds the string and its encoding?
* It's pretty much obsolete, given that the whole world is moving to UTF-8
anyway. All a programming language needs is to let you handle UTF-8 and
binary data, and for non-UTF-8 data you can transcode at the boundary.
For stateful encodings you have to do this anyway.
* It's half-baked. You can't convert between uppercase and lowercase,
and you can't compare strings using UCA. So anyone doing serious
Unicode stuff is still going to need an external library.
* It's ill-conceived. Knowing the encoding is sufficient to pick characters
out of a string, but other operations (such as collation) depend on the
locale. And in any case, the encoding and/or locale information is often
carried out-of-band (think: HTTP; MIME E-mail; ASN1 tags), or within the
string content (think: <?xml charset?>)
* It's too stateful. If someone passes you a string, and you need to make
it compatible with some other string (e.g. to concatenate it), then you
need to force it's encoding. That's impolite to the caller, as you've
mutated the object they passed; furthermore, it won't work at all if they
passed you a frozen string. So to do this properly, you really have to
dup the string you're being passed, which needlessly copies the entire
content.
# ruby 1.8
def append(str)
@buf << str
end
# ruby 1.9
def append(str)
@buf << str.dup.force_encoding("ASCII-8BIT")
end
* The arbitrary rules are constantly changing. For example, the behaviour
of \w and \s in a regexp will be different in the 1.9.2 release.
http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-core/30543
However I am quite possibly alone in my opinion. Whenever this pops up on
ruby-talk, and I speak out against it, there are two or three others who
speak out equally vociferously in favour. They tell me I am doing the
community a disservice by warning people away from 1.9. The remainder are
silent, apart from the occasional comment along the lines of "I wish this
encoding stuff was optional."
I will now try very hard to find something positive to say about all this.
* You can write programs to truncate a string to N characters, e.g.
if str.size > 50
str = str[0,47] + "..."
end
I can only think of one occasion where I've ever had to do this. Maybe
other people do this all the time.
(In any case, this gives you N codepoints, which is not necessarily the
same as N graphemes or "visible characters". Some codepoints are invisible,
and some combinations of codepoints form a single grapheme, so you could
end up splitting a grapheme)
* You can write regular expressions to match against UTF-8 strings. Of
course, ruby 1.8 can do that, by the much simpler approach of tagging the
regexp as UTF-8, rather than every other string object in the system.
* I can see how it might appeal to be able to write programs in non-Roman
scripts. Howver this is rather defeated by the fact that constants
must start with a capital 'A' to 'Z'.
* Erm, that's all I can think of at the moment.
=end