Skip to content

Commit

Permalink
clarify importance of constructor over type in traits and docstrings
Browse files Browse the repository at this point in the history
  • Loading branch information
ablaom committed May 30, 2024
1 parent 54a5f9b commit 0af5476
Show file tree
Hide file tree
Showing 6 changed files with 47 additions and 12 deletions.
7 changes: 7 additions & 0 deletions docs/src/anatomy_of_an_implementation.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ mechanism for creating new versions of itself, with modified property (field) va
this end, we implement `LearnAPI.constructor`, which must return a keyword constructor:

```@example anatomy
"""
Ridge(; lambda=0.1)
Instantiate a ridge regression algorithm, with regularization of `lambda`.
"""
Ridge(; lambda=0.1) = Ridge(lambda)
LearnAPI.constructor(::Ridge) = Ridge
nothing # hide
Expand All @@ -60,6 +65,8 @@ So, if `algorithm = Ridge(lambda=0.1)` then `LearnAPI.constructor(algorithm)(lam
is another algorithm with the same properties, except that the value of `lambda` has been
changed to `0.05`.

Note that we attach the docstring to the constructor, not the struct.


## Implementing `fit`

Expand Down
15 changes: 11 additions & 4 deletions docs/src/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ for such algorithms [`LearnAPI.is_composite`](@ref)`(algorithm)` must be `true`
is `false`). Generally, the keyword constructor provided by [`LearnAPI.constructor`](@ref)
must provide default values for all non-algorithm properties.

Any object `algorithm` for which [`LearnAPI.functions`](@ref)`(algorithm)` is non-empty is
understood have a valid implementation of the LearnAPI.jl interface.

### Example

Any instance of `GradientRidgeRegressor` defined below is a valid algorithm.
Expand All @@ -110,8 +113,11 @@ GradientRidgeRegressor(; learning_rate=0.01, epochs=10, l2_regularization=0.01)
LearnAPI.constructor(::GradientRidgeRegressor) = GradientRidgeRegressor
```

Any object `algorithm` for which [`LearnAPI.functions`](@ref)`(algorithm)` is non-empty is
understood have a valid implementation of the LearnAPI.jl interface.
### Documentation

Attach public LearnAPI.jl-related documentation for an algorithm to it's *constructor*,
rather than to the struct defining its type. In this way, an algorithm can implement
non-LearnAPI interfaces (such as a native interface) with separate document strings.


## Methods
Expand All @@ -125,8 +131,9 @@ Only these method names are exported by LearnAPI: `fit`, `transform`, `inverse_t
### List of methods

- [`fit`](@ref fit): for training or updating algorithms that generalize to new data. Or,
for non-generalizing ("static") algorithms, wrap `algorithm` in a mutable struct that
can be mutated by `predict`/`transform` to record byproducts of those operations.
for non-generalizing algorithms (see [Static Algorithms](@ref)), wrap `algorithm` in a
mutable struct that can be mutated by `predict`/`transform` to record byproducts of
those operations.

- [`predict`](@ref operations): for outputting [targets](@ref proxy) or [target
proxies](@ref proxy) (such as probability density functions)
Expand Down
11 changes: 6 additions & 5 deletions docs/src/traits.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,13 @@ Multiple traits can be declared like this:
To ensure that trait metadata can be stored in an external algorithm registry, LearnAPI.jl
requires:

1. *Finiteness:* The value of a trait is the same for all algorithms with same
underlying `UnionAll` type. That is, even if the type parameters are different, the
trait should be the same. There is an exception if `is_composite(algorithm) = true`.
1. *Finiteness:* The value of a trait is the same for all `algorithm`s with same value of
[`LearnAPI.constructor(algorithm)`](@ref). This typically means trait values do not
depend on type parameters! There is an exception if `is_composite(algorithm) = true`.

2. *Serializability:* The value of any trait can be evaluated without installing any
third party package; `using LearnAPI` should suffice.
2. *Immediate serializability:* It should be possible to call a trait without first
installing any third party package. Importing the package that defines the algorithm,
together with `import LearnAPI` should suffice.

Because of 1, combining a lot of functionality into one algorithm (e.g. the algorithm can
perform both classification or regression) can mean traits are necessarily less
Expand Down
8 changes: 6 additions & 2 deletions src/traits.jl
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,12 @@ julia> algorithm2.lambda
# New implementations
All new implementations must overload this trait. It must be possible to recover an
algorithm from the constructor returned as follows:
All new implementations must overload this trait.
Attach public LearnAPI.jl-related documentation for an algorithm to the constructor, not
the algorithm struct.
It must be possible to recover an algorithm from the constructor returned as follows:
```julia
properties = propertynames(algorithm)
Expand Down
2 changes: 1 addition & 1 deletion src/types.jl
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ end

const CONCRETE_TARGET_PROXY_TYPES_LIST = join(
map(CONCRETE_TARGET_PROXY_TYPES_SYMBOLS) do s
"`$s`"
"`$s()`"
end,
", ",
" and ",
Expand Down
16 changes: 16 additions & 0 deletions test/integration/regression.jl
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,17 @@ import DataFrames
# We overload `obs` to expose internal representation of input data. See later for a
# simpler variation using the `obs` fallback.

# no docstring here - that goes with the constructor
struct Ridge
lambda::Float64
end

"""
Ridge(; lambda=0.1)
Instantiate a ridge regression algorithm, with regularization of `lambda`.
"""
Ridge(; lambda=0.1) = Ridge(lambda) # LearnAPI.constructor defined later

struct RidgeFitObs{T,M<:AbstractMatrix{T}}
Expand Down Expand Up @@ -176,9 +184,17 @@ end

# # VARIATION OF RIDGE REGRESSION THAT USES FALLBACK OF LearnAPI.obs

# no docstring here - that goes with the constructor
struct BabyRidge
lambda::Float64
end

"""
BabyRidge(; lambda=0.1)
Instantiate a ridge regression algorithm, with regularization of `lambda`.
"""
BabyRidge(; lambda=0.1) = BabyRidge(lambda) # LearnAPI.constructor defined later

struct BabyRidgeFitted{T,F}
Expand Down

0 comments on commit 0af5476

Please sign in to comment.