Mc-Zen / tidy

A documentation generator for Typst in Typst.
https://typst.app/universe/package/tidy
MIT License
46 stars 2 forks source link

Support for curried functions (func.with()) #20

Open JamesxX opened 1 month ago

JamesxX commented 1 month ago

As per title, functions defined by currying are treated as variables rather than functions. Perhaps, could Tidy check if a variable is set using .with(), and import the documentation (if any) for that function? (recursively)

Mc-Zen commented 1 month ago

Hi, good to know that someone would like to use that. Originally, variables were not supported and it was possible to document curried functions just like any other function. But variables were more important, so that was changed.

But I think, what you suggested should be possible. Let's find out the precise behaviour. When a documented declaration of the form

/// ...
#let alias = func.with()

is encountered, it should be added to the functions, not the variables.

JamesxX commented 1 month ago

What do you mean exactly by importing the documentation? Shouldn't the new function have an own documentation in most cases? Maybe it is enough to link to the "parent" function?

The curried function will obviously share a lot of functionality with the parent function, which we will assume is already well documented. It wouldn't make sense to duplicate those comments documenting the function in the source code unnecessarily. Hopefully the package could automatically detect which function the parent is, and therefore know its docs. In that case, it could either render parts of that documentation automatically, and/or link to the parent function's documentation while explaining that it shares those arguments just with certain defaults set.

How to go about default arguments (that would be set with .with())

It probably makes sense to reflect that the default of the argument is changed to something else. Perhaps some text in a colour that stands out that this argument has been specialized to such and such value?

Mc-Zen commented 1 month ago

Mmh, this thing seems not so straightforward. How much of the parent's documentation should be included? I can also imagine cases where the fact that this is just a curried function should not be mentioned, i.e., where a really general function is broken into several specific ones. Maybe someone wants to have a full documentation for those.

I wouldn't like making a default decision and taking away some fine-grained control for these cases.

One option would be to just store some information about curried functions in the docstring info that is returned by the parser and make it the task of a style to deal with the details. For example, the parser could just detect curried functions and store the name of the "parent". In the showing step, the style can look up the docstring of the parent function (recursively, if necessary).

That wouldn't work out-of-the-box between different modules though.

Mc-Zen commented 1 month ago

Hi @JamesxX ,

I implemented some detection for curried functions in the branch detect-curried-functions. It looks for a .with statement in the same line and extracts the name of the parent function as well as the provided arguments (which need not be on the same line). This information is stored under the key parent in the function info dictionaries returned by parse-module() and contains the name of the parent function, a list of pos arguments and a dictionary of named arguments.

The utilized style would be responsible for using this information. Currently, the shipped styles just ignore it but by customizing one, it should be possible to display some useful information.

Would you like to try it out and see if this satisfies your needs?

JamesxX commented 1 month ago

Sweet, thanks! I'll take a look after work.

Mc-Zen commented 2 weeks ago

@JamesxX Hi, did you already have time to check it out?

jneug commented 5 days ago

@Mc-Zen I took a look at the branch, but couldn't make it work. Curried functions are still passed to#show-variable without any additional fields. Do I need any additional setup?

jneug commented 5 days ago

I used this file for testing:

/// My base function.
/// - foo (content): Something.
/// - bar (boolean): A boolean.
/// -> content
#let myfunc(foo, bar: false) = strong(foo)

/// My curried function.
/// -> content
#let curried = myfunc.with(bar: true)
Mc-Zen commented 2 days ago

@jneug ouf, my bad. It still thinks that it is a variable because there are of course no parentheses😆

Mc-Zen commented 2 days ago

I wonder, should they go under functions or variables? (justified suggestions welcome!)

jneug commented 2 days ago

I think they should look like any other function, but all functions should have a parent key that is none for normal functions and set to the name of the parent for curried functions.

The parameters of the parent should be copied to the curried function with updated defaults. This way, the theme could show the full function signature and a reference to the parent for further details.

jneug commented 2 days ago

For my example, it would look something like this:

#{
(functions: (
(
  name: "myfunc",
  parent: none,
  description: "My base function.",
  args: (
    foo: (
      description: "Something",
      types: ("content",),
    ),
    bar: (
      default: "false",
      description: "A boolean",
      types: ("bool",),
    ),
  ),
  return-types: ("content",),
),
(
  name: "curried",
  parent: "myfunc",
  description: "My curried function.",
  args: (
    foo: (
      description: "Something",
      types: ("content",),
    ),
    bar: (
      default: "true",
      description: "A boolean",
      types: ("bool",),
    ),
  ),
  return-types: ("content",),
),
))
}
Mc-Zen commented 2 days ago

So, for once the issue is fixed (and your code sample works)!

At the moment, the parent is not resolved, i.e., functions contains the entry

    (
      name: "curried",
      description: " My curried function.\n\n",
      type: "content",
      parent: (name: "myfunc", pos: (), named: (bar: "true")),
    ),

The parent info consists of the name, an array of prepended positional arguments and a dictionary of prepended named arguments.

I test-implemented (not yet pushed) an additional resolving step that copies the docstring info for the parent.Then, the dictionary from above would instead read as:

  (
    name: "curried",
    description: " My curried function.\n\n",
    type: "content",
    parent: (
      name: "myfunc",
      pos: (),
      named: (bar: "true"),
      description: " My base function.\n",
      args: (
        foo: (description: "Something.", types: ("content",)),
        bar: (
          default: "false",
          description: "A boolean.",
          types: ("boolean",),
        ),
      ),
      return-types: ("content",),
    ),
  ),

One could say that the resolving part is up to the showing step (and therefore the style) but for show-function (which will in future be replaced by a type for better customization options) it would be hard to retrieve the parent's info.

jneug commented 2 days ago

So the pos and named keys in parent hold the parameters that are passed to .with()?

Mc-Zen commented 2 days ago

Yes

jneug commented 1 day ago

The parent definitely should be resolved as suggested, imo. The style logic gets really convoluted for parsing curried functions right now because they have a different structure from "normal" functions. (As a side note: the default style fails right now because of this).

I still think from the styles' perspective there should be no difference between a curried function and others (other than a parent key with the name of the parent function to show a reference to it). They are both functions with parameters. The resolving should be done by Tidy.

There is no difference between these two functions:

#let func-b = func-a.with(x: "y")

#let func-c(x: "y", ..args) = func-a(x: x, ..args)

Both are basically func-a with x set to "y". To the user of these functions, the docs should probably look the same.