ruricolist / serapeum

Utilities beyond Alexandria
MIT License
422 stars 42 forks source link

New defun* macro for type declaration #40

Open Ambrevar opened 5 years ago

Ambrevar commented 5 years ago

Common Lisp has a nice (gradual) type system with support for algebraic data types.

However declaring function types can be cumbersome:

(declaim (ftype (function (ARG-TYPES) RETURN-TYPES)) FUNCTION-NAME)
(defun FUNCTION-NAME ...)

Besides the repetition of the function name and the keyword arguments (if any) is prone to typo errors.

It'd be nice to have a defun* macro where we can specify the types inline, e.g.

(defun* foo ((arg1 :string) &key ((arg2 :integer) default-value)) (:integer)
  "docstring"
  ...
)

Thoughts?

This could be done together with #38.

ruricolist commented 5 years ago

Have you looked at the arrow macro (->)? It does repeat the function name but is fairly compact.

ruricolist commented 5 years ago

Now that I'm at a keyboard -- I've actually attempted to design such a macro in the past, but was never happy with the syntax. In particular specifying types for optional and keyword arguments is very cumbersome -- with an argument like ((:keyword var) optional var-supplied-p) there's no obvious place to put the type. (It might be relevant that the designers of CLOS never attempted to allow dispatching on non-required arguments.)

You may know this already, but it's worth noting that in CL the declaimed ftype of a function and the internally-declared types of its arguments are orthogonal -- the ftype is information for the compiler about how to compile calls to the function (roughly equivalent to wrapping the around the call) but, as I understand it, it doesn't affect how the function itself is compiled. And declaiming ftypes is not always a good idea -- SBCL in particular trusts ftypes blindly, so if your declared types are too broad you lose the benefits of type inference.

That said, I'm happy with ->, but I'll think about it.

Ambrevar commented 5 years ago

Thanks for considering it! :)

My above syntax suggestion was a completely random sketch and I understand it might be difficult to get right. Anyways, I would still consider it an improvement if we could get such a syntax right for most cases if not for all cases.

For the edge cases that wouldn't work, we could still fall back on (declaim (ftype ...)).

You may know this already, but it's worth noting that in CL the declaimed ftype of a function and the internally-declared types of its arguments are orthogonal -- the ftype is information for the compiler about how to compile calls to the function (roughly equivalent to wrapping the around the call) but, as I understand it, it doesn't affect how the function itself is compiled.

This is interesting but do you think this diminishes the usefulness of ftype? As I understand it, we precisely want to do type checking on the function calls, I don't intend to change how a function is compiled with ftype.

Although I read in other places something that may contradict your claim: adding type information allows the compiler to optimize the function definition. I haven't looked at the disassembled code myself though.

And declaiming ftypes is not always a good idea -- SBCL in particular trusts ftypes blindly, so if your declared types are too broad you lose the benefits of type inference.

I'm not sure I understand the problem here. The user chooses the type declaration, so it's up to them whether they are broad or not. What did you mean?

ruricolist commented 5 years ago

I was under the impression that, on SBCL, the ftype declaration would override the inferred type, so you might end up with a less precise type -- e.g. if SBCL inferred a function returns (simple-array character (*)), but you specified string, you would get worse results from the less specific type. But I can't actually make this happen, so it may be a nonissue.

SBCL uses the ftype declaration in compiling the function, but CCL does not.

ruricolist commented 5 years ago

Syntax mock-up:

(defun-ftype foo (arg1 &key (arg2 default-value))
  (-> (string &key (:arg2 integer)) integer)
  "docstring..."
  ...)

Separating the arguments and the types adds a little verbosity, but the macro can check that they match, so typos aren't a problem.

Ambrevar commented 5 years ago

That could work.

Name: what about defun*? It's fairly conventional and shorter. Otherwise the f in ftype might be redundant with the defun, so we could have defun-type (or defun-typed?) instead.

Verbosity: I'm still leaning towards embedding the type in the lambda list. What about using (NAME TYPE) all the time?

Example:

(defun* foo (type-less-foo (foo-with-type string) &key ((bar integer) 17))
  ...)

For defmethods, there is no use in changing positional arguments because they already use the (NAME TYPE) syntax. We can still enhance the typing of optional argument and key arguments though.

Thoughts?

ruricolist commented 5 years ago

In my previous experiments I was going to call it defwithsig, after Liskell.

Ambrevar commented 4 years ago

Also see http://quickdocs.org/defstar/.