{"id":17259850,"url":"https://github.com/ztangent/valsplit.jl","last_synced_at":"2025-04-12T14:54:06.533Z","repository":{"id":46823726,"uuid":"462900370","full_name":"ztangent/ValSplit.jl","owner":"ztangent","description":"Compile away dynamic dispatch on Val-typed arguments via value-splitting.","archived":false,"fork":false,"pushed_at":"2023-10-12T12:37:53.000Z","size":37,"stargazers_count":56,"open_issues_count":2,"forks_count":4,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-03-26T09:32:43.138Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Julia","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ztangent.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,"publiccode":null,"codemeta":null}},"created_at":"2022-02-23T20:47:26.000Z","updated_at":"2024-06-30T23:53:00.000Z","dependencies_parsed_at":"2025-03-26T09:30:13.025Z","dependency_job_id":"0b544b95-2a24-4f18-889c-f94fbe4a01d3","html_url":"https://github.com/ztangent/ValSplit.jl","commit_stats":{"total_commits":16,"total_committers":1,"mean_commits":16.0,"dds":0.0,"last_synced_commit":"a10608fca33afc907902a133fdd86363d32c6b76"},"previous_names":[],"tags_count":2,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ztangent%2FValSplit.jl","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ztangent%2FValSplit.jl/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ztangent%2FValSplit.jl/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ztangent%2FValSplit.jl/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ztangent","download_url":"https://codeload.github.com/ztangent/ValSplit.jl/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248586238,"owners_count":21128995,"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","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":[],"created_at":"2024-10-15T07:46:21.779Z","updated_at":"2025-04-12T14:54:06.510Z","avatar_url":"https://github.com/ztangent.png","language":"Julia","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ValSplit.jl\n\n![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/ztangent/ValSplit.jl/CI.yml?branch=main)\n![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/ztangent/ValSplit.jl)\n![GitHub](https://img.shields.io/github/license/ztangent/ValSplit.jl?color=lightgrey)\n\nCompile away dynamic dispatch over methods with `Val`-typed arguments by \"`Val`-splitting\" (similar to [union splitting](https://julialang.org/blog/2018/08/union-splitting/)) using the `@valsplit` macro. By annotating a function definition with `@valsplit` and choosing arguments to split upon, the resulting function will be a switch statement over all `Val` parameters associated with the chosen arguments. Requires Julia 1.3 and above.\n\n## Installation\n\nValSplit.jl is a registered package. To install, press `]` at the Julia REPL to enter `Pkg` mode, then run:\n```\nadd ValSplit\n```\n\n## Example\n\nSuppose we have a function `soundof` that takes in a `Val`-typed argument,  and returns how an animal sounds:\n\n```julia\nsoundof(animal::Val{:dog}) = \"woof\"\nsoundof(animal::Val{:cat}) = \"nyan\"\n```\n\nWe might want a version of `soundof` that takes in `Symbol` values directly, and hence define:\n```julia\nsoundof(animal::Symbol) = soundof(Val(animal))\n```\n\nHowever, when using `soundof(animal::Symbol)` in another function, dynamic dispatch might occur if Julia cannot infer the value of the argument `animal` at compile time, resulting in [considerable slowdowns](https://docs.julialang.org/en/v1/manual/performance-tips/#man-performance-value-type).\n\n\nUsing `@valsplit`, we can avoid this issue by *compiling away the dispatch logic as a switch statement*. We do this simply by annotating our method definition with `@valsplit`, and annotating each argument `x::T` we want to switch upon as `Val(x::T)`:\n```julia\n@valsplit function soundof(Val(animal::Symbol))\n    error(\"Sound not defined for animal: \\$animal\")\nend\n```\n\nThe resulting function effectively compiles to the following switch statement,  where the original method body is used as the default branch:\n```julia\nfunction soundof(animal::Symbol)\n    if animal == :dog\n        return \"woof\"\n    elseif animal == :cat\n        return \"nyan\"\n    else\n        error(\"Sound not defined for animal: \\$animal\")\n    end\nend\n```\n\nHowever, unlike a manually-written switch statement, `@valsplit`-defined functions will automatically recompile when new methods are added. For example, if we add the method:\n```julia\nsoundof(animal::Val{:human}) = \"meh\"\n```\n\nThen `soundof(animal::Symbol)` will recompile to a switch statement with an additional branch:\n```julia\nfunction soundof(animal::Symbol)\n    if animal == :dog\n        return \"woof\"\n    elseif animal == :cat\n        return \"nyan\"\n    elseif animal == :human\n        return \"meh\"\n    else\n        error(\"Sound not defined for animal: \\$animal\")\n    end\nend\n```\n\nAs such, `@valsplit`-annotated functions preserve extensibility, while achieving the run-time performance of switch statements (or better, if constant propagation results in compile-time pruning of branches).\n\n## Motivation\n\nThe `@valsplit` macro is intended to address the following two issues:\n- Dynamic dispatch over `Val`-typed arguments is slow\n- Alternative solutions such as manually-written switch statements and global dictionaries are often insufficient for the purposes of extensibility.\n\nNote that dynamic dispatch does not always occur: When there are a small number of values to split on (less than 4, as of Julia 1.6), the Julia compiler automatically generates a switch statement:\n\n```julia\nsoundof(animal::Val{:dog}) = \"woof\"\nsoundof(animal::Val{:cat}) = \"nyan\"\nsoundof(animal::Symbol) = soundof(Val(animal))\n\njulia\u003e @code_typed soundof(:cat)\nCodeInfo(\n1 ─ %1  = invoke Main.Val(_2::Symbol)::Val{_A} where _A\n│   %2  = (isa)(%1, Val{:cat})::Bool\n└──       goto #3 if not %2\n2 ─       goto #6\n3 ─ %5  = (isa)(%1, Val{:dog})::Bool\n└──       goto #5 if not %5\n4 ─       goto #6\n5 ─ %8  = Main.soundof(%1)::String\n└──       goto #6\n6 ┄ %10 = φ (#2 =\u003e \"nyan\", #4 =\u003e \"woof\", #5 =\u003e %8)::String\n└──       return %10\n) =\u003e String\n```\n\nBut once more methods are defined, the Julia compiler no longer performs this optimization:\n\n```julia\nfor i in 1:4\n    sound = \"sound $i\"\n    eval(:(soundof(animal::Val{Symbol(:animal, $i)}) = $sound))\nend\nsoundof(animal::Symbol) = soundof(Val(animal))\n\njulia\u003e @code_typed soundof(:animal1)\nCodeInfo(\n1 ─ %1 = invoke Main.Val(_2::Symbol)::Val{_A} where _A\n│   %2 = Main.soundof(%1)::Any\n└──      return %2\n) =\u003e Any\n```\n\nTo avoid dynamic dispatch, manually switching on a set of values is the fastest in terms of both compile-time and run-time, but the set of values to switch upon cannot be extended. Global dictionaries can partially address this problem by associating values with code:\n\n```julia\nconst SOUND_OF = Dict{Symbol,Function}()\n\nwoof() = \"woof\"\nSOUND_OF[:dog] = woof\n\nnyan() = \"nyan\"\nSOUND_OF[:cat] = nyan\n\nsoundof(animal::Symbol) = SOUND_OF[animal]()\n```\n\nHowever, dictionary lookup times [are usually slower](https://groups.google.com/g/julia-users/c/jUMu9A3QKQQ/m/qjgVWr7vAwAJ) compared to (small) switch statements. In addition, this approach [runs into issues with precompilation](https://docs.julialang.org/en/v1/manual/modules/#Module-initialization-and-precompilation), preventing a downstream module from adding new entries to a global dictionary defined in another module (except at run-time using the `__init__` function). In other words, global dictionaries are not extensible across module boundaries.\n\nThe `@valsplit` macro addresses this problem because new methods can always be introduced by downstream modules, resulting in recompilation of the `@valsplit` annotated function. It effectively uses Julia's method table as a global dictionary, but avoids the overhead of dynamic dispatch using the same `@generated` function tricks used to implement `static_hasmethod` in [`Tricks.jl`](https://github.com/oxinabox/Tricks.jl).\n\nA small benchmark is [provided here](benchmarks/benchmarks.jl). With 10 values to branch on, running Julia 1.6.1 on a Windows machine, the results of the benchmark are as follows:\n\n```julia\nManual switch statement:\n  3.275 μs (0 allocations: 0 bytes)\nGlobal Dict{Symbol,String}:\n  78.800 μs (0 allocations: 0 bytes)\nGlobal LittleDict{Symbol,String}:\n  111.600 μs (0 allocations: 0 bytes)\nDynamic dispatch:\n  2.300 ms (0 allocations: 0 bytes)\nVal-splitting with @valsplit:\n  3.275 μs (0 allocations: 0 bytes\n```\n\n## Utilities\n\nValSplit.jl provides a few other utility functions for determining whether a method with particular `Val`-typed argument exists.\n\nTo determine the set of all `Val` parameters associated with a particular argument of a particular function, use `valarg_params`:\n\n\u003e    `valarg_params(f, types::Type{\u003c:Tuple}, idx::Int, ptype::Type=Any)`\n\u003e\n\u003e Given a method signature `(f, types)`, finds all matching methods with a concrete `Val`-typed argument in position `idx`, then returns all parameter values for the `Val`-typed argument as a tuple. Optionally, `ptype` can be specified to filter parameter values that are instances of `ptype`.\n\u003e\n\u003eThis function is statically compiled, and will automatically be recompiled whenever a new method of `f` is defined.\n\nTo determine whether a particular argument of a particular function has a specific `Val` parameter, use `valarg_has_param`:\n\n\u003e    `valarg_has_param(f, types::Type{\u003c:Tuple}, param, idx::Int, ptype::Type=Any)`\n\u003e\n\u003e Given a method signature `(f, types)`, returns `true` if there exists a matching method with a `Val`-typed argument in position `idx` with parameter `param` and parameter type `ptype`.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fztangent%2Fvalsplit.jl","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fztangent%2Fvalsplit.jl","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fztangent%2Fvalsplit.jl/lists"}