[e-lang] A When-Catch Problem of Modest Difficulty

marcs marcs at skyhunter.com
Wed Dec 22 14:39:43 EST 2004


The interesting differences between the when-catches do not reveal
themselves properly, I believe, unless you look at some examples of moderate
complexity. To find such examples, I have looked to DonutLab. Programs like
CapDesk and CapEdit fall into a category I would refer to as "barely
distributed": they are almost entirely single-vat systems, with one or two
reasons to go outside the box. 

DonutLab, on the other hand, is by its nature extremely distributed. All the
major services are separate, and donutlab evolves by adding new services,
which are also separate. Eventual sends are the norm, not the exception.
DonutLab gives promise pipelining a workout. And it's the kind of workout
that E must support: if it can't do a distributed system as simple as
donutlab well, then E is not the right tool for seriously distributed
computing.

I have chosen as the first experiment (and probably the last experiment for
a while :-) a problem that should be (must be?) simple, but which really
exercises the distributed computing facilities of E. Here's the scenario:

A User wants to use a Service that requires payment.
The user's account may not be in the same currency as the service.
If the currencies are the same, the user just asks the service about the
price and pays that price from his account to the service to perform its
function.
Otherwise, the user goes to a combio (money changer), specifies the
currencies, specifies the amount he wants in the target currency, and gets
back an "option". An option is a one-time exchanger for this specific
exchange that guarantees a locked-in exchange rate (options expire
eventually if unused). The user retrieves from the option the price he must
pay to get back the right amount of money to pay the service. He then
exercises the option (pays the option price), gets back the money he needs,
and then pays the service to perform its function.

It seems necessary for this example to be a common pattern in agoric
distributed systems. It shouldn't be a big deal. It better not be a big
deal! :-)

Pass-by-reference objects generally work just as well when only promised as
when they are actually fulfilled. There are 2 obstacles planted in the
structure of the example that make us pause and consider what we are doing:
there is an EQ test between two reference objects, and there are a couple of
methods that demand that you send them real integers, not merely promises
for integers (for reasons outside the scope here, I believe it will be the
common pattern in practice to find that, if a method needs an integer, it
will demand a real integer, not a promise, so I have incorporated that into
the scenario, both because it reflects my forecast, and because it gives the
problem the spice it needs to be interesting).

Having completed this example, I conclude that it is probably atypical, so I
also wind up saying, more examples are needed. However, this example is
still interesting, and possibly important, for a non-obvious reason. I tried
to build this example a couple of times with the old when catch. I tried it
for two reasons:

-- it was clearly an important exercise for DonutLab, so I was interested
for purely personal reasons
-- it would make a great example for the E Fundamentals class.

I failed to implement it with the old when-catch. The personal reasons for
implementing it were inadequate to drive me to take as much time and pain as
required, and the pain was sufficiently great, I concluded that it would be
a terrible example for the class.

I predict it will not be obvious, looking at the series of versions of code
below, why this was such a difficult problem that I choked on it. As often
happens, by the time you have solved it, everything is clear. I will make a
small attempt to explain why it was difficult, but only a small one. The
problem was the combination of requirements to think about what had to be
done before entering the when-chain, what could be done before entering the
when-chain, what you were going to find out in the middle of the when-chain
that you really wanted to have done before the when-chain, and the fact that
a part of the when-chain occurs in the else clause of an if statement, which
is really irksome, and the fact that the old when statement was so heavy
weight it magnified all the other problems. Scramble all these issues
together, and the size of the problem appears, at the outset, to be
enormously larger than the amount of code needed to solve it.

The series of versions below are in the following sequence: First an
immediate call version that gives us a "reference version" to see how big a
barrier to entry the other versions represent. Then a brutal, thoughtless,
slavish translation of the immediate call version to a when-chain. Next a
when-chain version that has been fixed up. An "easy-when" version. And an
__when version. 

I will now make the claim that will irritate dean :-) The ability to create
the slavish translation using a when-chain as an intermediate step really
helped me get all the way through the problem. The other really irritating
thing is that, despite all its weaknesses, the slavish when-chain version
would work in the field just fine.

I will now make the observation that will leave dean just chortling up his
sleeve. By the time I was done building the "fixed-up when-chain version",
it no longer used a when-chain. Yup, the fixed up when chain and the
easy-when versions are identical. Since the last remaining nested when after
the fixup lay in an else clause, I couldn't flatten it out: I was still
stuck with explicitly nested when clauses.

I will now make the observation that we should be most concerned about: the
immediate call version is far simpler than any of the others. The reason
appears to be that all variants of "when" impose upon the programmer the
additional burden of constructing intermediate variables for everything.
Look and see what other conclusion you may draw.

