I am a Strange Loop by Douglas Hofstadter isn't GEB II, as the author ably explains in the preface. While GEB touched on many topics that apply to Hofstadter's perspective on intelligence and illustrated those topics with Dialogues, this book has tighter focus on consciousness and strange loops. It has a couple Dialogues, much fewer visuals, and mostly photographs in place of artwork (with some great color pictures in the middle). Escher and Bach have fewer appearances. But Gödel and his Theorem are still prominent, which is fitting given the applicability to strange loops. The writing tone remains frank and conversational and it brims with extended analogies, in my opinion too many. I particularly dislike the analogies whose length is excessive, or whose connection to reality is threadbare, or whose wordplay is nauseatingly cute (Hofstadter is remarkably bold here and in GEB--I wouldn't have the chutzpah to mention the coincidental similarity between "Gödel" and "God"). It also felt repetitive to me in its insistence on restating the same conclusion in several ways, but I respect the effort to thoroughly explain rich yet abstract notions. For intangibles, especially directed at a general audience, too verbose is probably better than too terse. At any rate, I believe that I understood what I read, which counts as praise to any nonfiction author. Also praiseworthy is the undeniable pleasure I experienced through the reading. I hope it's not gratuitous to express my gratitude for the gratification, and I hope that it gratifies the one responsible for the gracious gratuity of an additional book beyond GEB.
But like my other "reviews" I'm less interested in critiquing style than musing on ideas. The book's title is apt, since the topic is the big questions about consciousness and the primary answer is strange loops. A strange loop occurs when multiple "levels" exist but the levels mutually intertwine such that repeatedly jumping "outwards" from level to level results in a return to the starting level. All the "level-jumping" in a strange loop appears to be paradoxical until viewed in a larger context, where the strange loop is clearly seen to in actuality be a circular entity (a loop) dependent on outside support for its creation and/or maintenance. An unofficial strange loop that I thought of is the (childish) economic question "where does a factory worker's money come from?". Well, the worker receives money from the level of a manufacturer, the manufacturer receives money from the level of a distributor, the distributor receives money from the level of a retailer, the retailer receives money from the level of a customer such as a plumber, and perhaps the plumber received payment from the level of the factory worker...
Gödel-like self-reference is a more subtle example because it consists of jumping between levels of meaning. To not be patently meaningless, a "self-referential" statement must jump to a new level of meaning. That is, self-referential statements must be considered as meta-statements. Assuming I comprehend him correctly, Hofstadter asserts that consciousness is a similar instance of a self-reference. Consciousness happens when the capability to think and perceive is sufficiently powerful to have its object be thinking and perceiving. The strange loop of consciousness is a thinker who thinks about his or her thinking, thereby "jumping out" to an enclosing level of context/meaning, and then possibly thinks about thinking about his or her thinking, and then possibly thinks about thinking about thinking about his or her thinking (as an aside, I wonder precisely how many meta-levels a book about consciousness has?). One of Hofstadter's objectives is to show that the strange loop of consciousness isn't really infinite, paradoxical, or otherworldly, but it is a natural epi-phenomenon which is a consequence of the symbolic, abstractive power of the human brain attempting to "wrap around" onto itself like how a Gödel theorem wraps a symbolic logic system around onto itself.
This means that consciousness is a highly convincing illusion. I like the comparison of consciousness' reality to the reality of a rainbow. A rainbow appears to be definitely located and as colorfully vivid as a flower, but if the viewer or the light moves or the water stops then the rainbow vanishes. I was a bit relieved to read someone else writing that consciousness is illusory because I've thought the same thing numerous times. Consider how little consciousness/attention a learned or practiced behavior requires. Consider that in split-hemisphere experiments the verbal hemisphere claims to have performed an action that the nonverbal hemisphere just did by itself. Consider the many, many cases when people "unintentionally" express or "betray" their "true" beliefs/feelings. Consider the many, many "gaps" in consciousness that a typical person experiences: sleep, intoxication/sedation, hypnosis (I remember going through a childhood stage of not wanting to fall asleep due to anxiety about the accompanying lack of consciousness). Consider how many of consciousness' conclusions are post-hoc--"I snapped at someone, therefore I must be cranky". Finally, consider the often-abysmal accuracy of people about themselves, the pleasant fiction they cling to--if you doubt whether someone is innocent, just ask him or her, they'll tell you.
To categorize consciousness as illusory is to conflict with dualist perspectives on consciousness. Some of my favorite parts of the book contain patiently logical dismantling of dualism. Its words again mirrored some of the deductions I have reached independently. Belief in objective and physical causality (is there another kind?) implies that a consciousness, a soul, is an unavoidable and convenient mental/social concept rather than a "known unknown" that in some magical way exists separately from reality yet simultaneously impacts it. Before reading this book I've pondered the question about whether unique self-identity is preserved after a perfect clone emerges from a vat or a traveler from a teleporter, and I reasoned that an exact duplicate must exhibit the "same" soul/identity as the original. If no practical method exists to make the distinction between x and y, x and y are for all purposes as identical as 3 and 2.999999999... The same proof applies to distinguishing a machine's or zombie's thoughts from a human's thoughts, which is another question that I encountered before reading this book--if the machine or zombie shows every indication of being as conscious and capable as a human, then it's reasonable to say that the machine or zombie has a claim to a consciousness/soul every bit as much as a human does.
I have more musings to be covered in subsequent parts.
Saturday, July 19, 2008
Friday, July 18, 2008
the Woven cipher
Like the previous other two ciphers, the Woven cipher isn't great at encrypting, but nevertheless it was an engrossing hobby project that had a few fascinating wrinkles (is it any wonder I sometimes have trouble explaining to other people how I spend my time?). The basic idea behind this cipher is to convert all the characters of the plaintext into binary digits (bits), combine the bits of adjacent characters by alternately taking n bits from one then the other, and then iteratively mixing the resulting adjacent bit sequences in the same manner until only one remains. The key is the set of n values to use, which the algorithm cycles. Forming an intact whole out of little parts through an alternating process is analogous to weaving, hence the name "Woven". A second excellent name, perhaps more accurate but less appealing, is "the Multiplexed cipher", where the multiplexing is done using fixed data lengths, as opposed to fixed time periods for instance.
To work, the algorithm can't be quite that simple...
In the encryption and decryption, the role of each bit sequence is filled by a generator. This includes the special-case generators at the start and finish, described in the code as "Initial" or "Final". These generators are somewhat similar to the rest but perform additional conversion tasks and/or obtain data directly from arrays. As a result the program's high-level design is also functional: first it constructs one or more function(s) that will produce the answer (bit by bit) and then it repeatedly calls the function(s). By contrast, the imperative design would be to manipulate actual bit sequences, not generators, by using one set of sequences to create the next set of sequences, etc. The loops are virtually the same in the imperative and functional designs, but the functional equivalent has generators in place of bit sequences--each set of generators is the input for the next set of generators. And each generator (a closure) only knows about the generators that were explicitly placed into its scope at creation.
Encrypt generators merge the output of two encrypt generators: they have two parents and one child. Decrypt generators do the reverse, requesting solely "half" of the output from one source decrypt generator and then dividing it up so that it can return the "left" half to one destination decrypt generator and the "right" half to another destination decrypt generator: they have one (-half?) parent and two children. Encrypt generators act like multiplexers. Decrypt generators act like demultiplexers. The individual generators know and do very little:
My Javascript implementation of the Woven cipher is part of the blog layout, down at the bottom on the right. In the page source, search for 'wovencipher' (one word). If you use it or download it I am not responsible for any awful consequences. I don't advise you to use it for encrypting anything important or unimportant. Legal disclaimer, etc. It's purely for entertainment, educational, and/or edutainment purposes.
To work, the algorithm can't be quite that simple...
- Most of the time, when the numbers don't match up exactly right, a source sequence won't have enough bits. When that happens, the strategy is to instead use an extra 0. Since the "weaving" happens from right to left, least-significant to most-significant, these are leading 0s so the numerical value of the original sequence is unchanged. Of course, the intermediate sequences aren't numbers so the leading 0s matter very much. And if the very last sequence, or each chunk (7 bits long?) that rather long sequence is broken into, must be interpreted as a fully-representative binary numeral, there would need to be an additional convention like always prepending an extra 1. Extra 0s are needed if the sequences are different lengths, like a space's 6 compared to a letter's 7, and/or the sequence isn't evenly divisible by n. These filler 0s cause the encrypted (i.e. transposed?) message to be longer than the original. Unorganized testing shows that the amount of expansion for short sentences is commonly 100% or more, probably an exponential-like effect of extra 0s requiring still more extra 0s in successive iterations.
- Another extremely common complication is an odd quantity of sequences to be woven. The coping technique I eventually settled on was to spontaneously create a "null" or "empty" sequence of length 0 to pair up with the loner. Loner or not, the quantity of data sequences to be woven is half the previous quantity of sequences, rounded up. For instance, 10, 5, 3, 2, 1. Or 8, 4, 2, 1. The rounding--throwing away information-- implies that when decryption happens, the encrypted message and the key aren't enough to reconstruct the original. This is one reason why the third decryption input, the original number of characters, is necessary upfront (the other reason is to let the decryption algorithm know for certain when to quit unraveling sequences).
In the encryption and decryption, the role of each bit sequence is filled by a generator. This includes the special-case generators at the start and finish, described in the code as "Initial" or "Final". These generators are somewhat similar to the rest but perform additional conversion tasks and/or obtain data directly from arrays. As a result the program's high-level design is also functional: first it constructs one or more function(s) that will produce the answer (bit by bit) and then it repeatedly calls the function(s). By contrast, the imperative design would be to manipulate actual bit sequences, not generators, by using one set of sequences to create the next set of sequences, etc. The loops are virtually the same in the imperative and functional designs, but the functional equivalent has generators in place of bit sequences--each set of generators is the input for the next set of generators. And each generator (a closure) only knows about the generators that were explicitly placed into its scope at creation.
Encrypt generators merge the output of two encrypt generators: they have two parents and one child. Decrypt generators do the reverse, requesting solely "half" of the output from one source decrypt generator and then dividing it up so that it can return the "left" half to one destination decrypt generator and the "right" half to another destination decrypt generator: they have one (-half?) parent and two children. Encrypt generators act like multiplexers. Decrypt generators act like demultiplexers. The individual generators know and do very little:
- When an encrypt generator is created, it's passed a 'left' source generator, a 'right' source generator, and the number of bits to read from a generator before switching to the other--this number is one of the digits from the key, so only digits 2-9 are admissible in the key. The encrypt generator keeps a running count of how many bits it has processed, whether it is currently reading from the left or right, and whether either of the generators has run out of bits. If both generators are empty (i.e., return '') then the generator returns '', the empty string. If the generator set to return the next bit isn't empty, then return that bit. If the generator set to return the next bit is empty but the other generator is not empty, return a '0' for filler. Encrypt generators take one 'op' parameter. If the op is 'peek' then the generator skips updating any variables (and it also passes 'peek' if it needs to ask a source generator for the next bit) , otherwise it increments the bit counter and then switches which generator to read if the counter modulo the key value is 0.
- When a decrypt generator is created, it's passed one source generator, which "side" of bits the decrypt generator will always ask for from its source generator (left or right), and the number of bits to process before switching the side that should receive the next bit coming from the source generator--once again, this number is a key digit. Decrypt generators take one parameter, the "side" that is requesting its next bit. The decrypt generator keeps a running count of how many bits it has processed and whether the next bit it reads from the source generator should be returned as a 'left' bit or 'right' bit. It also keeps short queues for left bits and right bits. If it's asked for a bit for a side whose queue isn't empty, then it returns from the queue. Else, it checks which side should receive the source generator's next bit. If the side matches, get a bit and return it. If the side doesn't match, fill up the queue for the opposite side with bits from the source generator, then return the next bit that does apply to the requested side. Naturally, at any time the source generator sends a bit, update the bit counter and toggle the "side" variable if counter modulo n is 0. When the requested side is "all", the decrypt generator does nothing more than return the next bit it receives from its source generator (which is always just from one "side"). The "all" case is purely for the last stage of decryption, when no more "unraveling" is necessary.
My Javascript implementation of the Woven cipher is part of the blog layout, down at the bottom on the right. In the page source, search for 'wovencipher' (one word). If you use it or download it I am not responsible for any awful consequences. I don't advise you to use it for encrypting anything important or unimportant. Legal disclaimer, etc. It's purely for entertainment, educational, and/or edutainment purposes.
Thursday, July 17, 2008
hey genious, you misspelled something there
I know it's just mean to laugh at spelling mistakes. The point of writing is to successfully communicate, and most of the time minor errors don't hamper that. (On the other hand, if it's a setting in which correctness matters--e.g., formal messages or applications for jobs that involve writing them--little oversights might rightly or wrongly communicate subtle secondary information about the incompetence or apathy of the author!) Also, each time I jibe I'm inviting fate to trip me next so as to restore the Universal Balance. Yes, I know there's no such thing, but in case humanity does someday discover it I figure I should be prepared.
However, I draw the line at "genious", because that's just too hilarious and self-referential to pass up. Ha ha ha! I can think of some related quotes: "Judge a pig contest? I don't know, I'm no super-genius... or are I?" by a Mr. Homer Simpson. "Me fail English? That's unpossible." by Ralph Wiggum.
However, I draw the line at "genious", because that's just too hilarious and self-referential to pass up. Ha ha ha! I can think of some related quotes: "Judge a pig contest? I don't know, I'm no super-genius... or are I?" by a Mr. Homer Simpson. "Me fail English? That's unpossible." by Ralph Wiggum.
Tuesday, July 15, 2008
peeve no. 258 is dynlang users who disrespect Perl
Before I get my rant on, I'll do the lawyerly thing and attempt to clarify my terms to avert misunderstanding.
So I know that Perl has its problems. In fact, my encounters with Perl nowadays stem from two sources: 1) ad-hoc administrative tasks and/or data processing (but if I need to interact with one or more of standard formats, databases, JVM APIs then I switch to groovy), 2) upkeep of legacy Perl that acts either as "glue" between systems or as rough internal CGI interfaces for simple yet vital business tasks (I'd also note that the "legacy" Perl has no firm date for replacement). I'm not bothered when language cheerleaders call attention to Perl's weaknesses; this practice is hardly a new phenomenon, and when the latest critic repeats the same years-old tired refrain I can barely manage to react at all. No, what gets me worked up is when someone is dismissive, contemptuous, or mocking toward Perl, which has on numerous occasions, despite its acknowledged icky portions, served my purposes, enabled me to achieve unconventional solutions, and taught me intermediate-to-advanced programming concepts. Without Perl, I might have had to hack together a comparatively ugly combination of grep, find, sed, awk, etc., and shell script. (At the time I'm describing, pretty much the only other programming language available on the system was C or C++, and later Java.)
I'm not too irked by cheerleaders for static languages who throw rocks at Perl for fun. Their derisiveness is a subset of their general antipathy for dynamic languages. If Perl was the epitome of language design perfection then they would still pronounce it junk because of characteristics that by definition apply to any dynamic language: performance overhead, lack of mandatory type enforcement, multiple inheritance, chaotic data structures that are modifiable at will, and so on.
As the title says, the true irritants are dynamic language users who explicitly or implicitly assert that Perl is horrible, terrible. These users know the advantages (and disadvantages) of a dynamic language. They know Perl is a dynamic language. They know that they would probably rather use Perl rather than a static language to solve the same problems (okay, maybe some of them would instead use ML-family, Haskell, or statically-compiled Lisp-family--just maybe). Moreover, the reasons they give for why Perl is abominable sometimes strongly resemble the reasons static language cheerleaders give for avoiding dynamic languages altogether: too many operators and punctuation in general, syntax features that can interact in complicated ways, too few built-in data types (no "official" numbers or strings, just scalars!), the choice to use or ignore OO, special variables that affect execution, accumulated design cruft that makes some tasks too awkward or tricky. The stranger cases are dynamic language users who formerly used Perl primarily, but after changing their habits suddenly stop recognizing anything valuable about Perl at all. Perhaps they used Perl for the general benefits of a dynamic language, all the while having strong distaste for the actual "Perl-ness", and as soon as they could obtain a dynamic language without that flavor they were glad to kick it to the curb. Or (shudder) perhaps some current dynamic language users spent a long time solely in the static language camp, where they grew accustomed to decrying Perl as a mysterious, confounding mess, but after using and liking a dynamic language they continued to denounce Perl more out of habit than out of scorn for dynamism anymore.
Perl is what it is (except when it purposely breaks compatibility in some ways for Perl 6), though it does keep changing and code written using current recommended practices is superior to what you might remember. All I wish is for dynamic language users to give Perl the credit it's earned, is still earning, will earn. Stop mercilessly bashing it as if it ate your parents. But know that I join you in disliking sigils, list/scalar context distinctions, and autovivification.
- "dynlang" is 9 characters shorter than "dynamic language", the name of a fuzzy category of programming languages that allow, support, and exploit the capability to make run-time changes to a program's very structure--its data types, classes/data structures, method/function dispatch. Although program interpretation is quite better suited than compilation to implement such a capability, it's too simplistic to assume that all dynamic languages are necessarily interpreted and all static languages are necessarily compiled, and for that matter too simplistic to assume that a particular language implementation never does both. (Groovy cheerleaders are fond of mentioning that their dynamic language is compiled, while compiled Objective-C code sends object messages at run-time.)
- "Disrespect" refers to an attitude, not to an engineering-like objective evaluation of trade-offs. An example of the latter is "Java's static typing nature can result in code that is difficult to adapt to unanticipated purposes." An example of disrespect is "Java sucks. And one of the reasons it sucks, and the people who choose to use it also suck, is that the stupid types strewn throughout a class do nothing but get in the way when I just want to pass in an instance of MyTempObject, and get the idiotic compiler to stop tying my hands on my behalf". (By the way, I apologize for the inaccuracy. This example of Java disrespect is a bit too lucid and courteous to match the corresponding real statements elsewhere on the Web.)
So I know that Perl has its problems. In fact, my encounters with Perl nowadays stem from two sources: 1) ad-hoc administrative tasks and/or data processing (but if I need to interact with one or more of standard formats, databases, JVM APIs then I switch to groovy), 2) upkeep of legacy Perl that acts either as "glue" between systems or as rough internal CGI interfaces for simple yet vital business tasks (I'd also note that the "legacy" Perl has no firm date for replacement). I'm not bothered when language cheerleaders call attention to Perl's weaknesses; this practice is hardly a new phenomenon, and when the latest critic repeats the same years-old tired refrain I can barely manage to react at all. No, what gets me worked up is when someone is dismissive, contemptuous, or mocking toward Perl, which has on numerous occasions, despite its acknowledged icky portions, served my purposes, enabled me to achieve unconventional solutions, and taught me intermediate-to-advanced programming concepts. Without Perl, I might have had to hack together a comparatively ugly combination of grep, find, sed, awk, etc., and shell script. (At the time I'm describing, pretty much the only other programming language available on the system was C or C++, and later Java.)
I'm not too irked by cheerleaders for static languages who throw rocks at Perl for fun. Their derisiveness is a subset of their general antipathy for dynamic languages. If Perl was the epitome of language design perfection then they would still pronounce it junk because of characteristics that by definition apply to any dynamic language: performance overhead, lack of mandatory type enforcement, multiple inheritance, chaotic data structures that are modifiable at will, and so on.
As the title says, the true irritants are dynamic language users who explicitly or implicitly assert that Perl is horrible, terrible. These users know the advantages (and disadvantages) of a dynamic language. They know Perl is a dynamic language. They know that they would probably rather use Perl rather than a static language to solve the same problems (okay, maybe some of them would instead use ML-family, Haskell, or statically-compiled Lisp-family--just maybe). Moreover, the reasons they give for why Perl is abominable sometimes strongly resemble the reasons static language cheerleaders give for avoiding dynamic languages altogether: too many operators and punctuation in general, syntax features that can interact in complicated ways, too few built-in data types (no "official" numbers or strings, just scalars!), the choice to use or ignore OO, special variables that affect execution, accumulated design cruft that makes some tasks too awkward or tricky. The stranger cases are dynamic language users who formerly used Perl primarily, but after changing their habits suddenly stop recognizing anything valuable about Perl at all. Perhaps they used Perl for the general benefits of a dynamic language, all the while having strong distaste for the actual "Perl-ness", and as soon as they could obtain a dynamic language without that flavor they were glad to kick it to the curb. Or (shudder) perhaps some current dynamic language users spent a long time solely in the static language camp, where they grew accustomed to decrying Perl as a mysterious, confounding mess, but after using and liking a dynamic language they continued to denounce Perl more out of habit than out of scorn for dynamism anymore.
Perl is what it is (except when it purposely breaks compatibility in some ways for Perl 6), though it does keep changing and code written using current recommended practices is superior to what you might remember. All I wish is for dynamic language users to give Perl the credit it's earned, is still earning, will earn. Stop mercilessly bashing it as if it ate your parents. But know that I join you in disliking sigils, list/scalar context distinctions, and autovivification.
Tuesday, July 08, 2008
performance dunderheads check this first...
(Note that even the best people are dunderheaded from time to time.) If an application has been deployed to a new environment but its performance is, say, 8x worse or more, ensure that the relevant database indexes are functioning properly. The hardware may be magnificent and the database well-tuned, but a sequential scan of many records will deliver a gut-punch to performance. It's also obviously not well-suited for caching.
Bonus tip: a good database engine has the ability to show the plan/strategy it computes for a query. Start there to find query optimizations that matter.
Bonus tip: a good database engine has the ability to show the plan/strategy it computes for a query. Start there to find query optimizations that matter.
Monday, June 30, 2008
I am reality pondering reality
Philosophical thought experiments like the Chinese Room can lead to an unnerving conclusion. These experiments argue that an indispensable ingredient of human-like consciousness is semantics. Human thought uses "high-fidelity" representations of reality, not merely symbols. And a mind switches between "raw" (though still contextualized) data and symbols as necessary...while talking, for instance. Being minds, this is natural to us.
If an essential trait of genuine thinking is the affinity of thoughts to reality, then clearly the very capability to think is deeply influenced--informed--by someone's experiences. And if experiences inhabit a strong role as the substances of thoughts, then what the mind does is intimately tied to how reality impinges on it. Thus, whenever a mind thinks, one simplified interpretation is that reality is doing a computation on itself. An analogy is two parts of reality finding each other. Reality co-wrote the concept of tabula rasa.
An alternative statement is that sensations mold neurons but those same neurons perhaps significantly influence future thoughts. This is consistent with the research that has confirmed the benefit of deep and varied education, in the broadest sense, on effective mental function.
If an essential trait of genuine thinking is the affinity of thoughts to reality, then clearly the very capability to think is deeply influenced--informed--by someone's experiences. And if experiences inhabit a strong role as the substances of thoughts, then what the mind does is intimately tied to how reality impinges on it. Thus, whenever a mind thinks, one simplified interpretation is that reality is doing a computation on itself. An analogy is two parts of reality finding each other. Reality co-wrote the concept of tabula rasa.
An alternative statement is that sensations mold neurons but those same neurons perhaps significantly influence future thoughts. This is consistent with the research that has confirmed the benefit of deep and varied education, in the broadest sense, on effective mental function.
Wednesday, June 25, 2008
the Autonomous cipher
The basic outline of a cipher is that it combines a message and a key to produce unreadable text. At some level, then, the key is like a set of instructions that informs the encryption algorithm about what to do with the message. Compare the process to that of assembling an attractive grill. The message is like the parts, the key is like the sheet of directions, and the finished grill is the encrypted message (presuming the directions' language isn't French--"Le grille?...What the hell is that?!"). Why not have the key actually be a set of encryption instructions?
The Autonomous cipher uses this concept. In this cipher, each digit of the key represents two operations (the key "self-directs" the encryption to a large degree, hence the name Autonomous). The operations' instructions assume a "current cipher index" variable, i, of an array of numbers representing the message. For that matter, the message array could be visualized as a long "tape" and i could be visualized as a "read/write head" that shifts position along the tape:
The Autonomous cipher uses this concept. In this cipher, each digit of the key represents two operations (the key "self-directs" the encryption to a large degree, hence the name Autonomous). The operations' instructions assume a "current cipher index" variable, i, of an array of numbers representing the message. For that matter, the message array could be visualized as a long "tape" and i could be visualized as a "read/write head" that shifts position along the tape:
- "0" increases by 3 the array value whose index is i and subtracts 3 from i; corresponding operations of the remaining digits will be displayed as pairs
- "1" is (3,2)
- "2" is (1,2)
- "3" is (3,-1)
- "4" is (1,-3)
- "5" is (2,3)
- "6" is (1,-1)
- "7" is (3,4)
- "8" is (2,-2)
- "9" is (1,4)
- Convert the message to a numerical representation as an array of numbers. Duh.
- Set both i and the reading position of the key (key index) to 0. Perform the following two steps until the resulting value of i is 0.
- Use the key index to determine which instruction to execute, then execute it. This means i and the message array are updated accordingly. When i "falls off" either end of the array, merely wrap it around to the other end.
- Increment the key index. Wrap around to the start of the key if the end is reached.
- Undo the last increment of the key index; it wasn't actually used. Then append this last value of the key index to the end of the message array.
- Any given key may not really work for any given plaintext, for two definitions of "not work": 1) the key starts with a sequence like '18' (i + 2 followed by i - 2) and therefore halts before barely encrypting at all, 2) the key consists of instructions that will jump around but never return to 0 in a reasonable time for the message text. The most extreme example of case 2 is a key that starts with '9' (i + 4) then continues in infinite repetitions of '583' (i + 3, i - 2, i - 1). However, all finite keys will repeat eventually, and with enough repetitions all keys must return to 0. When the net effect of a key is to go from 0 to "x" on a message of length "m", there exist a number of repetitions "r" and a multiple of m "n" such that m*n = r*x. (Any multiple of m works for returning to index 0 because it's identical--congruent--to repeatedly moving from 0 to 0, 0 = (0 + m) % m.) Rearranging terms yields m/x = r/n where m and x are constants. One solution among many to this two-variable equation is r = m and n = x. Stated in practical language, no matter what a finite key does and no matter how long the message is, the index will certainly return to 0 after cycling through the entire key a number of times equal to the message length. For long keys and even longer messages, this absolute upper bound on the number of key repetitions is hardly restrictive. In any case, the key may return to 0 much sooner anyway, of course.
- Although infinite keys may cause problems, any finite key may be arbitrarily extended to produce a longer key, a key that requires more cycles to return to 0. This extension can be accomplished with a couple techniques, but I suppose there are many more possibilities. 1) Replace one or more digit(s) with other digits that have the same net effect. The key thereby will take more iterations to return to 0, as long as none of the "intermediate" steps of the replacement lands on 0. Two '1' or '2' digits (+2 each) can take the place of one '9' digit (+4). A '76' (+4 then -1) can take the place of a '5' (+3). A more complicated replacement is '2' (+2) by '2233' (+2 then +2 then -1 then -1). And each '2' in '2233' can then be replaced by '2233', and so on--but each of these specific replacements only succeed when the value of i before the replacement point, "j", solves none of the equations (j + 4) % arraylength = 0, (j + 3) % arraylength = 0, (j + 2) % arraylength = 0. 2) At some point in the key when i is not 0, i.e not at the start or end, insert another (full) key. By definition, the net effect of the inserted key will be 0. However, as before, the inserted key itself must not incidentally hit 0 as it runs. Assuming the value of i in the enclosing key is "j" at the point before insertion, the inserted key must not ever attain the i index "message length - j" when it runs on its own. For instance, consider two boring keys for a simple message length of 8. Key A is '99' (+4 then +4). It has only one intermediate value, 4. Key B is '2143' (+2 then +2 then -3 then -1). To make B longer by inserting A, A could be inserted before '3' to produce '214993' because one of A's intermediate i values isn't 8 - 1 = 7. A couldn't be inserted before '4' to produce '219943' because (the only) one of its intermediate values is 8 - 4 = 4. The key would end up being '219', one digit smaller than the original! Notice that B could also be inserted into B, '2(2143)143', since B never assigns i the index 8 - 2 = 6.
- Looking at the list of ten operations, some interrelationships stick out. Five decrease i (03468), five increase i (12579). For each particular change in i, one or two operations perform it. -3 has 0 and 4, -2 has 8, -1 has 3 and 6, +2 has 1 and 2, +3 has 5, +4 has 7 and 9. Complementary pairs of digits that undo each other's effects, and so are by definition keys, are (0 or 4) and 5, (1 or 2) and 8. Complementary triples of digits that have a net effect of 0, and so are by definition keys, are (1 or 2) and (3 or 6) and (3 or 6), (3 or 6) and 8 and 5, (7 or 9) and 8 and 8, (7 or 9) and (0 or 4) and (3 or 6). Remember that order doesn't matter in these keys. '385' is a key so '538' is also a key.
- The digits of irrational numbers might not be as promising for keys as one would think. pi is not. '3141', one of the possible four-digit keys, is when pi stops. The square root of 2 isn't much better. It stops at '14142', five digits long. '141' having a net effect of +1 is a bit of a show-stopper.
- A direct crack of a ciphertext, as opposed to indirect methods like brute-force and related-key, begins by looking for clues that help indicate how the ciphertext could have been generated. Metaphorically, the destination ciphertext aids in reconstructing and reversing the encryption journey. But for this cipher, the origin and the destination are the same in one aspect: i's value. The tracing of i hinges on examining the array values; the question is how each final array value differs from what it was before one or more visits from i. Unfortunately, those differences are difficult to compute, because determining the unknown starting value of each array element is precisely the goal of the entire cracking process. Nevertheless, since the starting values are in a known range and each individual increase is in the range 1-3, someone could calculate "ballpark estimates" for the number of "i visits" that possibly resulted in a particular number. The final number of the encrypted message array, an index into the key, also establishes a lower bound for key size. Additionally, a key that repeated before returning to 0 would probably show more telling patterns than a key that didn't. On the other hand, larger keys are more bothersome to work with.
- Alternative bases for the key digits is a possible variation that would enable a greater quantity of symbolized operations. The operations could become as complex as desired, too, although all operations must be exactly reversible, have inverses. A key in one base could be equivalent (isomorphic) to a key in a second base as long as the set of symbolized operations for the keys was equivalent. The key in the larger base would then be a "short version" of the key in the smaller base, similar to how programmers use hexadecimal.
Monday, June 23, 2008
the Pointed cipher
Sometimes a leisure-time project can take on a life of its own. In this case, it started with an inspiration I had. I lay much of the blame on my recent reading. What if someone used a cellular automaton (like Game of Life) to encrypt a message? Specifically, Alice would encrypt by encoding a message as an initial configuration of a cellular automaton (like Game of Life) and using the key to determine how board positions map to characters. My motivation was that a message "becoming alive and growing" into unexpected patterns would be quite hard to crack. Imagine if the information content of a message repeatedly folded over onto itself, thereby exhibiting strange chaotic order sensitive to initial conditions. Let a codebreaker try to analyze that.
The problem, as some readers have already guessed, is that the more interesting examples of automata are just not guaranteed to be reversible. A message's encoded information wouldn't always survive the journey. Two different messages could end up with the same outcome, for instance, or a message could merely evaporate after a short number of iterations. The original signal would be so entangled with the encryption-introduced noise that it wouldn't be salvageable...by anyone. Grudgingly, I adopted the restriction that none of my automata rules would include creation or destruction.
This restriction, along with the need to assign axes to the automata cells for the character mapping, just left me with a set of Cartesian coordinates whose movements somehow affected one another. Then I made several observations: 1) per common practice, the number of dimensions can be set arbitrarily; 2) each character can be encoded as the distance along a particular axis for a particular point; 3) interactions among multiple points at once, especially given the number of points needed to encode a message, are often too impractical to calculate.
After experimentation, I've settled on this impractical encryption algorithm. The idea is to divide up a message into tuples or "points", and perform pairwise-defined substitution and transposition on each point for a number of cycles. Track the values of the three parameters that control the operation of the substitution and/or transposition, and increase them (with wrap-around back to the start value) after each cycle. The three parameters are 1) the axes of the two points to manipulate, 2) the index gap between the two points (later points will wrap around the current "block" of points to find a partner and only if the later point wasn't already the partner for that earlier point), 3) the amount to manipulate with. The key has two parts, 1) the positive integer number of cycles "n" and 2) an integer array of axis initializations "d". The dimension "D" is the number of axes, so it's the length of d. D must be one of 2, 3, 5, 6, 10, 15--kudos to anyone who recognizes the definition of this set of numbers before the end of this paragraph. The recommended n is at least 210. However, the size of n must be carefully counterweighted with message length because either one has a strong tendency to drop time-performance (i.e., complexity) like a brick. Analytically speaking, I believe the relationship is O(n), but I only have rough empirical confirmation of that.
One characteristic of this cipher that it shares with many other ciphers is its cyclic nature. In earlier tries at an algorithm, when the block size was the length of the text and no transposition step was integrated, specific plaintext lengths and dimension choices would result in characters that only flipped sign, no matter the number of cycles! These were "degenerate" cases when two indexes always paired up with one another; therefore, each gap parameter value was always paired up with the same axis parameter value (although the converse was usually not true--a specific axis parameter might or might not always have been paired up with the same gap parameter).
In a simple example, a message length (padded if necessary) of 32 and a dimension of 4 results in 32/4 = 8 "points" and consequently 8 possible gap values between indexes (0,4...24,28). The gap parameter value cycles through its 8 possibilities as the axis parameter value simultaneously cycles through its 4 possibilities (0,1,2,3), so now a gap of 4 always applies to an axis of 1, a gap of 8 always applies to an axis of 2, etc. The pattern is clear: a "degenerate" case corresponds to when the number of axes is a divisor of the number of possible gaps, for it is those circumstances when the cycle of axis parameter values maps nicely onto the cycle of gap parameter values. What we have here is an application of the fundamental theorem of cyclic groups (the description "maps nicely onto" is an informal way of stating that one cycle or cyclic group is a subgroup of another cycle or cyclic group).
The complication of avoiding these degenerate cases motivated me both to switch to a set block length and to a set collection of dimension choices, where the dimension choices are 1) divisors of the block length and are not 2) divisors of the associated quotient. Put another way, the block length is the least common multiple of the dimension and the block length divided by the dimension. In addition, I preferred smaller block lengths and dimension choices but I also preferred larger amounts of dimension choices. Check the handy Wikipedia table yourself if you like. As previously mentioned, I opted for a block length of 30. The recommended number of cycles, 210, is the least common multiple of the gap parameter possibilities, axis parameter possibilities, and amount parameter possibilities (7).
Key strength is a question I'm not sure how to answer. The key's "n", the number of cycles to run on the set of message points, isn't much of an obstacle to brute-force cracking. If x number of cycles hasn't yielded even a partial message, trying x + 1 cycles is no problem at all! The key's "d", the array of axis initializations, is more promising. Assuming 2000 possible values for an axis (-999 to 1000 inclusive), for a choice of 2 dimensions that would be a mere 2000^2 = 4,000,000. But a choice of 15 dimensions would be 2000^15 = 3.2768 * 10^49 according to my calculator, which is greater than 2^128. In any case, my limited testing indicated that the initialization value for a chosen axis affects a proper subset of message characters, not all--partial keys are useful, but to verify the correctness of the partial key one must have the number of cycles right.
My Javascript implementation of the Pointed cipher is part of the blog layout, down at the bottom on the right. In the page source, search for 'pointedcipher' (one word). If you use it or download it I am not responsible for any awful consequences. I don't advise you to use it for encrypting anything important or unimportant. Legal disclaimer, etc. It's purely for entertainment, educational, and/or edutainment purposes.
The problem, as some readers have already guessed, is that the more interesting examples of automata are just not guaranteed to be reversible. A message's encoded information wouldn't always survive the journey. Two different messages could end up with the same outcome, for instance, or a message could merely evaporate after a short number of iterations. The original signal would be so entangled with the encryption-introduced noise that it wouldn't be salvageable...by anyone. Grudgingly, I adopted the restriction that none of my automata rules would include creation or destruction.
This restriction, along with the need to assign axes to the automata cells for the character mapping, just left me with a set of Cartesian coordinates whose movements somehow affected one another. Then I made several observations: 1) per common practice, the number of dimensions can be set arbitrarily; 2) each character can be encoded as the distance along a particular axis for a particular point; 3) interactions among multiple points at once, especially given the number of points needed to encode a message, are often too impractical to calculate.
After experimentation, I've settled on this impractical encryption algorithm. The idea is to divide up a message into tuples or "points", and perform pairwise-defined substitution and transposition on each point for a number of cycles. Track the values of the three parameters that control the operation of the substitution and/or transposition, and increase them (with wrap-around back to the start value) after each cycle. The three parameters are 1) the axes of the two points to manipulate, 2) the index gap between the two points (later points will wrap around the current "block" of points to find a partner and only if the later point wasn't already the partner for that earlier point), 3) the amount to manipulate with. The key has two parts, 1) the positive integer number of cycles "n" and 2) an integer array of axis initializations "d". The dimension "D" is the number of axes, so it's the length of d. D must be one of 2, 3, 5, 6, 10, 15--kudos to anyone who recognizes the definition of this set of numbers before the end of this paragraph. The recommended n is at least 210. However, the size of n must be carefully counterweighted with message length because either one has a strong tendency to drop time-performance (i.e., complexity) like a brick. Analytically speaking, I believe the relationship is O(n), but I only have rough empirical confirmation of that.
- Divide the plaintext into blocks of length 30, padding out the last block with blanks if necessary. After each conversion from character to number happens, add the corresponding element from d (plaintext index % dimension works fine). Perform the following steps on each block and concatenate all results. Actually, my implementation modifies the original array in-place, block by block, by passing indexes around.
- Start the axis parameter value at 0 (the first axis), the gap at 2*D (pair the current point to the point after the next point), and the amount at 1. Perform the following steps n times.
- Loop over the block point by point (this just means increasing the index by D each time), performing the following two steps.
- Use the current values of the axis and gap parameters to determine what two indexes to work on. If the referenced values are equal, multiply both values by -1. Otherwise, adjust each referenced value such that the greater increases and the lesser decreases by the value of the amount parameter.
- Swap the value of the first axis of the current point with the value of whichever axis of the next point (again, wrapping around the block if necessary) is indicated by the axis parameter.
- After looping over the points, adjust the three parameters for the next cycle, starting over at 0 if exceeding maximums. The axis value increases by 1 up to D - 1, the gap value increases by D up to 29 (remember that the gap calculation wraps around the block of points), and the amount value increases by 1 up to 6.
One characteristic of this cipher that it shares with many other ciphers is its cyclic nature. In earlier tries at an algorithm, when the block size was the length of the text and no transposition step was integrated, specific plaintext lengths and dimension choices would result in characters that only flipped sign, no matter the number of cycles! These were "degenerate" cases when two indexes always paired up with one another; therefore, each gap parameter value was always paired up with the same axis parameter value (although the converse was usually not true--a specific axis parameter might or might not always have been paired up with the same gap parameter).
In a simple example, a message length (padded if necessary) of 32 and a dimension of 4 results in 32/4 = 8 "points" and consequently 8 possible gap values between indexes (0,4...24,28). The gap parameter value cycles through its 8 possibilities as the axis parameter value simultaneously cycles through its 4 possibilities (0,1,2,3), so now a gap of 4 always applies to an axis of 1, a gap of 8 always applies to an axis of 2, etc. The pattern is clear: a "degenerate" case corresponds to when the number of axes is a divisor of the number of possible gaps, for it is those circumstances when the cycle of axis parameter values maps nicely onto the cycle of gap parameter values. What we have here is an application of the fundamental theorem of cyclic groups (the description "maps nicely onto" is an informal way of stating that one cycle or cyclic group is a subgroup of another cycle or cyclic group).
The complication of avoiding these degenerate cases motivated me both to switch to a set block length and to a set collection of dimension choices, where the dimension choices are 1) divisors of the block length and are not 2) divisors of the associated quotient. Put another way, the block length is the least common multiple of the dimension and the block length divided by the dimension. In addition, I preferred smaller block lengths and dimension choices but I also preferred larger amounts of dimension choices. Check the handy Wikipedia table yourself if you like. As previously mentioned, I opted for a block length of 30. The recommended number of cycles, 210, is the least common multiple of the gap parameter possibilities, axis parameter possibilities, and amount parameter possibilities (7).
Key strength is a question I'm not sure how to answer. The key's "n", the number of cycles to run on the set of message points, isn't much of an obstacle to brute-force cracking. If x number of cycles hasn't yielded even a partial message, trying x + 1 cycles is no problem at all! The key's "d", the array of axis initializations, is more promising. Assuming 2000 possible values for an axis (-999 to 1000 inclusive), for a choice of 2 dimensions that would be a mere 2000^2 = 4,000,000. But a choice of 15 dimensions would be 2000^15 = 3.2768 * 10^49 according to my calculator, which is greater than 2^128. In any case, my limited testing indicated that the initialization value for a chosen axis affects a proper subset of message characters, not all--partial keys are useful, but to verify the correctness of the partial key one must have the number of cycles right.
My Javascript implementation of the Pointed cipher is part of the blog layout, down at the bottom on the right. In the page source, search for 'pointedcipher' (one word). If you use it or download it I am not responsible for any awful consequences. I don't advise you to use it for encrypting anything important or unimportant. Legal disclaimer, etc. It's purely for entertainment, educational, and/or edutainment purposes.
Thursday, June 19, 2008
if you want to be taken seriously...
...don't use Comic Sans MS in your email sig. Just don't. I suppose you can use it for your own name to try to give the impression that you're "just a simple, ordinary guy", but under no circumstances use it for your job title or employer. Form reinforces or contradicts content, people. Seeing the words "analyst", "engineer", "senior" in Comic Sans MS font is irrepressibly funny, and I will proceed to mentally point and laugh at your correspondence.
It's also rather disheartening to read the word "services" in Comic Sans MS as part of your employer's name. It really doesn't shore up your customers' confidence that they're buying high-quality expertise...
It's also rather disheartening to read the word "services" in Comic Sans MS as part of your employer's name. It really doesn't shore up your customers' confidence that they're buying high-quality expertise...
Thursday, June 05, 2008
when code simplicity is detrimental
I like code simplicity. Code is a composition of pieces or elements, and simplicity refers to a minimal quantity of those. Generally, simplicity confers greater productivity: quicker speed of initial development, safer modifications and later additions, faster understanding for newcomers, easier debugging and profiling, a smaller proportion of time spent on peripheral tasks. I suspect that the insightful application of simplicity is part of the huge skill disparity between exceptional programmers and the rest. An E. F. Schumacher quotation says it better:
Any intelligent fool can make things bigger, more complex, and more violent. It takes a touch of genius—and a lot of courage—to move in the opposite direction.However, slavish attachment to code simplicity could have counterintuitive results that solve the task harder or wrongly.
- NIH (Not Invented Here) syndrome. I doubt this term needs any more explanation. Simplicity may be one of its motivations, as in "The solutions that were Not Invented Here are all needlessly intricate for my use case(s); I'll circumvent the learning curve and integration headache by writing for myself just exactly what I need". I admit that occasionally such reasoning is valid, because the outside solutions truly are either too cumbersome or lacking. But NIH is prone to producing code (a custom web framework and/or, horror of horrors, another template language?) that combines breathtaking simplicity with the subtle bugs and limitations that its matured counterparts patched several versions ago.
- Flattened "lightweight". Obviously, one of the best ways to keep code small and simple is to focus it on a minimal, carefully-selected set of features or capabilities, and maybe also sacrifice unwanted flexibility and reuse. Code written explicitly with that design decision is "lightweight". Lightweight code is often excellent at fulfilling limited requirements in a way that's easy to understand. Unfortunately, when requirements expand, the trickier situations that the lightweight code ignores could abruptly become more important. The good news is that several approaches virtually eliminate the lightweight/heavyweight dilemma: plugin-oriented code for using separate modules as needed, code (component) specialization for connecting up several expert "lightweights" to match a generalized "heavyweight", APIs and standardized cooperation mechanisms for combining code that's similar but has complementary pros and cons.
- Security. This is one of the inherently trickiest domains of software development. Its experts tend to be tricksy and unconventional, no matter what color of hat they wear. Here more than elsewhere, simple thoughts are typically naive thoughts. Developers who aren't specialists shouldn't be attempting to create "fast, easy, elegant, novel" data protection algorithms. When security matters, simplicity is a much lower priority than proven best practices. To the untrained, inaccurate evaluations of security weaknesses are perilously likely, and the cliché is accurate: a whole security scheme is no stronger than its weakest point.
- Interoperability. Any successful communication depends on a mutually-understood encoding strategy. For code, whose "intelligence" is drastically limited, deviation from a chosen encoding strategy will result in communication failure in some way, although the failure might not be absolute or fatal, e.g., discarding just the unexpected symbols. Therefore, de-facto and de-jure standards for data encoding are important. But in the majority of situations, the entire depth and breadth of one or more standards really isn't strictly applicable or necessary. Thus, the temptation is to write or reuse code that simply handles the "core subset" of a communication standard. Sometimes this choice is justified, when all the communicators are exhaustively known and under tight control (within an intranet, for instance). Other times, this choice is short-sighted because code that tries to communicate using the standard could try to use the complex parts that were left out for simplicity's sake. XML namespaces and Unicode are two common targets. Fortunately, conforming XML parsers are everywhere and Unicode is built right into programming languages.
- Scalability. As hardware's twin performance pushes of miniaturization and faster clock frequencies give way to the alternate path of workload parallelization (more cores, more sockets), ignoring code scalability in favor of simplicity becomes a less competitive option. Sure, the individual units of parallelized code are usually pretty simple. Depending on the architecture, the local memory available to each unit might be tiny in modern terms. The complexity arises in the efficient coordination of all the units and in the decomposition of an algorithm into parallel tasks. Another scalability realm is the deployment of code to multiple hosts, especially to high-traffic Web servers. Code that's not written toward this goal has the potential to hinder an even distribution of the user load.
Subscribe to:
Posts (Atom)