SRFI 198: clear enough for next draft? Lassi Kortela (02 Aug 2020 11:35 UTC)
Re: SRFI 198: clear enough for next draft? hga@xxxxxx (02 Aug 2020 12:22 UTC)
Re: SRFI 198: clear enough for next draft? Lassi Kortela (02 Aug 2020 13:21 UTC)
Re: SRFI 198: clear enough for next draft? hga@xxxxxx (02 Aug 2020 15:04 UTC)
Re: SRFI 198: clear enough for next draft? John Cowan (06 Aug 2020 03:49 UTC)
Re: SRFI 198: clear enough for next draft? hga@xxxxxx (12 Aug 2020 16:56 UTC)
Re: SRFI 198: clear enough for next draft? John Cowan (06 Aug 2020 03:22 UTC)
Re: SRFI 198: clear enough for next draft? hga@xxxxxx (12 Aug 2020 16:33 UTC)
Re: SRFI 198: clear enough for next draft? John Cowan (05 Aug 2020 06:13 UTC)
Re: SRFI 198: clear enough for next draft? hga@xxxxxx (05 Aug 2020 13:11 UTC)
Re: SRFI 198: clear enough for next draft? Lassi Kortela (05 Aug 2020 15:23 UTC)
Re: SRFI 198: clear enough for next draft? hga@xxxxxx (05 Aug 2020 15:59 UTC)
Re: SRFI 198: clear enough for next draft? Lassi Kortela (05 Aug 2020 16:17 UTC)

Re: SRFI 198: clear enough for next draft? Lassi Kortela 02 Aug 2020 13:21 UTC

>> The foreign-error type is an abstract data type (ADT).
>
> Must we *require* it be an ADT?  foreign-error? can be nearly
> sure an object that's a plist is a foreign-error.

What you're thinking of is an opaque data type -- one where you can't
look under the hood at the implementation using standard tools. For
example, record types, hash tables and CLOS classes are opaque in this
sense (there are various ways to look inside them, but AFAIK none of
those ways are standard).

An abstract type is not the same thing. An abstract type can be
implemented as any opaque or transparent type. The choice is up to each
implementor.

The point of abstraction is to only specify the interface -- the
implementation doesn't matter, and different implementations are
interchangeable or can coexist as long as the interface stays the same.

SRFI 174 (POSIX Timespecs) is a good example. The timespec type is an
abstract datatype. It can be implemented as a record type (opaque), a
pair i.e. a cons cell (transparent), or something else.

A valid implementation of foreign errors, using a transparent (magic .
plist) representation, would be something like this:

