What's new in arc3?

The long-awaited new update of Arc (arc3) has been made available. I've examined the release to determine what's new in arc3 beyond the mysterious new logo: arc logo.

arc3 is a major change from arc2, with about 3000 lines and most files changed. The following gratuitous pie chart illustrates that the largest number of changes are in the news server (news.arc), with many changes to the fundamental language (ac.scm and arc.arc) as well as the web and application servers (app.arc and srv.arc).
Summary of changes
The news server has been heavily modified with new ranking, support for polls, and improved spam rejection, among other features. A new "how-to-run-news" file explains how to set up your own Hacker News-like server. I will discuss the news server, web server, app server, and html library in more detail in a future article. I will focus this article on the language and main libraries.

If you're looking to try arc3, it can be downloaded here. Arc3 has significant incompatibilities with previous versions that will cause problems if you're not prepared.

Incompatibilities

  • Arc now requires mzscheme 372. Arc will not work with the version 352 required by arc2, or with the newer versions of mzscheme.
  • flush-output is disabled by default on write, disp, and functions that use these. See declare below.
  • The result of accum is no longer in reverse order.
  • news.arc is no longer loaded as part of arc.
  • a.b.c is now ((a b) c), not (a b c)
  • a!b!c is now ((a 'b) 'c), not (a 'b 'c)
  • The set function has been renamed assign, and the assert function has been renamed set.
  • writefile1 removed.
  • flat no longer supports stringstoo option.
  • to-nearest renamed to nearest.
  • random-elt renamed to rand-elt.
  • firstn-that renamed to retrieve.
  • plural renamed to pluralize.

New functions

  • int function coerces to an integer.
  • force-close discards buffered output and closes the descriptor.
  • mvfile moves a file.
  • memory returns the current memory use.
  • sin, cos, tan, and log are finally available.
  • declare is used to set system variables: explicit-flush to control automatic output flushing, and direct-calls for the function call optimization.
  • timedate converts seconds to date.
  • copylist copies a list.
  • defs applies def to name, args, body triples to define multiple functions at once.
  • get provides simpler table / array lookup. get!foo creates a function that takes a table and looks up 'foo. (Actually, it's a generic mechanism to create a function to apply an argument to something.)
  • writefile writes to a temporary file first.
  • letter tests if a character is a letter.
  • sum sums up function results.
  • med returns median of list.
  • minutes-since returns minutes since a time.
  • defcache wrapper around cache to reduce boilerplate.
  • datestring creates "y - m - d" string.
  • or= modifies a variable if the variable is false.
  • out evaluates and prints an expression.
  • fromdisk, diskvar, disktable, and todisk provide a simplified mechanism for loading and storing data.
  • halve splits a string in two on the first separator.
  • positions returns a list of positions where a test is true.
  • lines splits a string into lines.
  • urlencode urlencodes a string.
  • nonblank returns a string if it's nonblank.
  • plural returns a count and pluralized word.

Bug fixes

  • atomic-invoke fixed.
  • socket-accept uses custodians so web servers can forcibly close connections if the clients aren't reading data.
  • coerce of string into int rounds the value as does number to char.
  • list modified to copy the list.
  • safeset now writes warnings to stderr.
  • each fixed to avoid conflict with afn.
  • cut fixed.
  • trues fixed.
  • whiler fixed.
  • rand-string now more random to help fix huge security hole.
  • split fixed for negative positions..
  • memo now memoizes nil results too.
  • readline will terminate on nil.
  • copy fixed.
  • hours-since fixed.
  • date fixed to avoid system operation.
  • intersperse now works with nil.
  • load fixed.
  • posmatch fixed.
  • num fixed to handle negative numbers.

Optimizations

  • complement in function position
  • The <, >, and + operations when applied to two arguments.
  • (foo bar) is optimized to call foo directly if foo is a global bound to a function. This is controlled by declare.

Other changes

  • Debugging has been improved by giving the Arc names of things to Scheme, and turning on line counting.
  • A default value can be provided when accessing a hash table.
  • Currying was largely implemented but not enabled.
In conclusion, arc3 has some nice improvements, bug fixes, and optimizations, but nothing too dramatic in the language itself. If you're using the Arc news server, you'll definitely want arc3 for the security fixes. On the other hand, the unofficial Anarki release of Arc has a lot of interesting features too.

Disclaimers: I have provided my interpretations of the changes in arc3, and I'm sure there are some errors; please provide feedback if you find mistakes. Note that I am not connected with the arc team in any way, and this is not an "official" list of changes. In addition, arc3 may change without warning, so any of the above can be obsoleted. (There are some good reasons for real version numbers, bug tracking, and source code control, but I won't belabor that point.)

Arc errors (slightly) demystified

If you've programmed in Arc, you've probably noticed that the error messages are often cryptic at best. Error handling is a major weakness of the current Arc implementation; I've wasted a lot of time trying to figure out why my code fails, because the error message has no connection to the actual problem.

This article gives examples of some mysterious error messages, how they arise, and what they mean. Hopefully this can save some debugging time.


