Repository navigation
Description
Found while testing Pyfun main (the 0.10.0 cold Async, #177) against
super-scrabble's network code.
An extern whose callback parameter is written with a plain arrow rejects
every Pyfun function that does anything, and the error gives no hint of the
fix. The fix is an effect variable on that arrow:
extern import asyncio
extern type Reader
extern type Writer
extern type Server
extern serve: (Reader -> Writer -> Async unit) -> string -> int -> Async Server = asyncio.start_server
extern closeConn: Writer -> unit = .close()
let handle r w =
async {
closeConn w
}
let listen = serve handle "127.0.0.1" 7777
error: effect mismatch: cannot unify Writer -> Async unit with Writer ->{io, async} Async unit
--> 13:14
|
13 | let listen = serve handle "127.0.0.1" 7777
| ^^^^^^^^^^^^
Same message on 0.9.0 and on main (18a4a31). Writing the callback arrow as
(Reader -> Writer ->{e} Async unit) checks clean, works for any handler
body, and keeps the _pf_run wrapping #177 gives an Async-returning
callback.
The tell
The mismatch is real and the checker is right to report it: an async { }
block performs async where it is written (DESIGN.md §4), so handle is
Reader -> Writer ->{io, async} Async unit, and declared effects are exact.
The message has three problems, though:
- It says what failed but not where the closed set came from. A user who
wroteReader -> Writer -> Async unitreads it as "my handler isn't
async", which it plainly is. - The obvious fix, copying the inferred set into the extern
(->{io, async}), type-checks, but it is wrong: it only accepts handlers
whose effect is exactly{io, async}. A handler that only awaits
({async}) is then rejected the same way. I made exactly this mistake
before finding->{e}. - The caret covers
serve handleinstead ofhandle, the argument whose
effect didn't fit.
->{e} is the documented answer (DESIGN.md §4 "effect variable,
extern-only"; lesson 12; examples/interop/README.md), but nothing points a
user from this error to it.
Why it happens
src/types/mod.rs:9439: the(Ty::Fun, Ty::Fun)arm ofunifybuilds
the message onceunify_efffails. It sees only two types and knows
nothing of an extern or an application.src/types/mod.rs:8774:infer_apply_plainunifies the whole function
type againstarg_ty -> resultat the application's span. A bad argument
is therefore reported onfunc arg, and nothing at this level adds
context.operator_hint(src/types/mod.rs:6673) is the existing pattern
for wrapping a generic error with guidance, throughmap_err.src/types/mod.rs:3380-3395: the extern's scheme carries its effect
variables ineff_vars. A closed set on a callback arrow can only come
from a declaration, because a Pyfun-defined higher-order function infers
an effect variable for its parameter's arrow. So a closed callback effect
that fails to unify is a strong sign of a declared signature that wants
->{e}(in anextern) or the exact set spelled out (in atype, where
effect variables are an error).
What I would like
When an effect mismatch comes from applying an argument whose arrow meets
a closed, declared arrow, say so and name the effect variable, pointing at
the argument:
error: `handle` performs {io, async}, but `serve` declares this callback as `Writer -> Async unit`, which performs nothing
--> 13:20
|
13 | let listen = serve handle "127.0.0.1" 7777
| ^^^^^^
= help: to accept a callback whatever it does, give the arrow an effect variable in the extern: `(Reader -> Writer ->{e} Async unit)`
The wording is only a sketch. The two parts that matter are the pointer to
->{e} and the warning, implied or stated, against pasting in the inferred
set.
Activity
types: a callback that misses a closed arrow points at ->{e} (#181)