I'm pleasantly surprised to see some promise in Bicicleta's current
concrete syntax.

A call to collect
-----------------

So consider this call to prog.collect:

    prog.collect(f: "(" + f.item + ")", for_each="cthulhu", 
        where=(f.item == "u").not)

This returns the list ["(c)", "(t)", "(h)", "(l)", "(h)"].

Having just explained this code to several people, I am now aware that
it is not a marvel of clarity, so I will start by explaining what this
does, and some of its internal workings.

It makes a list of "(" plus a character plus ")", for each character
of the string "cthulhu", as long as the character is not "u".

There are three arguments to 'prog.collect': 'arg1', which is "(" +
f.item + ")"; 'for_each', which is "cthulhu"; and 'where', which is
(f.item == "u").not.  'f' is a name used to refer to the collect
expression as a whole.

It's a little odd to have "arguments" whose value depends on the
function they're being passed to, so I should explain that they're not
really arguments, but methods.  Bicicleta doesn't really have
arguments.

This expression is evaluated by evaluating prog.collect (the results
of the 'collect' method on the variable 'prog', which conventionally
refers to the top level of the program), deriving a new object from it
by overriding the 'arg1', 'for_each', and 'where' methods described
above, and then calling the '()' method on the resulting object.  If
we didn't want to do this last step, we could write

    prog.collect {f: "(" + f.item + ")", for_each="cthulhu", 
        where=(f.item == "u").not }

which is just the object (the same object named by f).

Prog.collect evaluates 'arg1' and 'where' in a series of objects with
different 'item' methods, which return successive elements of the
'for_each' value, in order to construct the list.  'for_each' can be
any kind of sequence, not just a string.  

Mechanics of 'collect'
----------------------

Here's the full code for prog.collect:

    # Collect: map+filter, in a more listcompy shape.
    # WORDY! Uck!  Avoids prog.if because prog.if depends on collect.
    collect = {collect: arg1 = collect.item,
        cursor = collect.for_each.cursor
        item = collect.cursor.item
        where = prog.sys.bool.true
        next = collect { cursor = collect.cursor.advanced }
        '()' = collect.cursor.empty.if_true(
            then = collect.cursor
            else = collect.where.if_true(
                then = collect.arg1 @ collect.next()
                else = collect.next()))
    }

'arg1' defaults to collect.item (the same as f.item in the call
earlier); 'item' is defined as collect.cursor.item; 'cursor' defaults
to collect.for_each.cursor; 'where' defaults to true; 'next' is a
method that returns the same object, except with a new value for
'cursor' (giving it different values for 'item', 'arg1', 'result', and
maybe 'where'); and '()' either returns an empty list (if 'cursor' is
empty) or either collect.next() or collect.arg1 @ collect.next(),
depending on whether 'where' is true or false.  '@' is the cons or
list-construction operator.

So, in the call above, initially 'item' is "c", 'arg1' is overridden
to be "(c)", 'where' is overridden to be true, and the cursor is not
empty, so we end up with '()' returning "(c)" @ collect.next().  In
collect.next, the cursor is advanced to point to the next item, 'item'
is "t", arg1 evaluates to "(t)", and '()' evaluates to "(t)" @
collect.next(), so the top-level '()' evaluates to "(c)" @ ("(t)" @
some other stuff), and so it goes on.

In the case where item is "u", because cursor is pointing to the
beginning of "ulhu", 'where' evaluates to 'false', so the
collect.where.if_true expression returns collect.next(), ignoring the
"(u)" that 'arg1' would compute.

Eventually, the cursor is empty, and '()' just returns that (empty)
cursor, which serves to terminate the list; probably I should return
prog.sys.nil instead.  In those cases, it doesn't matter what 'item'
and 'arg1' evaluate to, even if they evaluate to errors, because
they're not being returned.  Likewise in cases where 'where' evaluates
to false --- '()' just returns collect.next() and bypasses 'result'
and 'arg1' entirely.

I anticipate that utilities like "collect" will be able to keep
explicit recursion confined to tiny corners of the system libraries
and to problems that really benefit from recursion.

Why I Think This is Cool (Bicicleta, Python, OCaml, and Squeak)
---------------------------------------------------------------

Loops are confusing and complicated, especially in functional
languages that implement them by recursion.  A lot of loops can be
subsumed by simple one-variable list-comprehensions, often with
improved comprehensibility and brevity.

For this reason, Python and Haskell have list-comprehension syntax
built into the language, so that you can write (in Python):

    ["(" + item + ")" for item in "cthulhu" if item != "u"]

Which gives you the same result as the Bicicleta expression:

    prog.collect(f: "(" + f.item + ")", for_each="cthulhu", 
        where=(f.item == "u").not)

(The .not is just because I haven't implemented != for strings yet,
because right now my !=-derived-from-== magic is locked up in a
numeric class from which I should factor out a "comparable".)

To my eyes, the Python version is more readable, but the difference is
not enormous; they are closer to one another than either is to

    rv = []
    for item in "cthulhu":
        if item != "u": rv.append("(" + item + ")")
    # now do something with rv

If I added special syntax to Bicicleta to do list-comprehensions, I
coule eliminate the "prog.collect" part:

    [f: "(" + f.item + ")", for_each="cthulhu", where=(f.item == "u").not]

But even without special syntax, I think it's better already than
Smalltalk:

    'cthulhu' asArray select: [:c | c ~= $u] 
        thenCollect: [:c | '(', c asString, ')']

Or OCaml:

    let list_of_string string = 
      let rv = ref [] in 
        for i = String.length string - 1 downto 0 do 
          rv := string.[i] :: !rv 
        done ; 
        !rv
    in
    List.map (fun item -> "(" ^ String.make 1 item ^ ")") 
      (List.filter ((<>) 'u')
        (list_of_string "cthulhu")) ;;

Although, to be fair, a lot of the verbosity in the Smalltalk and
OCaml versions has to do with excessive incompatible types (lists,
strings, arrays, characters) rather than the clumsiness of the
non-list-comprehension syntax.  But consider in the ideal case, where
those incompatibilities don't exist:

    ["(" + item + ")" for item in "cthulhu" if item != "u"]
    'cthulhu' select: [:c | c ~= $u] thenCollect: [:c | '(', c, ')']
    prog.collect(f: "(" + f.item + ")", for_each="cthulhu", 
        where=f.item != "u")
    List.map (fun item -> "(" ^ item ^ ")") 
      (List.filter ((<>) 'u') "cthulhu") ;;

With corresponding bits rearranged to more or less line up:

             "(" + item + ")" for item in "cthulhu"  if item != "u"
    thenCollect: [:c | '(', c, ')']       'cthulhu' select: [:c | c ~= $u] 
       "(" + f.item + ")",       for_each="cthulhu", where=f.item != "u"
   List.map (fun item -> "(" ^ item ^ ")")"cthulhu"  (List.filter ((<>) 'u')

This suggests that there is some brevity benefit that attaches
specifically to the practice of defining new methods in the form
f.item != "u", rather than creating anonymous functions, even such
lightweight functions as Squeak has [:c | c ~= $u].  Currying, such as
((<>) 'u') is even shorter, of course.

It turns out that you can use currying in Bicicleta similarly; you can
write "u".'!=' to mean {op: '()' = "u" != op.arg1}.  In this case,
though, collect is defined to expect a method definition on itself,
not an anonymous function that it would have to pass something to
explicitly.

Reply via email to