Usually Arc doesn't provide any indication of where the error occurred. In the cases where Arc does provide location information, the location is a byte offset, not a line number, which is hard to interpret:
arc> (load "t.arc")
Error: "t.arc::720: read: expected a ')'"
If you're loading a file, how do you find byte offset 720? One way is dd if=t.arc bs=1 skip=720 which will display the file from the 720th character onward.

Similar errors can be generated from REPL input. In this case, the unexpected parenthesis is at character 910 that's been typed to the REPL (although that's unlikely to be useful):

arc> )
Error: "UNKNOWN::910: read: unexpected ')'"

arc> (def foo (x) (prn x))
#<procedure: foo>
arc> (foo 1 2)
Error: "procedure  foo: expects 1 argument, given 2: 1 2"
This error message is straightforward - too many arguments were given to the procedure..
arc> (def (bar (x) (prn x)))
Error: "#<procedure>: expects at least 2 arguments, given 1: (bar (x . nil) (prn x . nil) . nil)"
In this case, the expected arguments error is more mysterious. Misplaced parentheses generate this error from the depths of the interpreter.
arc> (+ 1 ("s"))
Error: "car: expects argument of type <pair>; given ()"
You probably have extra parentheses, attempting to index into a string. For a number, the error message is much clearer:
arc> (+ 1 (42))
Error: "Function call on inappropriate object 42 ()"

arc> (def foo (a (b c)) (prn a))
arc> (foo 1 2)
Error: "car: expects argument of type <pair>; given 2"
Destructuring bind fails because argument format doesn't match function definition.
arc> (def f (o x (table)) (prn x))
arc> (f 42)
Error: "car: expects argument of type <pair>; given ()"
You need two sets of parentheses in the def, one around the arguments, and one around the optional argument:
arc> (def f ((o x (table))) (prn x))

