From d0b68ab9ccf342d74b5f7d3cd23dcce83b9becc2 Mon Sep 17 00:00:00 2001 From: Zachary P Christensen Date: Sat, 6 Jun 2020 12:46:45 -0400 Subject: [PATCH] 0.2 (#2) * Adds common interface for fixed functions * New NFix type --- .travis.yml | 27 +++--- Project.toml | 2 +- README.md | 117 ++++++++++++++++++-------- docs/Project.toml | 2 + docs/make.jl | 16 ++++ docs/src/index.md | 6 ++ src/ChainedFixes.jl | 197 +++++++++++++++++++++++++++++++++++++++++--- test/Project.toml | 3 + test/runtests.jl | 171 +++++++++++++++++++++++++++++++++++--- 9 files changed, 473 insertions(+), 68 deletions(-) create mode 100644 docs/Project.toml create mode 100644 docs/make.jl create mode 100644 docs/src/index.md create mode 100644 test/Project.toml diff --git a/.travis.yml b/.travis.yml index a00ed30..f6d76e5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,20 +1,25 @@ language: julia os: - - linux - - osx + - osx + - linux julia: - - 1.3 - - nightly + - 1.3 + - nightly codecov: true -matrix: - allow_failures: - - julia: nightly - fast_finish: true - notifications: - email: false + email: false + +jobs: + include: + - stage: "Documentation" + julia: 1.3 + os: linux + script: + - julia --project=docs/ -e 'using Pkg; Pkg.develop(PackageSpec(path=pwd())); Pkg.instantiate()' + - julia --project=docs/ docs/make.jl + after_success: skip after_success: - - julia -e 'using Pkg; Pkg.add("Coverage"); using Coverage; Codecov.submit(process_folder())' + - julia -e 'import Pkg; Pkg.add("Coverage"); using Coverage; Codecov.submit(Codecov.process_folder())' diff --git a/Project.toml b/Project.toml index d7b6729..dcf1805 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "ChainedFixes" uuid = "9706b775-b1f4-4c74-b677-0491368ea71c" authors = ["Zachary P. Christensen "] -version = "0.1.1" +version = "0.2.0" [compat] julia = "1" diff --git a/README.md b/README.md index c511f1b..83e9fc5 100644 --- a/README.md +++ b/README.md @@ -1,61 +1,108 @@ # ChainedFixes [![Build Status](https://travis-ci.com/Tokazama/ChainedFixes.jl.svg?branch=master)](https://travis-ci.com/Tokazama/ChainedFixes.jl) [![codecov](https://codecov.io/gh/Tokazama/ChainedFixes.jl/branch/master/graph/badge.svg)](https://codecov.io/gh/Tokazama/ChainedFixes.jl) +[![stable-docs](https://img.shields.io/badge/docs-stable-blue.svg)](https://Tokazama.github.io/ChainedFixes.jl/stable) +[![dev-docs](https://img.shields.io/badge/docs-dev-blue.svg)](https://Tokazama.github.io/ChainedFixes.jl/dev) -Chain operators `Base.Fix2` operations with two possible methods. - -`and` is synonymous with bitwise `&` operator but may be used to chain multiple `Fix1` or -`Fix2` operations. The `⩓` (`\\And`) operator may be used in its place (e.g., `x ⩓ y`). +`ChainedFixes.jl` provides useful tools for interacting with functions where arguments are fixed to them. +This includes support for those found in Julia's `Base` module (`Base.Fix1`, `Base.Fix2`) and exported from `ChainedFixes` (`ChainedFix` and `NFix`). +Some simple functionality available form this package is chaining any fixed function. ```julia julia> using ChainedFixes -julia> and(true, <(5))(1) +julia> gt_or_lt = or(>(10), <(5)); + +julia> gt_or_lt(2) true -julia> and(<(5), false)(1) +julia> gt_or_lt(6) false -julia> and(and(<(5), >(1)), >(2))(3) -true -julia> and(<(5) ⩓ >(1), >(2))(3) # ⩓ == \\And +julia> gt_and_lt = and(>(1), <(5)); + +julia> gt_and_lt(2) true +julia> gt_and_lt(0) +false ``` -`or` is synonymous with bitwise `|` operator but may be used to chain multiple `Fix1` or -`Fix2` operations. The `⩔` (`\\Or`) operator may be used in its place (e.g., `x ⩔ y`). - +There's more convenient syntax for these available in the Julia REPL. ```julia -julia> using ChainedFixes +julia> gt_or_lt = >(10) ⩔ <(5); # \Or -julia> or(true, <(5))(1) +julia> gt_or_lt(2) true -julia> or(<(5), false)(1) -true +julia> gt_or_lt(6) +false + -julia> or(<(5) ⩔ >(1), >(2))(3) # ⩔ == \\Or +julia> gt_and_lt = >(1) ⩓ <(5); # \And + +julia> gt_and_lt(2) true + +julia> gt_and_lt(0) +false +``` + +Any function can have methods fixed to it with the `NFix` function. + +```julia +julia> fxn1(x::Integer, y::AbstractFloat, z::AbstractString) = Val(1); + +julia> fxn1(x::Integer, y::AbstractString, z::AbstractFloat) = Val(2); + +julia> fxn1(x::AbstractFloat, y::Integer, z::AbstractString) = Val(3); + +julia> fxn2(; x, y, z) = fxn1(x, y, z); + +julia> fxn3(args...; kwargs...) = (fxn1(args...), fxn2(; kwargs...)); + +julia> NFix{(1,2)}(fxn1, 1, 2.0)("a") +Val{1}() + +julia> NFix{(1,3)}(fxn1, 1, 2.0)("a") +Val{2}() + +julia> NFix{(1,3)}(fxn1, 1.0, "")(2) +Val{3}() + +julia> NFix(fxn2, x=1, y=2.0)(z = "a") +Val{1}() + +julia> NFix(fxn2, x=1, z=2.0)(y="a") +Val{2}() + +julia> NFix{(1,2)}(fxn3, 1, 2.0; x=1.0, z="")(""; y = 1) +(Val{1}(), Val{3}()) + ``` -## Conveniant Type Constants - -| Syntax | Type Constant | -| -----------: | ----------------------- | -| `and`/`⩓` | `And{F1,F2}` | -| `or`/`⩔` | `Or{F1,F2}` | -| `isapprox` | `Approx{T,Kwargs}` | -| `in` | `In{T}` | -| `!in` | `NotIn{T}` | -| `<` | `Less{T}` | -| `<=` | `LessThanOrEqual{T}` | -| `>` | `Greater{T}` | -| `>=` | `GreaterThanOrEqual{T}` | -| `==` | `Equal{T}` | -| `isequal` | `Equal{T}` | -| `!=` | `NotEqual{T}` | -| `startswith` | `StartsWith{T}` | -| `endswith` | `EndsWith{T}` | + +## Constants + +The following constants are exported. + +| Syntax | Type Constant | +|------------------------------------------:|:------------------------| +| `and(f1::F1, f1::F2)`/`⩓(f1::F1, f1::F2)` | `And{F1,F2}` | +| `or(f1::F1, f1::F2)`/`⩔(f1::F1, f1::F2)` | `Or{F1,F2}` | +| `isapprox(x::T; kwargs::Kwargs)` | `Approx{T,Kwargs}` | +| `!isapprox(x::T; kwargs::Kwargs)` | `NotApprox{T,Kwargs}` | +| `in(x::T)` | `In{T}` | +| `!in(x::T)` | `NotIn{T}` | +| `<(x::T)` | `Less{T}` | +| `<=(x::T)` | `LessThanOrEqual{T}` | +| `>(x::T)` | `Greater{T}` | +| `>=(x::T)` | `GreaterThanOrEqual{T}` | +| `==(x::T)` | `Equal{T}` | +| `isequal(x::T)` | `Equal{T}` | +| `!=(x::T)` | `NotEqual{T}` | +| `startswith(x::T)` | `StartsWith{T}` | +| `endswith(x::T)` | `EndsWith{T}` | + diff --git a/docs/Project.toml b/docs/Project.toml new file mode 100644 index 0000000..dfa65cd --- /dev/null +++ b/docs/Project.toml @@ -0,0 +1,2 @@ +[deps] +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" diff --git a/docs/make.jl b/docs/make.jl new file mode 100644 index 0000000..8201f76 --- /dev/null +++ b/docs/make.jl @@ -0,0 +1,16 @@ +using Documenter, ChainedFixes + +makedocs(; + modules=[ChainedFixes], + format=Documenter.HTML(), + pages=[ + "ChainedFixes" => "index.md", + ], + repo="https://github.com/Tokazma/ChainedFixes.jl/blob/{commit}{path}#L{line}", + sitename="ChainedFixes.jl", + authors="Zachary P. Christensen", +) + +deploydocs( + repo = "github.com/Tokazama/ChainedFixes.jl.git", +) diff --git a/docs/src/index.md b/docs/src/index.md new file mode 100644 index 0000000..4ddae4c --- /dev/null +++ b/docs/src/index.md @@ -0,0 +1,6 @@ +# ChainedFixes + +```@autodocs +Modules = [ChainedFixes] +``` + diff --git a/src/ChainedFixes.jl b/src/ChainedFixes.jl index 8e098b9..9e43f0c 100644 --- a/src/ChainedFixes.jl +++ b/src/ChainedFixes.jl @@ -1,19 +1,21 @@ - module ChainedFixes -using Base: Fix2 +@doc let path = joinpath(dirname(@__DIR__), "README.md") + include_dependency(path) + replace(read(path, String), r"^```julia"m => "```jldoctest README") +end ChainedFixes + +using Base: Fix1, Fix2, tail +using Base.Iterators: Pairs export + # Types ChainedFix, - # ands - and, - ⩓, + NFix, + # Constants And, - # ors - or, - ⩔, - Or, Approx, + Or, In, Less, Greater, @@ -25,7 +27,17 @@ export NotIn, NotApprox, EndsWith, - StartsWith + StartsWith, + # methods + and, + ⩓, + execute, + getargs, + getfxn, + getkwargs, + is_fixed_function, + or, + ⩔ if length(methods(isapprox, Tuple{Any})) == 0 Base.isapprox(y; kwargs...) = x -> isapprox(x, y; kwargs...) @@ -40,6 +52,7 @@ const StartsWith{T} = Fix2{typeof(startswith),T} if length(methods(endswith, Tuple{Any})) == 0 Base.endswith(s) = Base.Fix2(endswith, s) end + const EndsWith{T} = Fix2{typeof(endswith),T} const Not{T} = (typeof(!(sum)).name.wrapper){T} @@ -63,7 +76,6 @@ const GreaterThanOrEqual{T} = Fix2{typeof(>=),T} const LessThanOrEqual{T} = Fix2{typeof(<=),T} # Compose ∘ - # schroeder et al., Cerebral cortex 1998 # Schroeder, Mehta, Foxe, Front Biosc, 2001 # Daniel polland - adaptive resonance @@ -159,4 +171,167 @@ const And{F1,F2} = ChainedFix{typeof(and),F1,F2} const Or{F1,F2} = ChainedFix{typeof(or),F1,F2} +# TODO Should have better arguments here +struct NFix{Positions,F<:Function,Args<:Tuple,Kwargs<:Pairs} <: Function + f::F + args::Args + kwargs::Kwargs + + function NFix{P,F,Args,Kwargs}( + f::F, + args::Args, + kwargs::Kwargs + ) where {P,F,Args<:Tuple,Kwargs<:Pairs} + + if !isa(P, Tuple{Vararg{Int}}) + error("Positions must be a tuple of Int") + elseif length(P) != length(args) + error("Number of fixed positions and fixed arguments must be equal," * + " received $(length(P)) positions and $(length(args)) positional arguments.") + elseif !issorted(P) + error("Positions must be sorted, got $P.") + else + return new{P,F,Args,Kwargs}(f, args, kwargs) + end + end + + function NFix{P}(f::F, args::Args, kwargs::Kwargs) where {P,F,Args<:Tuple,Kwargs<:Pairs} + return NFix{P,F,Args,Kwargs}(f, args, kwargs) + end + + NFix{P}(f, args...; kwargs...) where {P} = NFix{P}(f, args, kwargs) + + function NFix(f, args::NTuple{N,Any}, kwargs::Pairs) where {N} + return NFix{ntuple(i -> i, Val(N))}(f, args, kwargs) + end + + NFix(f, args...; kwargs...) = NFix(f, args, kwargs) +end + +makeargs(p::Tuple{}, argsnew::Tuple{}, args::Tuple{}, cnt::Int) = () +makeargs(p::Tuple{}, argsnew::Tuple, args::Tuple{}, cnt::Int) = argsnew +makeargs(p::Tuple, argsnew::Tuple{}, args::Tuple, cnt::Int) = args +@inline function makeargs(p::Tuple, argsnew::Tuple, args::Tuple, cnt::Int) + if first(p) === cnt + return (first(args), makeargs(tail(p), argsnew, tail(args), cnt + 1)...) + else + return (first(argsnew), makeargs(tail(p), tail(argsnew), args, cnt + 1)...) + end +end + +### +### Traits +### + +""" + is_fixed_function(f) -> Bool + +Returns `true` if `f` is a callable function that already has arguments fixed to it. +A "fixed" function can only be called on one argument (e.g., `f(arg)`) and all other +arguments are already assigned. Functions that return true should also have `getargs` +defined. +""" +is_fixed_function(::T) where {T} = is_fixed_function(T) +is_fixed_function(::Type{T}) where {T} = false +is_fixed_function(::Type{<:Fix2}) = true +is_fixed_function(::Type{<:Fix1}) = true +is_fixed_function(::Type{<:Approx}) = true +is_fixed_function(::Type{<:ChainedFix}) = true +is_fixed_function(::Type{<:NFix}) = true +is_fixed_function(::Type{<:Not}) = true + + +""" + getargs(f) -> Tuple + +Return a tuple of fixed positional arguments of the fixed function `f`. + +## Examples + +```jldoctest +julia> using ChainedFixes + +julia> getargs(==(1)) +(1,) + +``` +""" +getargs(x) = (x,) +getargs(x::Fix2) = (getfield(x, :x),) +getargs(x::Fix1) = (getfield(x, :x),) +getargs(x::Approx) = (getfield(x, :y),) +getargs(x::NFix) = getfield(x, :args) +getargs(x::Not) = getargs(getfield(x, :f)) +getargs(x::ChainedFix) = (getfield(x, :f1), getfield(x, :f2)) + +""" + getkwargs(f) -> Pairs + +Return the fixed keyword arguments of the fixed function `f`. + +## Examples + +```jldoctest +julia> using ChainedFixes + +julia> getkwargs(isapprox(1, atol=2)) +pairs(::NamedTuple) with 1 entry: + :atol => 2 + +``` +""" +getkwargs(x) = Pairs((), NamedTuple{(),Tuple{}}(())) +getkwargs(x::Approx) = getfield(x, :kwargs) +getkwargs(x::NFix) = getfield(x, :kwargs) +getkwargs(x::Not) = getkwargs(getfield(x, :f)) + +""" + getfxn(f) -> Function + +Given a fixed function `f`, returns raw method without any fixed arguments. +""" +getfxn(x) = identity +getfxn(x::ChainedFix) = getfield(x, :link) +getfxn(x::Function) = x +getfxn(x::NFix) = getfield(x, :f) +getfxn(x::Fix1) = getfield(x, :f) +getfxn(x::Fix2) = getfield(x, :f) +getfxn(x::Approx) = isapprox +getfxn(x::Not) = ! +getfxn(x::NotIn) = !in +getfxn(x::NotApprox) = !isapprox + +""" + positions(f) -> Tuple{Vararg{Int}} + +Returns positions of new argument calls to `f`. For example, `Fix2` would return (2,) +""" +positions(x) = () +positions(x::Fix1) = (1,) +positions(x::Fix2) = (2,) +positions(x::NFix{P}) where {P} = P +positions(x::Not) = positions(getkwargs(getfield(x, :f))) +positions(x::ChainedFix) = (1, 2) + +""" + execute(f, args...; kwargs...) -> f(args...; kwargs...) + +Executes function `f` with provided positional arugments (`args...`) and +keyword arguments (`kwargs...`). +""" +@inline execute(f, args...; kwargs...) = execute(f, args, kwargs) +@inline function execute(f, args::Tuple, kwargs::Pairs) + if is_fixed_function(f) + return getfxn(f)( + makeargs(positions(f), args, getargs(f), 1)...; + getkwargs(f)..., kwargs... + ) + else + return f(args...; kwargs...) + end +end + +(f::NFix)(args...; kwargs...) = execute(f, args, kwargs) + end # module + diff --git a/test/Project.toml b/test/Project.toml new file mode 100644 index 0000000..645baa0 --- /dev/null +++ b/test/Project.toml @@ -0,0 +1,3 @@ +[deps] +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" diff --git a/test/runtests.jl b/test/runtests.jl index 1ab5b35..e6336b2 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,50 +1,126 @@ -using Test, ChainedFixes +using Test +using ChainedFixes +using Documenter +using Base.Iterators: Pairs +using Base: Fix1 + +empty_pairs = Pairs((), NamedTuple{(),Tuple{}}(())) + +@test @inferred(ChainedFixes.positions(1)) == () +@test @inferred(ChainedFixes.positions(<(1))) == (2,) + +@test @inferred(getfxn(1)) == identity +@test @inferred(getfxn(+)) == + + +@test !is_fixed_function(1) + +@testset "Fix1" begin + fix1_fxn = Fix1(<, 1) + @test is_fixed_function(typeof(fix1_fxn)) + @test @inferred(getargs(fix1_fxn)) == (1,) + @test @inferred(getfxn(fix1_fxn)) == < + @test @inferred(getkwargs(fix1_fxn)) == empty_pairs + @test @inferred(ChainedFixes.positions(fix1_fxn)) == (1,) +end @testset "Not" begin - @test isa(!(+), Not) + notfxn = !(+) + @test isa(notfxn, Not) @test !isa(+, Not) + @test @inferred(is_fixed_function(typeof(notfxn))) + @test @inferred(getfxn(notfxn)) == ! + @test @inferred(getargs(notfxn)) == (+,) + @test @inferred(getkwargs(notfxn)) == empty_pairs + @test @inferred(ChainedFixes.positions(notfxn)) == () end @testset "In" begin - @test isa(in(1), In) + infxn = in(1) + @test isa(infxn, In) @test !isa(+, In) + + @test @inferred(is_fixed_function(typeof(infxn))) + @test @inferred(getfxn(infxn)) == in + @test @inferred(getargs(infxn)) == (1,) + @test @inferred(getkwargs(infxn)) == empty_pairs end @testset "NotIn" begin - @test isa(!in(1), NotIn) + notinfxn = !in(1) + @test isa(notinfxn, NotIn) @test !isa(!(==(1)), In) + + @test @inferred(is_fixed_function(typeof(notinfxn))) + @test @inferred(getfxn(notinfxn)) == !in + @test @inferred(getargs(notinfxn)) == (1,) + @test @inferred(getkwargs(notinfxn)) == empty_pairs end @testset "Approx" begin - @test isa(isapprox(1), Approx) + isapprox_fxn = isapprox(1; atol=2) + @test isa(isapprox_fxn, Approx) @test !isa(!(==(1)), Approx) + + @test @inferred(is_fixed_function(typeof(isapprox_fxn))) + @test @inferred(getfxn(isapprox_fxn)) == isapprox + @test @inferred(getargs(isapprox_fxn)) == (1,) + @test @inferred(getkwargs(isapprox_fxn)) == Pairs((atol=2,),(:atol,)) end @testset "NotApprox" begin - @test isa(!isapprox(1), NotApprox) - @test !isa(isapprox(1), NotApprox) + notisapprox_fxn = !isapprox(1; atol=2) + @test isa(notisapprox_fxn, NotApprox) + @test !isa(!(==(1)), NotApprox) + + @test @inferred(is_fixed_function(typeof(notisapprox_fxn))) + @test @inferred(getfxn(notisapprox_fxn)) == !isapprox + @test @inferred(getargs(notisapprox_fxn)) == (1,) + @test @inferred(getkwargs(notisapprox_fxn)) == Pairs((atol=2,),(:atol,)) end @testset "Less" begin - @test isa(<(1), Less) + ltfxn = <(1) + @test isa(ltfxn, Less) @test !isa(isapprox(1), Less) + + @test @inferred(is_fixed_function(typeof(ltfxn))) + @test @inferred(getfxn(ltfxn)) == < + @test @inferred(getargs(ltfxn)) == (1,) + @test @inferred(getkwargs(ltfxn)) == empty_pairs end @testset "Equal" begin - @test isa(==(1), Equal) + eqfxn = ==(1) + @test isa(eqfxn, Equal) @test !isa(isapprox(1), Equal) + + @test @inferred(is_fixed_function(typeof(eqfxn))) + @test @inferred(getfxn(eqfxn)) == == + @test @inferred(getargs(eqfxn)) == (1,) + @test @inferred(getkwargs(eqfxn)) == empty_pairs end @testset "EndsWith" begin + endswith_fxn = endswith("i") @test endswith("i")("hi") @test endswith("i") isa EndsWith + + @test @inferred(is_fixed_function(typeof(endswith_fxn))) + @test @inferred(getfxn(endswith_fxn)) == endswith + @test @inferred(getargs(endswith_fxn)) == ("i",) + @test @inferred(getkwargs(endswith_fxn)) == empty_pairs end @testset "StartsWith" begin + startswith_fxn = startswith("h") @test startswith("h")("hi") @test startswith("h") isa StartsWith -end + @test @inferred(is_fixed_function(typeof(startswith_fxn))) + @test @inferred(getfxn(startswith_fxn)) == startswith + @test @inferred(getargs(startswith_fxn)) == ("h",) + @test @inferred(getkwargs(startswith_fxn)) == empty_pairs +end @testset "and" begin @test and(true, <(5))(1) @@ -60,6 +136,12 @@ end @test @inferred(and(>(10), >(1))) == >(10) @test @inferred(and(>=(1), >=(10))) == >=(10) @test @inferred(and(>=(10), >=(1))) == >=(10) + + and_fxn = and(true, <(5)) + @test @inferred(is_fixed_function(typeof(and_fxn))) + @test @inferred(getfxn(and_fxn)) == and + @test @inferred(getargs(and_fxn)) == (true, <(5)) + @test @inferred(ChainedFixes.positions(and_fxn)) == (1, 2) end @testset "or" begin @@ -74,4 +156,73 @@ end @test @inferred(or(>(10), >(1))) == >(1) @test @inferred(or(>=(1), >=(10))) == >=(1) @test @inferred(or(>=(10), >=(1))) == >=(1) + + or_fxn = or(true, <(5)) + @test getfxn(or_fxn) == or + @test getargs(or_fxn) == (true, <(5)) end + +fxn1(x::Integer, y::AbstractFloat, z::AbstractString) = Val(1) +fxn1(x::Integer, y::AbstractString, z::AbstractFloat) = Val(2) +fxn1(x::AbstractFloat, y::Integer, z::AbstractString) = Val(3) +fxn1(x::AbstractFloat, y::AbstractString, z::Integer) = Val(4) +fxn1(x::AbstractString, y::Integer, z::AbstractFloat) = Val(5) +fxn1(x::AbstractString, y::AbstractFloat, z::Integer) = Val(6) +fxn2(; x, y, z) = fxn1(x, y, z) +fxn3(args...; kwargs...) = (fxn1(args...), fxn2(; kwargs...)) + +fix1 = NFix{(1,2)}(fxn1, 1, 2.0) +@test @inferred(fix1("a")) === Val(1) + +fix2 = NFix{(1,3)}(fxn1, 1, 2.0) +@test @inferred(fix2("a")) === Val(2) + +fix3 = NFix{(1,3)}(fxn1, 1.0, "") +@test @inferred(fix3(2)) === Val(3) + +fix4 = NFix{(1,2)}(fxn1, 1.0, "") +@test @inferred(fix4(2)) === Val(4) + +fix5 = NFix{(2,3)}(fxn1, 1, 1.0) +@test @inferred(fix5("")) === Val(5) + +fix6 = NFix{(1,2,3)}(fxn1, "", 1.0, 1) +@test @inferred(fix6()) === Val(6) + +### kwargs +fix7 = NFix(fxn2, x=1, y=2.0) +@test @inferred(fix7(z = "a")) === Val(1) + +fix8 = NFix(fxn2, x=1, z=2.0) +@test @inferred(fix8(y="a")) === Val(2) + +fix9 = NFix(fxn2, x=1.0, z="") +@test @inferred(fix9(y=2)) === Val(3) + +fix10 = NFix(fxn2, x=1.0, y="") +@test @inferred(fix10(z=2)) === Val(4) + +fix11 = NFix(fxn2, y=1, z=1.0) +@test @inferred(fix11(x="")) === Val(5) + +fix12 = NFix(fxn2, x="", y=1.0, z=1) +@test @inferred(fix12()) === Val(6) + +fix13 = NFix{(1,2)}(fxn3, 1, 2.0; x=1.0, z="") +@test @inferred(fix13(""; y = 1)) === (Val{1}(), Val{3}()) + +# positions must be NTuple{N,Int} +@test_throws ErrorException NFix{(1.0,2,3)}(fxn1, "", 1.0, 1) +# positions aren't sorted +@test_throws ErrorException NFix{(1,3,2)}(fxn1, "", 1.0, 1) +# position and args aren't same length +@test_throws ErrorException NFix{(1,3,2)}(fxn1, "", 1.0) + +@test is_fixed_function(fix1) + +@test @inferred(ChainedFixes.execute(+, 1, 2)) == 3 + +@testset "docs" begin + doctest(ChainedFixes) +end +