There were an awful lot of embarrassing bugs to work out, like parameters out of order and stuff, and the fact that it was easy to implement 'fail-aware-apply using 'failfn after all, but it they're squashed. Woo! I even put the 'fact example in there, but I'll probably move that into a separate unit test.
One thing this means is that I should be able to port a lot of my Penknife draft's utilities into Arc. It won't have Penknife's hygiene support, and the module system will still be horribly limited, but Lathe has the advantage of already having a self-ordering rule library and, you know, actually existing. ^_^
Actually, it also means I might be able to reuse my existing Penknife core and hack this onto it. Nahh, the core in my draft is already far better designed for extensibility, even if it doesn't work....
I don't see any difficulty in translating fail parameters to Wart, only complexity. :-p What do you think?
For what it's worth, I've abstracted away the parameter simulation and put it in dyn.arc, which is where Lathe's dynamic box API is kept. Now you can define your own "secretargs" with default values and have it automatically seem like they're passed to every function in the language. It's not necessarily pretty; the point is to build things like 'failcall on top of it.
; Surprise, this code has been tested!
(= lathe-dir* "your/path/to/lathe/arc/")
(load:+ lathe-dir* "loadfirst.arc")
(use-rels-as dy (+ lathe-dir* "dyn.arc"))
(= secret1 (dy.secretarg 4))
(= secret2 (dy.secretarg 5))
(= secret3 (dy.secretarg 6))
(dy:call-w/secrets (dy:secretarg-fn (a b c)
(list a b c d e (dy.secretargs)))
(list (list secret1 9)
(list secret3 200))
1 2 3)
=> (1 2 3 9 5 ((#(tagged ...) 9) (#(tagged ...) 200)))
There's a catch, though, which is that general-purpose function-calling functions like 'apply and 'memo don't propagate secretargs, and there's probably nothing I can do about that aside from hacking on the language core. This is the same kind of compatibility-breaking I brought up when we were talking about keyword arguments.
Fortunately, secretargs are pretty general-purpose--they're pretty much just keyword arguments with non-symbol keys--so things like keyword argument systems can be implemented on top of them. Just make a secretarg that stores a special-purpose keyword map.
I'm not sure if that'll come up in Lathe for a long time, though. For now the only benefit is that it's a little easier to explain 'failcall and 'failfn: they use a non-symbol keyword argument (a secretarg) to communicate, and 'failfn makes that argument into a more natural failsafe by chaining it to an escape continuation.
You seem very excited about this feature, but I'm afraid I'm having trouble understanding what it's about. :-/
I expected that. ^_^;; I kept trying to find ways to emphasize the motive, but I kept getting swept up in minutia.
Is it a sort of base case upon which all functions' extend layers can be built up? What you've written about it so far reminds me of Ruby's method_missing. Is that a related idea?
I think you're on the right track. The point of my fail parameter is to explicitly describe cases where a function is open to extension. Sometimes a function call has a well-defined return value, and sometimes it has a well-defined error value. If it doesn't end in either of those ways and instead fails--by calling its fail continuation--then it's undefined there, and you're free to replace it with your own function that's well-defined in all the same cases and more.
That was the original idea, anyway. It's a little flawed: If another function ever actually catches a failure and does something with it, then its behavior relies on the callee being undefined in certain cases. Take 'testify for instance; if 'testify checks a 'testify-extensions function first but acts as [fn (x) (iso x _)] on failure, then you can't actually extend 'testify-extensions without changing the behavior of 'testify. In fact, if you replace a function with another function that calls the first one ('extend as we usually know it), then you can no longer extend the first one without impinging upon the new one. But in practice, I think it'll work out well enough.
The alternatives ways to deal with extension I see are a) encoding defined-ness in the return value using a wrapper type, and b) throwing an exception. In a language where almost everything's extensible, to have to unwrap every return value would be madness. As for exceptions, it's hard to tell whether the exception came from the function you called or some function it called. In fact, I was actually trying figure out how to have function calls take ownership of exceptions when I finally realized there needed to be a per-function-call ID, generated on the caller's side; this idea refined a little to become fail parameters.
I know, I digressed a bit again. XD Did it work for you this time?
Also, in my Penknife draft, I'm finding myself much more comfortable using failures rather than exceptions, letting the failures become exceptions when something's called without 'failcall. Failures form a natural tree of blame; this rulebook failed because all these functions failed, and this function failed because this other function failed....
The need to write natural-language error strings diminishes to just the base cases; the rest is pretty-printing and perhaps even a bit of SHRDLU-style "Why did you decide that?" interactive tree-drilling. ^_^
Arc's known for having rotten errors 'cause it shirks the error-checking code; with fail parameters, awesomely comprehensive error reports can be made in almost a single token.