arc> (+ 1 "s")
Error: "+: expects type  as 2nd argument, given: \"s\"; other arguments were: 1"
Straightforward: wrong type to +.
(= t 42)
Error: "Can't rebind t"
The atoms t and nil can't be changed. Don't use t as a variable.
arc> (map (prn) '(a b c))
Error: "Function call on inappropriate object nil (a)"
You need to pass a function to map, such as prn or [prn _] rather than (prn)
arc> (car "abc")
Error: "Can't take car of \"abc\""
Car and cdr don't work on strings. You'll also get this error if the function erroneously uses car on a string internally:
arc> (counts "abc")
Error: "Can't take car of \"abc\""

arc> (with a 4 (+ a 1))
Error: "Can't take cdr of a"
The mysterious "can't take cdr" error shows up if you don't have parentheses around the variable assignments. You may be thinking of let, which doesn't use the parentheses and assigns a single variable.

Let me know if you have any other candidates for mystery errors.

F# for Cheapskates

Since I've been hearing good things about the F# language from various people, I wanted to give it a try, but I wasn't interested in spending $250 on Microsoft Visual Studio. It turns out, however, that you can run F# for free in several ways.

F# with Visual Studio Shell

On Windows, you can run F# with a complete IDE by using Visual Studio Shell (not to be confused with Visual Studio or Visual Studio Express). Visual Studio Shell is an IDE without any included languages, and is available for free.

Installation

(Instructions updated August 2009 for latest versions.)

  • Download Visual Studio 2008 Shell SP1 (integrated mode).
  • Run the download. Note: this download does not install Visual Studio Shell; it unpacks a "redistributable package" for Visual Studio Shell that contains the installer. Don't ask why. Make sure you note where it puts this, probably C:\VS 2008 Shell Redist\Integrated Mode.
  • Run the Visual Studio Shell installer vside.enu.exe that was unpacked above. Important: you must not have F# installed at this point; if you already have it installed, uninstall it first.
  • Make sure Microsoft Visual Studio 2008 shows up in your Start menu.
  • Download and install F# 2009 CTP (msi).

Running F# inside Visual Studio Shell

Start Visual Studio Shell. Start F# should appear under View -> Other Windows -> F# Interactive; Control-Alt-F should also work. Type into the F# Interactive window:
printfn "Hello world!";;
Hello world!
val it : unit = ()
Note that in the interactive window, you must enter ;; to have your input processed.

F# in Visual Studio

To see the F# tutorial, click "Create Project ..." in the "Recent Projects" window, then double-click "F# Tutorial".

To create a new program, click "Create Project ..." in the "Recent Projects" window, then double-click "F# Application". Type code into the "Program.fs" window, for example:

printfn "Hello world!"

open System
Console.ReadKey(true) |> ignore
To run the program, hit F5. The purpose of the last two lines is to keep the console window open until you hit a key; otherwise, the window will only flash up briefly. Note that the double-semicolons aren't required.

F# in Visual Studio

You can also highlight text in the program window and press Alt-Enter to have it executed in the F# Interactive window. Visual Studio includes a debugger with breakpoints and all. Select Debug -> Start Debugging to run under the debugger.

F# from the Windows command line

You can also skip Visual Studio and run F# from the command line. To do that, just download F# as above. Then run the F# interpreter fsi.exe either from a cmd window or from the Start menu:
C:\Program Files\FSharp-1.9.6.2\bin\fsi.exe
> printfn "Hello world!";;
Hello world!
val it : unit = ()

F# with Mono

Another option is running F# with Mono. This provides a way to run F# on Linux or OS X. To do this, download and unzip F# 2008 CTP (zip). Download and install Mono 2.X. The Readme file in the F# distribution provides all the details on running F# with Mono, so look there.

(I wasn't too successful with Mono; it's no longer supported on RedHat, and compiling from sources ran into libgdiplus incompatibility. I could only run fsi.exe with the --no-gui flag.)

F# under mono

F# vs Arc

Since this is nominally an Arc blog, I should compare F# and Arc. The F# language has some nice features such as type inference, currying, and pattern matching. On the other hand, it lacks powerful Lisp-style macros, and is not as terse as Arc. The F# implementation is much, much more polished than Arc. It has extensive documention, an IDE, real error messages, reasonable performance, huge libraries, a compiler, and so forth. Overall, I find programming in F# more enjoyable than in Arc, and find it easier to get something done in F# after using it for a few hours than in Arc after several months.

Next steps

Once you have F# running, you can take a look at Microsoft's F# in 20 Minutes tutorial to learn more. I'm currently going through the book Expert F#.

One of the interesting things about F# is you can easily write graphical Windows applications. A couple quick hints: to use the Windows Forms dlls, go to "Project -> Add Reference" and click "System.Windows.Forms", and then "ok". To suppress the console window, go to "Project -> ConsoleApplication2 Properties..." and select "Output type: Windows Application".

I found (and I realize this is an obvious lesson) that I learned much more from actually writing simple F# programs than from reading blog posts about it. So if you're still reading, stop now, install F#, and write some code!

Why Unicode is important

A couple months ago I went to a French cafe at the Denver airport. Outside the restaurant was a large rotating menu, clearly costly to manufacture. Some of the beverages on the menu puzzled me: "Caf  Latte", "Caf  Americano", and "Caf  Mocha".
faulty menu
When I saw "Proven al Salad", I realized that they had suffered an unfortunate accented character mishap; the menu was supposed to offer "Café Latte", "Café Americano", "Café Mocha", and "Provençal Salad". Unfortunately, the accented characters disappeared somewhere in the sign printing process. The font, likely Marker Felt Thin, has accented characters, so they should have been able to get it right.

I {entity} unicode So... can any deep insight be extracted from this, or is it just a somewhat entertaining picture? My first conclusion is that notwithstanding the Arc Unicode debacle, support for non-ASCII characters is important even if you're a sign shop in Denver. My second conclusion (based on personal experience) is that everything conspires to destroy non-ASCII characters, and it's non-trivial to get them right. My third conclusion - which will make sense if you've seen UTF-8 get misinterpreted as latin-1 - is the restaurant should be grateful that they didn't end up with "Café Latte".

The Y Combinator in Arc and Java

I was recently reading The Little Schemer, a very intersting book about how to think in Scheme. The book uses a unique question-response style to present simple concepts such as cons and recursion, as well as complex concepts such as using lambda functions to build arithmetic out of fundamental axioms, and deriving the Y Combinator.

As you're probably aware, the Y Combinator is a fixed-point combinator, which lets you build recursion out of an anonymous, non-recursive function. The tricky part of building recursion without naming is that if you can't name a function, how can the function call itself? The Y Combinator is a way to do this.

In Arc, it's easy to write a recursive factorial function:

(def fact (n)
  (if (is n 0) 1
      (* n (fact (- n 1)))))
Note that def assigns the name fact to this function, allowing it to recursively call itself. But without def, how can the function call itself?

Fixed point as an infinite stack of function calls

Let's make a function fact-gen that if passed the factorial function as input, returns the factorial function. (Note that fn is Arc's equivalent of lambda.)
(def fact-gen (fact-in)
 (fn (n)
  (if (is n 0) 1
      (* n (fact-in (- n 1))))))
This may seem rather useless, returning the factorial function only if you already have it. Since we don't have a factorial function to pass in, we'll pass in nil (technically bottom). This at least gets us started:
arc> ((fact-gen nil) 0)
1
arc> ((fact-gen nil) 1)
Error: "Function call on inappropriate object nil (0)"
We can compute factorial of 0, but factorial of 1 hits nil and dies. But we can take our lame factorial function, pass it in to fact-gen and get a slightly better factorial function that can compute factorials up to 1:
arc> ((fact-gen (fact-gen nil)) 1)
1
We can repeat this to get an even more useful factorial function:
arc> ((fact-gen (fact-gen (fact-gen (fact-gen (fact-gen nil))))) 4)
24
If we could have an infinite stack of fact-gen calls, then we would actually have the factorial function. But we can't do that. Or can we?

The fixed point of a function f is a value x for which f(x) = x. We can apply the same idea to our infinite stack of fact-gen. Since applying fact-gen to the infinite stack one more time makes no difference, (fact-gen infinite-stack) = infinite-stack, so the infinite stack is a fixed point of fact-gen. Thus, the fixed-point of fact-gen is the factorial function.

If taking the fixed point of an algorithmic function seems dubious to you, a full explanation is available in Chapter 5 of the 1300 page tome Design Concepts in Programming Languages; trust me, it's all rigorously defined.

The fixed-point combinator

So how do you find the fixed point without an infinite stack of functions? That's where the fixed-point combinator (also known as the Y combinator) comes in. The fixed-point combinator Y takes a function and returns the fixed point of the function. That is, applying the function once more makes no difference:
y f = f (y f)
You may wonder how the Y combinator computes an infinite stack of functions. The intution is it computes a finite stack that is just big enough for the argument.

The fixed-point combinator in Haskell

In Haskell, you can use the above definition of the Y combinator directly:
y(f) = f (y f)
fact f n = if (n == 0) then 1 else n * f (n-1)
y(fact) 10
This is a bit of a "cheat", since the definition of the y combinator takes advantage of Haskell's pre-existing recursion, rather than providing recursion from scratch. Note that this only works because of lazy evalation; otherwise the definition of y is an infinite loop. (Haskell includes the Y combinator under the name fix.)

The Y combinator in Arc

The Little Schemer derives the Y combinator in Scheme. The Arc version is very similar:
(def Y (r)
  ((fn (f) (f f))
   (fn (f)
     (r (fn (x) ((f f) x))))))
If the Y combinator is applied to the earlier fact-gen, it yields a recursive factorial function. Like magic:
arc>((Y fact-gen) 10)
3628800

You may protest that this doesn't really implement anonymous recursion since both Y and fact-gen are explicitly named with def, so you could really just call fact-gen directly. That naming is just for clarity; the whole thing can be done as one big anonymous function application:

arc> (((fn (r)
  ((fn (f) (f f))
   (fn (f)
     (r (fn (x) ((f f) x))))))

(fn (fact)
 (fn (n)
  (if (is n 0) 1
      (* n (fact (- n 1)))))))

10)
3628800
Now you can see the recursive factorial can be computed entirely with anonymous functions, not a def in sight. The first blob is the Y combinator; it is applied to the second blob, the factorial generator, and the resulting function (factorial) is applied to 10, yielding the answer.

Y Combinator in Java

The Y combinator in a Lisp-like language is not too tricky. But I got to wondering if it would be possible to implement it in Java. I'd done crazy continuation stuff in Java, so why not the Y combinator?

Several objections come to mind. Java doesn't have first-class functions. Java doesn't have closures. Everything in Java is an object. Java is statically typed. Is the idea of a Y combinator in Java crazy? Would it require total Greenspunning?

To implement the Y combinator in Java, I did several things. Since Java doesn't have first-class functions, I wrapped each function in an anonymous inner class with a single method apply(), which executes the function. That is, I used a function object or functor. Since "objects are a poor man's closures" (Norman Adams), I used this object creation in place of each closure. In order to define types, I restricted my Java Y combinator to integer functions on integers. Each type defines an interface, and each object implements the appropriate interface.

Using these techniques, I was able to fairly directly implement the Y combinator in Java. The first part defines a bunch of types: IntFunc is a simple function from integers to integers. IntFuncToIntFunc is the type of the factorial generator, taking an integer function and returning another integer function. FuncToIntFunc is the somewhat incomprehensible type of the Y combinator subexpressions that apply f to f yielding an integer function. Finally, the Y combinator itself is an IntFuncToIntFuncToIntFunc, taking an IntFuncToIntFunc (fact-gen) as argument and returning an IntFunc (the factorial function itself).

class YFact {
  // Integer function returning an integer
  // int -> int
  interface IntFunc { int apply(int n); }

  // Function on int function returning an int function
  // (int -> int) -> (int -> int)
  interface IntFuncToIntFunc { IntFunc apply(IntFunc f); };

  // Higher-order function returning an int function
  // F: F -> (int -> int)
  interface FuncToIntFunc { IntFunc apply(FuncToIntFunc x); }

  // Function from IntFuntToIntFunc to IntFunc
  // ((int -> int) -> (int -> int)) -> (int -> int)
  interface IntFuncToIntFuncToIntFunc { IntFunc apply(IntFuncToIntFunc r);};
Next comes the meat. We define the Y combinator, apply it to the factorial input function, and apply the result to the input argument. The result is the factorial.
  public static void main(String args[]) {
    System.out.println(
      // Y combinator
      (new IntFuncToIntFuncToIntFunc() { public IntFunc apply(final IntFuncToIntFunc r) {
      return (new FuncToIntFunc() {public IntFunc apply(final FuncToIntFunc f) {
          return f.apply(f); }})
 .apply(
          new FuncToIntFunc() { public IntFunc apply(final FuncToIntFunc f) {
         return r.apply(
                new IntFunc() { public int apply(int x) {
    return f.apply(f).apply(x); }});}});}}

    ).apply(
        // Recursive function generator
        new IntFuncToIntFunc() { public IntFunc apply(final IntFunc f) {
          return new IntFunc() { public int apply(int n) {
     if (n == 0) return 1; else return n * f.apply(n-1); }};}} 

    ).apply(
      // Argument
      Integer.parseInt(args[0])));
  }
}
The result is the factorial of the input argument: (source code)
$ javac YFact.java
$ java YFact 10
3628800
Surprisingly, this code really works, implementing the Y combinator. Note that there are no variables (apart from arguments), and no names are assigned to any of the anonymous functions. Yet, we have recursion.

