Hi Artyom,
Artyom Bologov <xxxxxx@aartaka.me> writes:
> Here are my chaotic comments on SRFI 284:
Thank you! They are very useful!
>> If you don’t want to name the procedure, adding types may not be
>> useful.
>
> Additional benefit of such code is that malformed JSON (say, with null
> rating) will inevitably raise type errors.
That’s a strong argument, because the goal of this SRFI is to provide a
typed API boundary, and a walker over a JSON file is clearly an API
boundary.
I now refactored the code to make lambda easy – and it actually works
nicely.
Thank you!
>> It works well together with the checks for data defined in SRFI 253
>> and SRFI 273.
>
> It would be nice if the document linked to respective SRFI documents
> when mentioning them. It’s the cheapest way to ensure the reader is not
> lost in all these opaque integers. I mean, SRFI-2X3 is pretty much my
> trademark now, but these are still unclear.
I agree (and now did that).
> On a related note: can you add IDs to the document sections so that
> hypertext people like me can link to the respective sections for
> reference purposes?
Gladly – the h2 sections have ids, but h3 ones didn’t. Fixed.
https://github.com/scheme-requests-for-implementation/srfi-284/pull/1
>> Different from define-checked in SRFI 253, define-typed does not
>> interleave types with arguments, but provides them as second list
>> after the arguments. This keeps the procedure arguments easier to
>> grasp at a glance as long as they all fit on a single line […]
>
> I have settled on a mix of parens and square brackets for checked
> procedures, as seen in the above lambda-checked and in this:
>
> (define-checked (read-x-if [block-start list?] [p port?])
> #| ... |#)
>
> This is _slightly_ non-portable, but reads much easier.
The reason I don’t want variable names and types interleaved isn’t from
SRFI 253, but from seeing types obscure the variable names in Typescript
and Python.
In production code I’ve seen function headers go to fully unreadable
when argument names got interleaved with ExceptionallyPreciseTypeNames,
but for the point I’m making here, the official documentation of both
systems (which shows their best cases) already suffices.
Examples, each with types, then without:
Typescript from https://www.typescriptlang.org/docs/handbook/2/functions.html#specifying-type-arguments
function filter1<Type>(arr: Type[], func: (arg: Type) => boolean): Type[] {
function filter1(arr, func) {
Python from https://docs.python.org/3/library/typing.html#abcs-and-protocols-for-working-with-i-o
def read_and_write(reader: Reader[str], writer: Writer[bytes]):
def read_and_write(reader, writer):
Try to see the argument names at a glance while reading the code.
Highlighting helps a bit, but not enough for me.
In the Python examples, they actually switch to to a different argument
style in the only case I saw where they have a complex type on the first
argument, so I think that problem is something that actually hits people
during programming:
Python from https://docs.python.org/3/library/typing.html#annotating-callable-objects
def async_query(on_success: Callable[[int], None],
on_error: Callable[[int, Exception], None]) -> None:
def async_query(on_success, on_error):
Once you have enough arguments to justify split lines style, this way
actually becomes nicer to read than splitting arguments and types (so
that’s where define-checked works better than define-typed), but with
fewer arguments I feel like adding types inline obscures the variable
names. And these are what I need while working on a procedure or just
taking a quick idea what arguments it takes.
That said: this switch between styles should actually be possible while
evolving a program, because define-checked and define-typed should be
compatible (I do not see any incompatibilities where one would block the
other).
And when introspection is implemented via SRFI-283, and we manage to get
both define-typed and define-checked compatible with that, then mixing
these shouldn’t even hurt the editing experience.
>> The argument types and the return type are separated by ->.
>
> May I suggest => instead? It’s already used for threading cond / case
> and SRFI 273 return type checks, so it makes sense to re-use it instead
> of adding new auxiliary syntax.
Can that get into trouble when a lambda as type predicate uses =>, i.e.
in cond?
I tried that in Guile and it seems to work.
However the only language I found that uses => to mark return types is
Typescript, and there only in type declarations for functions as
variables, not on functions themselves. Python, Rust, Haskell, and
others use ->.
On the other hand, in define-typed this is inside the type definition
context, so it may be clear that it’s about types.
But I think there’s a reason why they use -> and not =>: it’s that in
Calculus A → B is how a function mapping from one domain to the other is
written:
https://en.wikipedia.org/wiki/Glossary_of_mathematical_symbols#Calculus
> A → B denotes a function with domain A and codomain B. For naming
> such a function, one writes f : A → B, which is read as "function f
> maps any from set A into set B", or f maps A to B " .
So I think -> is the right symbol for separating argument types from
return types.
>> The form define-typed* supports named and optional arguments, while
>> define-typed only supports positional arguments, because supporting
>> named and optional arguments incurs a cost in runtime and
>> implementation complexity.
>
> I’d also have inserted a note on non-portability of named / optional /
> keyword argument here. Highlighting the safe standard status of
> define-typed is beneficial I think.
That’s a good point. I did so.
> Also, it’s a bummer that rest arguments are not supported. These are
> quite useful and prevalent!
They are, yes. But I found no way to represent them cleanly while also
having the return types after the argument types, because
(a b c . d -> e)
is not a valid form.
And I don’t want to split the return type from the arguments.
For define-typed* it would be possible to set a rest argument checking
procedure via #:rest, but this check procedure then also receives all
arguments defined via #:key.
Not perfect, but it at least provides the possibility.
>> The implementation for Guile is as fast as manually added type guards
>> at the start of the body as described in the article Optimizing Guile
>> Scheme. This makes define-typed an easier way to speed up hot inner
>> loops, as long as the predicates give the compiler the needed
>> information.
>
> I could’ve been snarky and suggested that we collaborate on improving
> define-checked that’s already in Guile 😜 But I don’t want to do that,
> because your work is important and useful too!
I’d have liked to do that, but it provides a lot more tools than I want
to provide with define-typed.
Also I hope that the implementation of define-typed already helped speed
up define-checked. The methods I used to get it fast are all there.
> In particular, I see aesthetic value in define-typed, because you’re
> reasonable in claiming that types separated from names might be more
> readable. I don’t necessarily agree with that, but I program in C, so my
> opinion can be safely discarded here 🤣
C actually was a big influence for me – more exactly: sph-sc
https://github.com/sph-mn/sph-sc#functions
It’s a way to write C in Scheme-Style:
(define (myfunction a b) (int char void)
"a description of this function"
(return 1))
sph-sc is the reason why the implementation also supports the style
(define (name a b) (ret? pred-a? pred-b?)
…)
It’s just that during usage I caught myself make more mistakes in that
style ("which one is the return type?") than in the style
(define (name a b) (pred-a? pred-b? -> ret?)
…)
that I mostly added because people on IRC asked for it and I realized
that I could hack the syntax rules to support both at the same time.
>> The return types can be either a single predicate for a single value,
>> for example string?, a form with multiple predicates for multiple
>> values, one per value, for example (string? string?), or a form with a
>> single predicate that receives all return values as list, for example
>> (all-string?).
>
> This is dangerous. What if I wanted to use something like
>
> (cut memq <> '(input output input-output))
>
> as my return type check? Would be recognized as multiple value checks
> instead of a single value check, purely because it contains parentheses?
> SRFI 253 / 273 avoid that ambiguity by always requiring
> parentheses. Much like standard let and cond requiring them. Sure, it’s
> ugly and inconvenient at times. But at least it won’t break in horrible
> ways.
You’re right – thank you for catching this!
The pattern wasn’t structurally clear.
But there’s a way to fix this: for multiple return types have the ->
inside the list of return types:
(define-typed (foo a) (string? -> string?)
a)
(foo "")
(define-typed (foo a) (string? (-> string? string?))
(values a a))
(foo "")
Now these work:
(define-typed (foo a) (string? -> (λ (x) (string? x)))
a)
(foo "")
(define-typed (foo a) (string? (-> (λ (x) (string? (car x)))))
a)
(foo "")
(define-typed (foo a) (string? (-> (λ (x) (string? x))(λ (x)(string? x))))
(values a a))
(foo "")
> On a related note: the format of argument checks in not fully specified
> as far as I can see. Can you specify these too?
I wrote a line about these now.
> Another note: maybe write out the full grammar of these in the document?
> It’s fun to reverse-engineer behaviors from syntax-rules, at least for
> me—but not for everyone I think.
I’m not well-versed in writing grammars, but I would gladly integrate
one. I can’t expect you to write it, but I’d be grateful if you did …
> All in all, this is all useful work, so thanks for using SRFI process to
> gather feedback on it. The document is a pleasurable read too!
Thank you!
And thank you very much for your feedback.
> Best of love,
Same to you!
Arne
--
Unpolitisch sein
heißt politisch sein,
ohne es zu merken.
https://www.draketo.de