[MLton] Callback to function pointer?
Wesley W. Terpstra
wesley@terpstra.ca
Fri, 15 Jul 2005 03:15:20 +0200
On Thu, Jul 14, 2005 at 08:03:21PM -0400, Matthew Fluet wrote:
> Syntax | Elaborated type
> ------------------------------------|---------------------
> _address "symbol" : ptrTy; : ptrTy
> _store * : cbTy; : ??? * cbTy -> unit
> _store "symbol": cbTy; : cbTy -> unit
> _import * : ffiTy; : ??? -> ffiTy
> _import "symbol": ffiTy; : ffiTy
> _export "symbol": ffiTy; : ffiTy -> unit
>
> There is also the incongruity that the annotation on _store/_export does
> not correspond to the type of the elaborated expression, but rather a
> particular component of the type.
So, you are suggesting that the stuff after the ':' should be exactly the
type which is returned? That makes sense, I didn't think of it that way.
So, here's an example of every case:
val add = _address "symbol" : MLton.Pointer.t;
val set = _store "symbol" : int -> unit;
val imp = _import "symbol" : int;
val imp = _import "symbol" : bool * int -> int;
val set = _store * : MLton.Pointer.t -> int -> unit;
val imp = _import * : MLton.Pointer.t -> int;
val imp = _import * : MLton.Pointer.t -> bool * int -> int;
val exp = _export "symbol" : int -> unit;
val exp = _export "symbol" : (bool * int -> int) -> unit;
Personally, I think the above interface is a lot simpler than the one you
propose below. There's only four keywords, each does exactly what it says.
Perhaps the _import could be globally renamed as _fetch, if you prefer.
The distinction between _import/_export for a function and a value
doesn't really lead to confusion; you can imagine that both operate at
the application, and you would not make a mistake. Sure, _import of a
function is evaluated at link time, but since it is unchanging, from
the user's point of view, it could be as well be when applied.
Amusingly, it might even make sense to use type inference here:
val add : MLton.Pointer.t = _address "symbol"
val set : int -> unit = _store "symbol"
val imp : int = _import "symbol"
...
> I'll also point out that the MLton.Pointer structure does have an
> advantage over _store*/_import* in that the MLton.Pointer functions allow
> for an offset from the base pointer, giving efficient access to C arrays.
However, because you can use MLton.Pointer.add, you can just add to the
pointer, getting a new pointer, and then use that for _import. Surely any
difference here is just a local optimization? ;-)
> I'm happy to see _store added, but I feel that with _store it makes more
> sense to make _fetch for C-base types and restrict _import to function
> types. Following that design, I would continue to leave _export restricted
> to function types.
I see that there is a distinction in the implementation of _import/_export
between arrow and non-arrow types, but why does this distinction matter to
the user? Does it need to be visible beyond the type signature?
Give me an example of how I might shoot myself in the foot.
> I admit that leaves _import * as somewhat of an outlier; it is a
> relatively new feature and I could imagine giving it a distinguished
> primitive.
What do you mean an outlier? _store * also uses a pointer.
Or do you mean to omit _store *?
> Finally, I recognize that this prevents declaring and allocating space for
> an object of C base type from ML. I'm interested in a real motivating
> example where this behavior is really necessary.
A good point.
> The only reason to export in the first place is to interface with C, and
> if you already have C code lying around why not declare the objects there.
Another good point.
However, sometimes the C code is a library you are wrapping.
If it is possible to get away with no C other than linking the library, I
think this has some benefit, maybe not much.
> So, my more radical proposal would be the following, adopting the
> convention of "cfTy" as a ground type which expands to a type equivalent
> to a function from a tuple of C base types to a C base type.
>
> Syntax | Elaborated type
> ------------------------------------|---------------------
> _address "symbol" : ptrTy; : ptrTy
> _fetch * : ptrTy -> cbTy; : ptrTy -> cbTy
> _fetch "symbol" : cbTy; : cbTy
> _store * : ptrTy * cbTy; : ptrTy * cbTy -> unit
> _store "symbol": cbTy; : cbTy -> unit
For symmetry, why not:
_store * : ptrTy -> cbTy -> unit; : ptrTy -> cbTy -> unit
_store "symbol": cbTy -> unit; : cbTy -> unit
I see these benefits:
1. * means prepend a "ptrTy ->" in all cases
2. the type after the ':' is always the type of the expression
3. _store * when curried with _address "sym" is the same as _store "sym"
(like _fetch * curried with _address "sym" = _fetch "sym")
> _iccall : ptrTy -> cfTy; : ptrTy -> cfTy
> _import "symbol" : cfTy : cfTy
> _export "symbol" : cfTy : cfTy -> unit
again:
_import * : ptrTy -> cfTy : ptrTy -> cfTy
_import "symbol : cfTy : cfTy
_export "symbol": cfTy -> unit; : cfTy -> unit
The same reasons as before.
> And if declaring a C object from ML is truly necessary
> _declare "symbol" : cbTy; : unit
> _declare "symbol" : cbTy -> ptrTy; : ptrTy
I dislike both of these.
My complaints are:
1. the type after the ':' doesn't relate to the expression type
2. this diverges from how _export gives you a 'setter'
I still am not really convinced that a user needs to see _import and _fetch
as two different things. MLton does fine now with them both in one, and I
would argue it's more simple and clear from a niave point-of-view.
I also don't see the need to distinguish _declare and _export.
They can both return a symbol 'setter', ie:
_export "foo" : (int -> int) -> unit;
_declare "foo" : int -> unit;
At this point, I would ask, why not call them both _export?
The semantics are perfectly clear, imo.
--
Wesley W. Terpstra