The Java version is considerably more verbose than the Arc version, since each function becomes an object creation wrapping an anonymous function declaration, with a liberal sprinkling of type declarations, public and final. Even so, there is a direct mapping between the Arc code and the Java code. There's no Greenspunning in there, no Lisp simulation layer. Ironically, the Java code starts to look like Lisp code, except with a bunch of }}} instead of ))).

To convince you that the Java recursion works even in a more complex case, we can implement Fibonacci numbers by simply replacing the input function: (source code)

...
        // Recursive Fibonacci input function
        new IntFuncToIntFunc() { public IntFunc apply(final IntFunc f) {
          return new IntFunc() { public int apply(int n) {
            if (n == 0) return 0;
            else if (n == 1) return 1;
            else return f.apply(n-1) + f.apply(n-2); }};}}
...
The code recursively generates Fibonacci numbers:
$ java YFib 30
832040

Is this the "real" Y Combinator?

The typical form of the Y combinator is:
λf.(λx.f (x x)) (λx.f (x x))
and you may wonder why the Y combinator in Arc and Java is slightly different. Because Java (and Scheme, Arc, etc.) are call-by-value languages and not call-by-name languages, they require the applicative-order Y combinator. This combinator has the form:
λr.(λf.(f f)) λf.(r λx.((f f) x))
The call-by-name Y combinator will go into an infinite loop in a call-by-value language. I found this out the hard way, when I implemented the "wrong" Y combinator in Java and quickly got a stack overflow.

For details on applicative-order, eta-reduction, why different combinators are required, and a derivation of the Y combinator, see Sketchy Lisp.

Java vs. Lambda Calculus

In the Java code, new takes the place of λ and apply explicitly shows application, which is implicit in lambda calculus. To make the connection between the Java code and the lambda expression clearer, I have highlighted the key parts of the Java Y combinator:
      // Y combinator
      (new IntFuncToIntFuncToIntFunc() { public IntFunc apply(final IntFuncToIntFunc r) {
      return (new FuncToIntFunc() {public IntFunc apply(final FuncToIntFunc f) {
          return f.apply(f); }})
 .apply(
          new FuncToIntFunc() { public IntFunc apply(final FuncToIntFunc f) {
         return r.apply(
                new IntFunc() { public int apply(int x) {
    return f.apply(f).apply(x); }});}});}}
Note the exact correspondence of the highlighted parts with the lambda calculus expression:
λr.(λf.(f f)) λf.(r λx.((f f) x))

Conclusion

It is possible to implement the Y combinator in Java, showing that Java has more power than many people realize. On the other hand, the Java code is ugly and bulky; a Lisp-like language is a much more natural fit. For more fun, try going through SICP in Java.

Postscript

I received some great feedback with interesting links. Apparently a number of people enjoy implementing the Y combinator in a variety of languages:

Readline support for Arc

When using the Arc REPL, I've often wanted to edit a previous line. Python lets you access and edit lines using the arrow keys, so why not Arc? You can do this with Arc by using rlwrap and the GNU Readline library. (Not to be confused with Arc's readline I/O operation.)

Readline provides many editing operations. If you're not an Emacs user, you'll probably want to use the arrow keys to move through your current line and history, Home and End to move to the beginning and end of the line, and Delete or Backspace to delete characters. Another useful feature is Control-underscore will undo an edit. If you enter a closing parenthesis, Readline will flash the corresponding opening parenthesis, which is helpful when entering Arc code. Control-R followed by text will search your history for a line containing that text. Readline supports the standard Emacs keybindings. For instance, Control-B and Control-F to move backwards and forward, Meta-B and Meta-F to move by words, Control-K kills (erases) to the end of the line, Control-Y yanks killed text back into the line, and so on. See the documentation for the full documentation.

Installing rlwrap

But how do you use readline with Arc? There has been discussion at arclanguage.org, but there are complications. Mzscheme added support for readline in version 360, but Arc inconveniently runs on version 352. Backporting readline to 352 doesn't work cleanly.

The easiest way is to install the rlwrap command, which wraps an arbitrary command in readline. To install it on Linux, run sudo yum install rlwrap or sudo apt-get install rlwrap as appropriate.

For Windows, rlwrap is part of cygwin, although you need to explicitly select in the installer under "Select Packages to Install"; it's under "Utils -> rlwrap".

Once installed, add rlwrap in front of your mzscheme command. For instance:

rlwrap mzscheme -m -f as.scm
More ways of running rlwrap with Arc are described in the arclanguage.org discussion.

Once you have rlwrap installed, you should find entering Arc code to be much easier.

Simple Cryptanalysis with Arc

I came across a thread on reddit about why you shouldn't write your own encryption algorithm. In it, one person presented a homegrown algorithm with a challenge to break it. The algorithm turned out to be the weak Vigenère cipher, so I thought it would be interesting to break it with Arc.

In a Caesar cipher, each character in the plaintext is shifted by a fixed amount. For instance, if the shift is 3, "a" becomes "d", "b" becomes "e", and so on. In the Vigenère cipher, multiple shifts are used. For instance, if the key is (2, 4, 9), the first character is shifted by 2, the second character is shifted by 4, the third character is shifted by 9. The pattern then repeats with shifts of 2, 4, 9, 2, 4, 9, and so on until the entire plaintext is encrypted. Note that the length of the key is arbitrary.