The fact that all versions are so much more complicated than the immediate
call version is terrible. The other debates pale in comparison to this.
Perhaps it is a necessary price. But wow, the benefits of improving it would
be tremendous.

code below. 

--marcs

---------------------------------------

pragma.enable("easy-return")
pragma.disable("explicit-result-guard")
pragma.enable("easy-when")
pragma.enable("when-sequence")

# account, service, option, and combio are stubs barely large enough
# to enable the example to run
def account {
    to getCurrency() {}
    to makeOffer(amount :int) {}
}

def service {
    to getCurrency() {}
    to getPrice() {return 3}
    to perform(payment) {}
}

def option {
    to getPrice() {return 4}
    to exercise(hold) {}
}
   
def combio {
    to makeOption(fromCurrency, toCurrency, toAmount :int) {}
}


#immediate call version
# few intermediate variables are necessary
if (service.getCurrency() == account.getCurrency()) {
    service.perform(account.makeOffer(service.getPrice()))
} else {
    def option :=combio.makeOption(account.getCurrency(), 
                      service.getCurrency(),
                      service.getPrice())
    def payment := option.exercise(account.makeOffer(option.getPrice()))
    service.perform(payment)
}

# naive, thoughtless, brutal, slavish when-chained
# created by cranking the syntax mechanically from immediate call
# to when-chain
# The general clumsiness of having to make more intermediate result vars
# makes the getting of the service price "when needed" truly 
# painfully clumsy. We learn that you want to get it early, before you fully
# understand how much you will need it later.
# Currently, you need to put "\" at the end of the when(blah) line
# if you want to put the "and then" symbol "->" on the 
# next line. Can this be fixed?
when (def myCurrency := account <- getCurrency()) \
-> (def servCurrency := service <- getCurrency()) \
-> {if (servCurrency == myCurrency) {
        when (def price := service <- getPrice()) -> {
            service <- perform(account <- makeOffer(price))
        }
    } else {
        when (def servPrice := service <- getPrice()) \
        -> (def option := combio <- makeOption(myCurrency,
                                              servCurrency,
                                              servPrice)) \
        -> (def price := option <- getPrice()) \
        -> (def payment := option <- exercise(account <- makeOffer(price)))\
        -> {service <- perform(payment)}
    }
}

#somewhat thoughtful, evolved event sequenced
# the big change is, we get the servprice at the beginning in
# parallel with the currency brands, solving the problem observed
# in the comments in the slavish when-chain version.
# Fascinating result: the event sequencing has disappeared, i.e., 
# it is now all normal "easy-when" clauses
# the nested when has not been wiped out by event sequencing because
# it is inside an else clause
when (def myCurrency := account <- getCurrency(),
    def servCurrency := service <- getCurrency(),
    def servPrice := service <- getPrice()) \
-> {if (servCurrency == myCurrency) {
        service <- perform(account <- makeOffer(servPrice))
    } else {
        def option := combio <- makeOption(myCurrency, 
            servCurrency,
            servPrice)
        when (def optionPrice := option <- getPrice()) -> {
            service <- perform(account <- makeOffer(optionPrice))
        }
    }
} catch prob {println("service failed: " + prob)}

# Normal easy-when: no additional example is needed, the above is
# already a normal easy-when!


println("finished")
interp.blockAtTop()

# Note: __when version not ever interpreted, probably has syntax errors

# version using __when. The thunk clause is just big enough
# to make it clumsier. I dislike the indentation I have used
# for this more than the indentation I have used in the others,
# perhaps someone else can do better.
# the intermixing of braces and parentheses feels more disturbing
# for me when I try to put on my non-lambda beginner hat (well, 
# actually, it disturbs me even without the beginner hat :-).
# I have used markm's intermediate variable with a when.broken
# for the error handler. Seems ok. Perhaps 3-arg allFulfilled is not
required.
def prob := __when.allFulfilled([def myCurrency := account <-
getCurrency()),
    def servCurrency := service <- getCurrency(),
    def servPrice := service <- getPrice()],
    thunk {
        if (servCurrency == myCurrency) {
            service <- perform(account <- makeOffer(price))
        } else {
            def option := combio <- makeOption(myCurrency, 
                servCurrency,
                servPrice)
            __when(def optionPrice := option <- getPrice()),
                thunk{
                    service <- perform(account <- makeOffer(optionPrice)
                }
            )
        }
    }
)
__when.broken(prob, thunk {println("service failed: " + prob)})
     





More information about the e-lang mailing list