grimme-lab / nlopt-f

Fortran bindings for the NLopt library
Apache License 2.0
28 stars 7 forks source link

Callback function design #16

Open awvwgk opened 2 years ago

awvwgk commented 2 years ago

The callbacks for this bindings are implemented in an ad hoc fashion, with the primary aim to closely match the NLopt API in a Fortran-friendly way. However, this might not be the best/only way to define a callback. A better target for designing the callback for the objective function should be to provide an uniform experience compared to other optimization libraries.

The callback is for this project is defined in src/nlopt_callback.f90. Note that this callback design creates an issue with object lifetimes (see #13).

Related:

ivan-pi commented 2 years ago

Generally speaking, these are the options I'm aware of:

(It would be nice to add some bullet points with their respective strengths and weaknesses.)

The second part of the problem is how to pass any parameters of the function. A few approaches are outlined on fortran90.org in the section Type-casting in callbacks.

Roughly speaking, if we stay within Fortran (no raw C pointers), the options I can recall are:

The last two of these will require the consumer to use a select type construct.

Edit 1: are Fortran procedures only distinguished by TKR, or can it also specialize based upon the callbacks with different interfaces?

Edit 2: ironically, I think this issue was less of a problem in the old days of punch-cards and external subprograms. At that time you would always need to compile your code anyway. Switching to a different callback function or algorithm, just amounted to replacing a deck of cards. The name of the callback procedure would be hard-coded in the algorithm code.

awvwgk commented 2 years ago

I think we should be able to implement several callback mechanism in nlopt-f. Using generic to overload type-bound procedure interfaces should hide most of the logic from our users. This would give us some insight on each mechanism how good they work implementation-wise (especially when round-tripping through C) and how the impact the user-experience.

A few notes:

ivan-pi commented 2 years ago

I'm on the same page. Generally, I find that composition (i.e. procedure pointer in a derived type) involves a bit less effort compared to inheritance. For most serious problems, the overhead from the indirect referencing should be negligible compared to the function evaluation. I also don't mind having a single select type (a small detail I would like to clarify is whether a default section would be a good practice or not; personally I think its not needed, but perhaps would be needed for debugging a deep a deep hierarchy of calls - maybe better if the compiler had an option to do this automatically).

In my old NLopt wrapper, instead of an unlimited polymorphic object for parameters, I expected consumers to extend an abstract derived type:

type, abstract :: nlopt_func_data
end type

But the unlimited polymorphic class involves slightly less effort and is also less restrictive, i.e. the same derived type that encapsulates problem parameters can be easily reused across various problems (optimization, differential equations, etc.).

ivan-pi commented 1 year ago

I noticed C23 is planning to introduce a pointer type for pairing code and data (https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2787.pdf). It looks very similar to the type you are using in nlopt-f:

https://github.com/grimme-lab/nlopt-f/blob/83d21e7ebda9279a6dd4cc11fe6c735f48eb8689/src/nlopt_callback.f90#L75

Admittedly, I don't yet have a full understanding of the proposed C enhancement, but I'm wondering if Fortran should have a similar built-in (language-level) callback+context abstraction.

ivan-pi commented 1 year ago

Instead of having to use host association to adapt the callback interface, maybe something like this

procedure(minpack_func_plus_context), delegate(context) :: f 
   ! context is a named dummy argument of the new procedure callback, 
   ! of type minpack_context
procedure(minpack_func), closure :: fold  
   ! the old interface

type(minpack_context) :: mydata

fold => delegate(f,mydata) 

! or maybe a spin on the old statement function syntax:
fold(x) => f(x,mydata)

call minpack_hybr(fold, ...)

This would be equivalent to

call minpack_hybr(fold, ...)
contains
real function fold(x)
  real, intent(in) :: x
  fold = f(x,mydata) ! f and mydata available through host association
end function

The language is pretty close already when you look at the derived type, and the use of associate to capture the context. A built-in language feature could also solve the problem of lifetime mentioned in https://github.com/grimme-lab/nlopt-f/issues/13