The encrypted string of the reddit cipher can be expressed in Arc as:

(= s "t/GlC/,@RiG.^(xBQ2.MslF27@w#xXw,KHx,/9xiU-(!\"ixf%\";iA-=@wiW-=6EnRX#,stL-\"6Mqy!6:HDf#,6LDA0,)LoS9@:slPY9!xmx!?/LiM,,MsjL16@xCx.,6zrT26:HDx-$,svM/,6MnQ!P6sXz#/%NBJ^6jsly,6#ttCX/+sjx9/+MuCX8/MiFY(.xAx$/+AxS!69AjL4/$ziR5,6tuE-(/MqKC")
The first thing we can do is determine how long the key is. Since the key repeats, it is likely that characters in the ciphertext will also repeat with the same frequency. We can apply a simple autocorrelation function:
(def autocorrelate (s offset)
  (let total 0
    (for i 0 (- (len s) offset 1)
      (if (is (s i) (s (+ i offset)))
        (++ total)))
    total))
This function will compare each character with the character offset later in the string, and count the number of matches. Let's find the best value for offset:
(for i 1 20 (prn i " " (autocorrelate s i)))
1 2
2 3
3 1
4 6
5 6
6 16
7 6
8 3
9 0
10 1
11 3
12 16
13 7
14 5
15 0
16 3
17 5
18 17
19 1
20 3
Note that comparing each character with the character 6 characters later has 16 matches, which is much better than the other offsets. So it's pretty likely that the key is length 6. (There are also spikes at multiples of 6, as you'd expect.)

Now let's make a function to decode an encrypted character given an offset. We need the algorithm's character set mapping m used to shift the characters. The typical Vigenère uses a character set of 26 letters, but the challenge algorithm uses a much larger character set. To decode a character, we look it up in the mapping to get an integer index, shift by the offset, and convert back to a character.

(= m " abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.,-\'?/\\!@#$%^*()+=~\";:")
(def decodechar (ch offset)
  (m (mod (- (pos ch m) offset) (- (len m) 1))))
