Skip to content

How to implement same procedures for different numeric kinds #35

Closed
@milancurcic

Description

@milancurcic

This question comes up in #34 and elsewhere. How to implement specific procedures that work on different kinds (sp, dp, qp, int8, int16, int32, int64) as well as characters, where the body of the procedure is the same (can be copy/pasted entirely without breaking it). Let's first just focus on this scenario, and we can consider more complex cases later.

I know of a few approaches:

  1. Repeat the code, that is, implement all specific procedures explicitly. That's what I did in functional-fortran, see https://github.com/wavebitscientific/functional-fortran/blob/master/src/lib/mod_functional.f90. Repeating is fine if you do it once and forget about it. The upside is that you can see the specific code and it needs no extra tooling. The downside is combinatorial explosion if you have procedures that are to handle all combinations of types and kinds. Most procedures are rather simple (one or two arguments), and I ended up with > 3K lines of code for 23 generic procedures. Most work was in editing the argument types to specific procedures, and less work was in copy/pasting of the repeatable content. I don't recommend this approach for stdlib.

  2. Approach 1 can be somewhat eased by explicitly typing out the interfaces, and using #include 'procedure_body.inc', defined in a separate file. Then your procedure body collapses to one line. This reduces the total amount of code, but not so much the amount of work needed, as most work is in spelling out the interfaces. This approach still doesn't need extra tooling as a C preprocessor comes with all compilers that I'm aware of.

  3. Use a custom preprocessor or templating tool. For example, a function that returns a set of an array:

pure recursive function set(x) result(res)
  integer, intent(in) :: x(:) !! Input array
  integer, allocatable :: res(:)
  if(size(x) > 1)then
    res = [x(1), set(pack(x(2:), .not. x(2:) == x(1)))]
  else
    res = x
  endif
end function set

A template could look like this:

pure recursive function set(x) result(res)
  {int*, real*}, intent(in) :: x(:) !! Input array
  {int*, real*}, allocatable :: res(:)
  ... ! body omitted for brevity
end function set

or similar, where the custom preprocessor would spit out specific procedures for all integer and real kinds. Some additional or alternative syntax would be needed if you wanted all combinations of type kinds between arguments.

There may be tools that do this already, and I think @zbeekman mentioned one that he uses. In general, for stdlib I think this is the way to go because we are likely to see many procedures that support multiple arguments with inter-compatible type kinds. The downside (strong downside IMO) is that we're likely to introduce a tool dependency that also depends on another language. If the community agrees, we can use this thread to review existing tools and which would be most fitting for stdlib.

Let's say we pick a tool to do the templating for us, we have two choices:

a) Have user build specifics from templates. In this scenario, the user must install the templating tool in order to build stdlib. I think we should avoid this.
b) Use the templating tool as developers only, and maintain the pre-built specifics in the repo. This means that when we're adding new code that will work on many type kinds, we use the tool on our end to generate the source, and commit that source to the repo (alongside the templates in a separate, "for developers" directory).

Assuming we can find a fitting tool, I'm in favor of the 3b approach here. There may be other approaches I'm not aware of or forgot about. What do you think and any other ideas?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions