{"id":30617454,"url":"https://github.com/curtd/forwardmethods.jl","last_synced_at":"2025-08-30T10:10:37.853Z","repository":{"id":176408098,"uuid":"657818050","full_name":"curtd/ForwardMethods.jl","owner":"curtd","description":"Composition made easy(ish)","archived":false,"fork":false,"pushed_at":"2024-09-02T21:50:55.000Z","size":579,"stargazers_count":25,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-08-09T10:16:09.496Z","etag":null,"topics":["julia","metaprogramming"],"latest_commit_sha":null,"homepage":"","language":"Julia","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/curtd.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null}},"created_at":"2023-06-23T23:45:34.000Z","updated_at":"2025-07-26T16:44:26.000Z","dependencies_parsed_at":"2023-12-30T01:25:48.600Z","dependency_job_id":"37f7f003-0c7a-4b64-aa8b-94be239663f1","html_url":"https://github.com/curtd/ForwardMethods.jl","commit_stats":null,"previous_names":["curtd/forwardmethods.jl"],"tags_count":13,"template":false,"template_full_name":null,"purl":"pkg:github/curtd/ForwardMethods.jl","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/curtd%2FForwardMethods.jl","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/curtd%2FForwardMethods.jl/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/curtd%2FForwardMethods.jl/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/curtd%2FForwardMethods.jl/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/curtd","download_url":"https://codeload.github.com/curtd/ForwardMethods.jl/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/curtd%2FForwardMethods.jl/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":272834409,"owners_count":25001089,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-08-30T02:00:09.474Z","response_time":77,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["julia","metaprogramming"],"created_at":"2025-08-30T10:10:36.680Z","updated_at":"2025-08-30T10:10:37.843Z","avatar_url":"https://github.com/curtd.png","language":"Julia","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ForwardMethods\n\n[![Stable](https://img.shields.io/badge/docs-stable-blue.svg)](https://curtd.github.io/ForwardMethods.jl/stable/)\n[![Dev](https://img.shields.io/badge/docs-dev-blue.svg)](https://curtd.github.io/ForwardMethods.jl/dev/)\n[![Build Status](https://github.com/curtd/ForwardMethods.jl/actions/workflows/CI.yml/badge.svg?branch=main)](https://github.com/curtd/ForwardMethods.jl/actions/workflows/CI.yml?query=branch%3Amain)\n[![Coverage Status](https://coveralls.io/repos/github/curtd/ForwardMethods.jl/badge.svg)](https://coveralls.io/github/curtd/ForwardMethods.jl)\n\n`ForwardMethods` provides macros that automate some of the boilerplate involved when using composition for object polymorphism. This package is essentially fancy copy + paste for forwarding function definitions, as well as providing automatically generated generic interfaces for struct types. \n\n# `@forward_methods` \nGiven a hypothetical definition for type `T` with a subfield `s` of type `S`, i.e., \n\n```julia\nstruct T \n    ...\n    s::S\n    ...\nend\n```\nsuppose that type `S` has a number of existing functions defined as `f₁(..., obj::S, ...; kwargs...)`, ..., `fₙ(..., obj::S, ...; kwargs...)`. In the argument list here, `...` indicates a fixed number of preceding or following arguments (and not a Julia splatting pattern).\n\nIf you wanted to define instances of these methods for `x::T` by forwarding them to `getfield(x, :s)`, the normal boilerplate code you would have to write in standard Julia would be \n```julia\n    for f in (:f₁, ..., :fₙ)\n        @eval $f(..., x::T, ...) = $f(..., getfield(x, :s), ...)\n    end\n```\nwhich works fine when all of the functions `fᵢ` have same number of arguments and the position of the argument of type `S` is the same in each argument list. If this is not the case, it can be fairly tedious to generate all of the specific signature expressions to evaluate. \n\nThe `@forward_methods` macro automates much of this boilerplate away by only requiring you to specify the type, the fieldname, and the signature of the methods you'd like to forward. \n\nTo take a concrete example, suppose we define \n```julia\n    struct T\n        s::Vector{Int}\n    end\n```\n\nWe'd like to forward a number of method signatures corresponding to the `AbstractArray` interface. We can write this compactly using `@forward_methods` as:\n\n```julia\n    @forward_methods T field=s Base.length(x::T) Base.getindex(x::T, i::Int) Base.setindex!(x::T, v, i::Int)\n```\n\nHere the position of the argument of interest will be inferred from the type annotation `::T`. We can write this even more compactly as\n\n```julia \n    @forward_methods T field=s Base.length Base.getindex(_, i::Int) Base.setindex!(x::T, v, i::Int)\n```\n\nHere a `0-`argument expression `Base.length` is expanded to `Base.length(x::T)` and the placeholder underscore `_` is expanded to `x::T`.\n\nRather than defining a forwarded method with an object argument to its child value, e.g., `x::T` -\u003e `getfield(x, :s)` as above, you can also forward the method with a type argument `::Type{T}` to `fieldtype(T, :s)` as follows \n\n```julia \n    @forward_methods T field=s Base.eltype(::Type{T}) Base.IndexStyle(::Type{T})\n```\n\nParametric types and type signatures are also supported, so if you have, say, \n```julia \nstruct A{B}\n    d::Dict{String, B}\nend\n```\n\nyou can easily forward parametric / non-parametric method definitions to field `d` simultaneously via  \n```julia \n    @forward_methods A{B} field=d (Base.getindex(a::A{B}, k::String) where {B}) Base.keys(x::A) Base.values(_)\n```\n\nLetting `x::T` denote the object of interest, the value of the keyword argument `field = _field` has the following effects on the generated expressions:\n\n- If `_field = k` with `k::Symbol` or a `k::QuoteNode`, or an expression of the form `getfield(_, k)`, in which case methods will be forwarded to `getfield(x, k)`\n\n- If `_field = a.b.c. ... .z` is a dotted expression, methods will be forwarded to `getfield(getfield(... getfield(getfield(x, :a), :b), ...), :z)`\n\n- If `_field` is an expression of the form `getproperty(_, k)`, the method instances will be forwarded to `getproperty(x, k)`\n\n- If `_field` is an expression of the form `t[args...]`, the method instances will be forwarded to `x[args...]`\n\n- If `_field` is an expression of the form `f(_)` for a single-argument function `f`, the method instances will be forwarded to `f(x)`\n\n\n# `@forward_interface` \nIf the type of `S` has a known interface (e.g., a fixed set of methods defined with a particular type signature), it may be more convenient to forward the entire suite of methods for that interface to objects `x::T`, rather than to specify each method signature individually.\n\n```julia \n    @forward_interface T field=_field interface=_interface [kwargs...]\n```\n\nHere `T` and `_field` are as above and `_interface` is one of a preset number of values, namely `iteration`, `indexing`, `array`, `dict`, `getfields`, and `setfields`. The value of `_interface` determines the specific forwarded method signatures that are generated, e.g., \n\n```julia\nstruct B\n    b::Dict{String, Int}\nend\n\n@forward_interface B field=b interface=dict \n\nb = B(Dict{String,Int}())\n```\n\n`b` can now be used as a drop-in replacement in any method where a `Dict{String,Int}` is supported.\n\nNote: certain methods for certain interfaces (e.g., `Base.similar` for the Array interface) are not included in this macro as direct method forwarding would not make sense to apply in these cases. \n\nThe `getfields` and `setfields` interfaces are dynamically generated based on the fields of type `T`. \n\nWhen `interface=getfields`, this macro forwards methods of the form `$field(x::T) = getfield(x, $field)` for each `field ∈ fieldnames(T)`\n\nWhen `interface=setfields`, this macro forwards methods of the form `$field!(x::T, value) = setfield!(x, $field, value)` for each `field ∈ fieldnames(T)`\n\n# `@define_interface`\nCertain interfaces are defined for objects `x::T` that don't involve explicit forwarding to a fixed-field, per-se, but can be generally useful. \n\n## `properties` interface \nThe `properties` interface allows a unified `Base.propertynames`, `Base.getproperty`, and `Base.setproperty!` interface for `x::T` which is composed of subfields `k1, k2, ..., kn` whose fields should also be included in the properties of `x`. This pattern arises when creating composite types. For example,\n\n```julia\njulia\u003e struct A\n           key1::Int\n           key2::Bool\n       end\n\njulia\u003e struct B\n           key3::String\n           key4::Float64\n       end\n\njulia\u003e struct C\n           a::A\n           b::B\n       end\n\njulia\u003e @define_interface C interface=properties delegated_fields=(a,b)\n\njulia\u003e c = C(A(1, true), B(\"a\", 0.0))\nC(A(1, true), B(\"a\", 0.0))\n\njulia\u003e (key1=c.key1, key2=c.key2, key3=c.key3, key4=c.key4, a=c.a, b=c.b)\n(key1 = 1, key2 = true, key3 = \"a\", key4 = 0.0, a = A(1, true), b = B(\"a\", 0.0))\n```\n\nEssentially the fields of `c.a` and `c.b` has been flattened to provide a unified view of the properties of `c`.\n\n## `equality` interface\nThe `equality` interface defines `Base.==` or `Base.isequal` for objects of `T` in the obvious way, i.e., \n\n```julia\n    Base.:(==)(x::T, y::T) = all( getfield(x,k) == getfield(y,k) for k in fieldnames(T) )\n```\n\nThere are some configurable options for this macro, so you can do some fancier equality comparisons such as \n\n```julia\njulia\u003e struct E \n           d::Dict{Symbol,Int}\n       end\n\njulia\u003e Base.propertynames(e::E) = collect(keys(getfield(e, :d)))\n\njulia\u003e Base.getproperty(e::E, k::Symbol) = getfield(e, :d)[k]\n\njulia\u003e Base.setproperty!(e::E, k::Symbol, v::Int) = getfield(e, :d)[k] = v\n\njulia\u003e @define_interface E interface=equality compare_fields=propertynames\n\njulia\u003e \n\njulia\u003e e = E(Dict(:a =\u003e 1))\nE(Dict(:a =\u003e 1))\n\njulia\u003e e == E(Dict(:a =\u003e 1))\ntrue\n\njulia\u003e e == E(Dict(:a =\u003e 2))\nfalse\n```\n\n\n## Similar Packages/Functionality\n- [ReusePatterns.jl](https://github.com/gcalderone/ReusePatterns.jl)\n- `@forward` in [Lazy.jl](https://github.com/MikeInnes/Lazy.jl)","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcurtd%2Fforwardmethods.jl","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcurtd%2Fforwardmethods.jl","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcurtd%2Fforwardmethods.jl/lists"}