For example, if our characters were shifted by 3, "D" should decode as "A":
(decodechar #\D 3)
#\A
At this point, there are multiple approaches to break the cipher. If you have any known plaintext, you can immediately get the key. Without a known plaintext, you can take an expected plaintext such as " the " and drag it through the ciphertext, generating a presumtive key at each point. If the same pattern pops out in several places, you may have the key.

Alternatively, if we take every 6th character in the ciphertext, we have 6 Caesar cipers, and we can break each one separately. Breaking a Caesar cipher is easy. One approach is frequency analysis; assuming there are more e's and t's than q's and z's in the plaintext, you can compute the likelihood of each key. Log-likelihood is a typical way, but Arc doesn't have a log function, annoyingly enough.

I decided to take a more brute force approach. I made a function that prints out the decoding of one of the Caesar substrings given an offset value.

(def decodesubstring (s startpos skip offset)
    (loop (= i startpos) (< i (len s)) (= i (+ i skip))
      (pr (decodechar (s i) offset)))
    (prn))
For instance, to take every sixth character, starting with the second, and shift it by 35:
(decodesubstring s 2 6 35)
"i$/#b$UV#=/d;cc/$c/$^;/d/e/\/dd$**^\d
That deciphering doesn't look too good if we're looking for English text. Either our plaintext is a Perl program or 35 is the wrong offset. Let's brute-force try all the offsets:
(def testall (s startpos skip)
  (for i 0 (len m)
    (pr i " ")
    (decodesubstring s startpos skip i)))

(testall s 2 6)
0 GRxswKx";wEsMHLLsxLsxzHsMsNstsMMxAAztM
1 FQwrvJw~"vDrLGKKrwKrwyGrLrMrsrLLwzzysL
2 EPvquIv=~uCqKFJJqvJqvxFqKqLqrqKKvyyxrK
3 DOuptHu+=tBpJEIIpuIpuwEpJpKpqpJJuxxwqJ
4 CNtosGt)+sAoIDHHotHotvDoIoJopoIItwwvpI
5 BMsnrFs()rznHCGGnsGnsuCnHnInonHHsvvuoH
6 ALrmqEr*(qymGBFFmrFmrtBmGmHmnmGGruutnG
7 zKqlpDq^*pxlFAEElqElqsAlFlGlmlFFqttsmF
8 yJpkoCp%^owkEzDDkpDkprzkEkFklkEEpssrlE
9 xIojnBo$%nvjDyCCjoCjoqyjDjEjkjDDorrqkD
10 wHnimAn#$muiCxBBinBinpxiCiDijiCCnqqpjC
11 vGmhlzm@#lthBwAAhmAhmowhBhChihBBmppoiB
12 uFlgkyl!@ksgAvzzglzglnvgAgBghgAAloonhA
13 tEkfjxk\!jrfzuyyfkyfkmufzfAfgfzzknnmgz
14 sDjeiwj/\iqeytxxejxejlteyezefeyyjmmlfy
15 rCidhvi?/hpdxswwdiwdiksdxdydedxxillkex
16 qBhcguh'?gocwrvvchvchjrcwcxcdcwwhkkjdw
17 pAgbftg-'fnbvquubgubgiqbvbwbcbvvgjjicv
18 ozfaesf,-emaupttaftafhpauavabauufiihbu
19 nye dre.,dl toss es ego t u a ttehhgat
20 mxd;cqd9.ck;snrr;dr;dfn;s;t; ;ssdggf s
21 lwc"bpc89bj"rmqq"cq"cem"r"s";"rrcffe;r
22 kvb~aob78ai~qlpp~bp~bdl~q~r~"~qqbeed"q
23 jua= na67 h=pkoo=ao=ack=p=q=~=ppaddc~p
24 it +;m 56;g+ojnn+ n+ bj+o+p+=+oo ccb=o
25 hs;)"l;45"f)nimm);m);ai)n)o)+)nn;bba+n
26 gr"(~k"34~e(mhll("l(" h(m(n()(mm"aa )m
27 fq~*=j~23=d*lgkk*~k*~;g*l*m*(*ll~  ;(l
28 ep=^+i=12+c^kfjj^=j^="f^k^l^*^kk=;;"*k
29 do+%)h+01)b%jeii%+i%+~e%j%k%^%jj+""~^j
30 cn)$(g)Z0(a$idhh$)h$)=d$i$j$%$ii)~~=%i
31 bm(#*f(YZ* #hcgg#(g#(+c#h#i#$#hh(==+$h
32 al*@^e*XY^;@gbff@*f@*)b@g@h@#@gg*++)#g
33  k^!%d^WX%"!faee!^e!^(a!f!g!@!ff^))(@f
34 ;j%\$c%VW$~\e dd\%d\%* \e\f\!\ee%((*!e
35 "i$/#b$UV#=/d;cc/$c/$^;/d/e/\/dd$**^\d
36 ~h#?@a#TU@+?c"bb?#b?#%"?c?d?/?cc#^^%/c
37 =g@'! @ST!)'b~aa'@a'@$~'b'c'?'bb@%%$?b
38 +f!-\;!RS\(-a=  -! -!#=-a-b-'-aa!$$#'a
39 )e\,/"\QR/*, +;;,\;,\@+, ,a,-,  \##@-
40 (d/.?~/PQ?^.;)""./"./!).;. .,.;;/@@!,;
41 *c?9'=?OP'%9"(~~9?~9?\(9"9;9.9""?!!\."
42 ^b'8-+'NO-$8~*==8'=8'/*8~8"898~~'\\/9~
43 %a-7,)-MN,#7=^++7-+7-?^7=7~787==-//?8=
44 $ ,6.(,LM.@6+%))6,)6,'%6+6=676++,??'7+
45 #;.59*.KL9!5)$((5.(5.-$5)5+565)).''-6)
46 @"948^9JK8\4(#**49*49,#4(4)454((9--,5(
47 !~837%8IJ7/3*@^^38^38.@3*3(343**8,,.4*
48 \=726$7HI6?2^!%%27%279!2^2*232^^7..93^
49 /+615#6GH5'1%\$$16$168\1%1^121%%69982%
50 ?)504@5FG4-0$/##05#057/0$0%010$$58871$
51 '(4Z3!4EF3,Z#?@@Z4@Z46?Z#Z$Z0Z##47760#
52 -*3Y2\3DE2.Y@'!!Y3!Y35'Y@Y#YZY@@3665Z@
53 ,^2X1/2CD19X!-\\X2\X24-X!X@XYX!!2554Y!
54 .%1W0?1BC08W\,//W1/W13,W\W!WXW\\1443X\
55 9$0VZ'0ABZ7V/.??V0?V02.V/V\VWV//0332W/
56 8#ZUY-ZzAY6U?9''UZ'UZ19U?U/UVU??Z221V?
57 7@YTX,YyzX5T'8--TY-TY08T'T?TUT''Y110U'
58 6!XSW.XxyW4S-7,,SX,SXZ7S-S'STS--X00ZT-
59 5\WRV9WwxV3R,6..RW.RWY6R,R-RSR,,WZZYS,
60 4/VQU8VvwU2Q.599QV9QVX5Q.Q,QRQ..VYYXR.
61 3?UPT7UuvT1P9488PU8PUW4P9P.PQP99UXXWQ9
62 2'TOS6TtuS0O8377OT7OTV3O8O9OPO88TWWVP8
63 1-SNR5SstRZN7266NS6NSU2N7N8NON77SVVUO7
64 0,RMQ4RrsQYM6155MR5MRT1M6M7MNM66RUUTN6
65 Z.QLP3QqrPXL5044LQ4LQS0L5L6LML55QTTSM5
66 Y9PKO2PpqOWK4Z33KP3KPRZK4K5KLK44PSSRL4
67 X8OJN1OopNVJ3Y22JO2JOQYJ3J4JKJ33ORRQK3
68 W7NIM0NnoMUI2X11IN1INPXI2I3IJI22NQQPJ2
69 V6MHLZMmnLTH1W00HM0HMOWH1H2HIH11MPPOI1
70 U5LGKYLlmKSG0VZZGLZGLNVG0G1GHG00LOONH0
71 T4KFJXKklJRFZUYYFKYFKMUFZF0FGFZZKNNMGZ
72 S3JEIWJjkIQEYTXXEJXEJLTEYEZEFEYYJMMLFY
73 R2IDHVIijHPDXSWWDIWDIKSDXDYDEDXXILLKEX
74 Q1HCGUHhiGOCWRVVCHVCHJRCWCXCDCWWHKKJDW
75 P0GBFTGghFNBVQUUBGUBGIQBVBWBCBVVGJJICV
76 OZFAESFfgEMAUPTTAFTAFHPAUAVABAUUFIIHBU
77 NYEzDREefDLzTOSSzESzEGOzTzUzAzTTEHHGAT
78 MXDyCQDdeCKySNRRyDRyDFNySyTyzySSDGGFzS
79 LWCxBPCcdBJxRMQQxCQxCEMxRxSxyxRRCFFEyR
80 KVBwAOBbcAIwQLPPwBPwBDLwQwRwxwQQBEEDxQ
81 JUAvzNAabzHvPKOOvAOvACKvPvQvwvPPADDCwP
82 ITzuyMz ayGuOJNNuzNuzBJuOuPuvuOOzCCBvO
83 HSytxLy; xFtNIMMtyMtyAItNtOtutNNyBBAuN
84 GRxswKx";wEsMHLLsxLsxzHsMsNstsMMxAAztM
85 FQwrvJw~"vDrLGKKrwKrwyGrLrMrsrLLwzzysL
Looking this over, it's pretty obvious that offset 19 yields normal English letters, punctuation, and spacing, and everything else doesn't. (Of course, since we're only looking at every sixth letter, the result isn't readable.) We can repeat this for the other positions in the string
(testall s 0 6)
...
59 SepdaVirouumw eelche e ne?i  iibri ier
...
(testall s 1 6)
...
59 ilr,leckwl e y syki,l ye  oImttidtcn i
...
By scanning the six sets of outputs, the obvious key set is (59 59 19 9 24 50). Now we can write a simple function to decrypt the entire string from a list of offsets:
(def decode (s offsets)
   (for i 0 (- (len s) 1)
     (pr (decodechar (s i) (offsets (mod i (len offsets))))))
   (prn))

(decode s '(59 59 19 9 24 50))
Sincerely impressed, cheald.  Very nice work.  Now, could you let me know that you've successfully cracked this one, and let me give you one more test?  Obviously I can make it a little bit harder without changing the algorithm.
The original algorithm uses a string for the key, not integers, so we can map the offsets back to a string key:
(each x '(59 59 19 9 24 50) (pr (m x)))
66sixXnil
Ignoring nil, the key is "66sixX". Thus, a few minutes with Arc is sufficient to break the cipher and determine the original key.

The reddit thread includes multiple other challenges. One is:

(= s2 "sBC^myM3f5MC6nBF~d?OM*mqt5y@wB3Bvt'm6vy%vIB3x9RNJmJB@y,Bt6rHN4m@FS3nrF8d(It5rpL8e8:t^BpN,e(tw,AuO?m^wB8ApG4C4Ny^GpI(x4NB-Fpy!g^SJ*vEH3q9NB@q+t3M.tB8mIO6g9yx^mpQ8p\\tx@Auf3dhtM-Asy%i\\St6BDA%e(OF4Gut,m!;t3Vvt,i4xI8FpH@x4t23nIE3x-uN3nJt/i5MN3uut!s(tw@AJC!y9tN@mCu?i4zO!mEz3x-CM3pyJ,i^tQ,vsB3m*tM@muu^C4ut1$pS8e^tI/qnt6s)Fx3pHu6o4CNJmEL3b'Nt6nDt5i4wL4pAy7d6St,nDx1d5ww@EtC!k4NI3BJB8v*;t3YuN3q9tL8Cuu*d#Hw8mCI%i4t23xDI+d'NtaN3t5i4wL4pAy7/4vO*mYt5i(t6m59t#i#JF8miL8h8CN3vIt!s(tr\\BIN3t9IJ/rnn3A#Hb*mtI3m(;")
The same technique finds the key length is 9, the offsets are (57 20 20 56 13 16 20 56 4), and the plaintext and key are:
(decode s2 '(57 20 20 56 13 16 20 56 4))
This is basically just a bunch of jibberish text, though certainly able to be read, so that chneukirchen may test out this encryption method.  If he succeeds  well done!  I sincerely congratulate him.  If he does not  I ask that at least he not continue to make fun of this cipher which is so easy a "7 year old" could crack it, or "it can be cracked by hand" according to others.  Let me repeat once more  I know it CAN be cracked, but I bet MOST people (reddit is not "most people") won't do it.

(each x '(57 20 20 56 13 16 20 56 4) (pr (m x)))
4tt3mpt3dnil
This shows how Arc can be used as a tool in simple cryptanalysis, and also shows that homegrown cryptography can be very weak. If you're interested in cryptography, I highly recommend that you read Applied Cryptography. If it's the history of cryptography that interests you, I recommend The Codebreakers.