(define foreign-error-magic (make-string 10 #\.))

(define (foreign-error? obj)
   (and (pair? obj) (eq? foreign-error-magic (car obj))))

(define (make-foreign-error . plist)
   (cons foreign-error-magic plist))

(define (foreign-error-ref ferr property . args)
   (assume (foreign-error? obj) "not a foreign error" ferr)
   (let ((value (plist-ref/default (cdr ferr) property #f)))
     (if (procedure? value) (apply value args) value)))

One Scheme implementation can also support more than parallel
implementation of the same abstract datatype, e.g. Gauche:

(define (foreign-error/plist? obj)
   (and (pair? obj) (eq? foreign-error-magic (car obj))))

(define (foreign-error? obj)
   (or (foreign-error/plist? obj)
       (and (condition? obj)
            (condition-has-type? err <system-error>))))

This is like inheritance or interfaces in object-oriented programming,
or union types in functional programming, and is one of the benefits of
an ADT.

>> (foreign-error? object) => boolean
>>
>> (foreign-error-ref ferr symbol) => object
>
> Leaves out the object being a lambda, with convenience additional
> arguments being the lambda's arguments to hand to it.  Not required,
> but why make more work for the consumer of an error?

True, I'd also vote to support lambdas as discussed.

> Which brought up my first thought of the day: localization conventions
> will necessarily have to be documented somewhere like in this SRFI,
> but any lambdas that take arguments should have a requirement to be
> documented in the error object (which may be localized, hence the
> above requirement its conventions be documented).
>
> That will be a duplicate of information in the registry if the creator
> of an error-set collection is diligent, but we can't expect users of
> libraries using SRFI 198 to find this out by looking at the registry,
> which also might not be in an easily digestible for humans format.
> (Of course, we could do something about the latter....)

We should decide on a standard way to do localization for all foreign
error objects. It'll be quite confusing if errors from different sources
have different conventions. Also most FFI binding / network protocol
implementation writers are not localization experts, so we'll keep
messing it up unless we all use a standard localization approach
approved by an expert like John.

We can make the registry easier to browse for humans using a simple
S-expressions -> HTML script. Arthur is already doing this kind of thing
at <https://github.com/scheme-requests-for-implementation/srfi-common>
to auto-generate much of the <https://srfi.schemers.org> website.

>> (make-foreign-error . plist) => ferr
>>
>> (raise-foreign-error . plist)
>
> Also (raise-continuable-foreign-error . plist) ?  I have no informed
> opinion on using continuable raising, have always used this paradigm
> to exit the program, but the option exists.  And document that both
> must use make-foreign-error, whereas exactly what raise and
> raise-continuable do is totally at the discretion of the implementer.

Somebody recommended in one of the other threads that
raise-continuable-foreign-error is too niche and should be left out. I
agree that this kind of discontinuity can be a bit odd to users (why
have one and not the other) but I'd advocate solving it by omitting
`raise-foreign-error` as well :)

Foreign errors are mostly going to be raised by fairly low-level FFI
bindings and network protocol implementations. That code tends to be
grotty enough anyway that typing (raise (make-foreign-error ...))
instead of (raise-foreign-error ...) will not make it harder to
understand than it already is.

>> - Plist keys should be symbols.
>
> Why not "must" be symbols?  Provides one way to validate the plist.

Good idea, let's do it.

John remarked elsewhere that "it is an error to pass non-symbols as
plist keys" would be the conventionally correct phrasing. "Must" is
meant to talk about things that the SRFI implementor must do, "is an
error" is for things that the SRFI's users must adhere to. If memory serves.

>> - Need to decide whether keyword objects are also accepted and converted
>> to symbols internally.
>
> Does that create a portability issue for libraries using SRFI 198, if
> the user chooses to use keyword objects?

Yes, but that issue can be easily solved by using symbols instead of
keywords in portable code. Making non-portable code portable, it would
be a search-replace operation to turn one into the other.

> Very unfortunately, you had
> to withdraw your keyword SRFI :-( One of the two things I miss the
> most from Common Lisp ).

Thanks for the kind words. It was a mostly pleasure since we mapped so
much ground in that design space and it was such a collaborative effort.
I'm confident the community can design something better in its place.

It's worth noting that keyword objects are a separate feature from
keyword arguments, and way simpler. For the next keyword SRFI, we could
submit a really simple one about a reader directive to support the
existing keyword read syntaxes. There's already SRFI 88 for keyword
objects but not all implementations use the read syntax in it.

>> - Specify what to do in case of duplicate plist keys (after normalizing
>> them all to symbols). First one wins?
>
> That's the general rule?

I'm not sure if there is any general rule.

`assoc` picks the first match from an alist. But the list was
constructed in reverse order from some source, then the last match from
the source list will win. The general expectation is that order of alist
keys doesn't matter, and there are no duplicates, so depending on things
like that is surprising.

> But why not stash duplicates somewhere in
> the error object?  In general we want to preserve information,
> especially when calls to it are malformed, the user should still be
> able to make some sense out of a mess (and of the least likely to be
> caught in testing).  Thus a call to make-foreign-error without an
> 'error-set key will still return an error object, with all the data
> handed to it in it.

Missing keys is a different problem from duplicate keys.

 From an algorithm standpoint, it's easy to specify that all keys are
optional, and missing ones return #f. No matter which data structure one
uses, there's one obvious way to implement that.

It's not obvious how to handle duplicates.

If we required a plist for the internals, then from:

(make-foreign-error
  'set 'errno
  'number 12
  'oops
  'message "Cannot allocate memory")

we could salvage the 'set and 'errno properties using
`foreign-error-ref`. But we could not salvage 'message since 'oops
throws the key-value pairing out of whack. And (foreign-error-ref e
'oops) would return 'message.

Additionally, if `make-foreign-error` refuses unbalanced plists, then
more errors in the usage of `make-foreign-error` are caught early
instead of propagating into the caller of the library who then has an
improper error object on his hands.

Dynamic languages (and dynamic systems, more generally) are filled with
problems like this. It's a matter of endless debate which of them are
real problems and which are just non-issues (meaning any solution would
be worse than the problem it solves).

In this case, I'd favor from simple interfaces: a foreign error object
stores a property-value collection with a get operation. Duplicates and
value-less keys are not well defined across different key-value
implementations (alist, plist, hashtables, trees, etc.) so I would
require the abstract datatype to cope with them.

This is a matter of taste, but taste is also influenced by experience.
In my experience, an interface can't be simple enough. GC is the best
abstraction of all time, and its interface is empty! There is nothing to
call, it simply works automatically. The closer we can get to the empty
interface, the better. In that respect your idea of dropping the
foreign-error:message and other special fields is already better than mine.

The way we simplify interfaces is to make them more restrictive.
Programmers generally hate restrictions of all kinds, and it requires a
lot of unlearning of instinctive mental habits to stop worrying and love
them.

JSON is an excellent abstraction of the collections in various
programming languages. It has almost no features. Many experienced
programmers complain about how few features it has. But it's extremely
reliable and widely applicable precisely because it's so restricted.

> I need to make that explicit in the SRFI specification text, what all
> malformed calls to make-foreign-error will do.  Probably two varieties,
> if a value for the key 'error-set can be extracted, and if not.

It depends on whether we allow malformed plists to make-foreign-error or
not. I agree that we should specify something in any case.

>> Do we need a `raise-foreign-error` in the SRFI? (raise
>> (make-foreign-error ...)) is not substantially more difficult.
>
> We ***really*** want to encourage this paradigm.  Quoting text from
> SRFI 170, the end of SRFI 198's Rationale is:
>
>> Sometimes it will make sense to simply return an error object, but an
>> often preferred method is to raise an exception, as in SRFI 170, in
>> which procedures never return error codes nor use an analogue of the
>> POSIX errno variable to indicate system-call-level errors.
>>
>> Thus procedures can return useful values, and the programmer can
>> assume that if a foreign interface procedure returns, it succeeded.
>> This greatly simplifies the flow of the code from the programmer's
>> point of view.
>
> And we might as well hide the details of whichever system's raising
> from the user of SRFI 198.

Great, we are in agreement :) John, do you concur?