Open oschulz opened 10 months ago
This would also enable constructions like
f1(x) = (a = x^2, b = 2*x)
f2(x) = (c = x^3, b = 3*x)
fchain(fcoll((f1, f2)), splat(merge))(x)
A specializable fcombine(op, fs)
with default implementation
fcombine(op, fs) = fchain(fcoll((f1, f2)), splat(op))
could also be very useful in this context.
Neat convergence of design approaches from different directions :)
julia> using AccessorsExtra
# like your fcoll(a=first, b=last):
julia> f = @optic₊ (a=first(_), b=last(_))
# non-macro interface also available:
julia> f === AccessorsExtra.ContainerOptic((a=first, b=last))
true
julia> x = [1, 2, 3, 4]
# can call as a function:
julia> f(x)
(a = 1, b = 4)
# and it's more than just an anonymous function:
julia> @set f(x) = (a=10, b=20)
4-element Vector{Int64}:
10
2
3
20
# opposite keys order + non-macro
julia> set(x, f, (b=10, a=20))
4-element Vector{Int64}:
20
2
3
10
Oh, very nice @aplavin ! Hm, maybe we don't even need this in FunctionChains.
Does AccessorsExtra.ContainerOptic
always represent a callable object?
Yeah, the full implementation of ContainerOptic
as a callable is effectively two lines: https://github.com/JuliaAPlavin/AccessorsExtra.jl/blob/00000000cef99a28debfb7cc31321eef769b8c87/src/concatoptic.jl#L100-L105.
It could use map
as you suggested instead of modify(values)
if not for Dict
s. I'm not really sure if supporting dicts is really useful there, only use Tuple/NamedTuple/SVector myself...
This simplicity indicates that such an object can easily live in a much lighter package, if useful in isolation. If such an object gets added somewhere (eg here), I think I can probably reuse it in AccessorsExtra and just define set()
... Probably the interplay with PropertyFunction and similar objects can be resolved in a reasonably clean way.
What does ContainerOptic
do for non-callables, actually?
Don't think it makes much sense for non-callables. I mean, you can create the ContainerOptic
object just fine no matter what's inside, there's just no actual functionality in that case.
What do you have in mind?
What do you have in mind?
I was just wondering if it might make sense to make it a subtype of Function
, if it has no non-callable use case.
Accessors callables don't subtype functions: eg, https://github.com/JuliaObjects/Accessors.jl/blob/dc42b02b21bc229d5bcd91071701dbec0b084095/src/optics.jl#L359. That's why I also don't do this in AccessorsExtra.
For some discussion, see https://github.com/JuliaObjects/Accessors.jl/issues/37.
And what concrete benefit would that bring? Dispatching on Function in Julia isn't recommended anyway.
Dispatching on Function in Julia isn't recommended anyway.
No, it would be more to express semantics. Base.ComposedFunction
also subtypes Function
, for example. But it's certainly not necessary.
Thought about this a bit more, fcoll
may not be an ideal name here, because it doesn't clarify if we're building a function that operates on "collections" or a function the generates "collections".
There are (at least) two kind of generalized products for functions - in respect to the input and in respect to the output:
For measures (which are functions on sets, semantically), probability densities and the like, we want something like
h = input_fprod((f, g), NamedTuple{(:a, :b)})
h(x) = f(x.a) * g(x.b)
In other cases, we may want
h = output_fprod((a = f, b = g))
h(x) = (a = f(x), b = g(x))
and in yet other cases
h = output_fprod(f, g, merge)
h(x) = merge(f(x), g(x))
I wonder if it's possible to find a "super-generalized" product that can cover all of these? This whole thing looks like it's going beyond the scope of FunctionChains though, it may deserve it's own package.
I wonder if it's possible to find a "super-generalized" product that can cover all of these?
This might work as a "super-generalized function product":
h = gfp(fs, fc_input, fc_output)
so that for h(x_h)
h(x_h) == fc_output(map(f -> f(x), fs, x_fs))
with
x_h == fc_input(x_fs)
Constructing x_fs
involves "inverting" the combinator fc_input
, so only ∘ unique
would turn into a fill operation, splat(merge)
into a splitting operation and so on. This would need to be specialized for most input combinators. It may seem backwards, but it's the only approach that provides what measures and the like need (it's what we're currently building for MeasureBase). This "input decomposition" would look like
x_fs = decompose_input(fs, fc_input, x_h)
With that, one may be able to do it all:
f(x.a) * f(x.b) == gfp((f, g), NamedTuple{(:a, :b)}, prod)((; a, b))
f((a = x.a, b = x.b)) * f((c = x.c, d = x.d)) == gfp((f, g), splat(merge), prod)((; a, b, c, d))
(a = f(x), b = g(x)) == gfp((a = f, b = h), only ∘ unique, identity)(x)
merge(f(x), g(x)) == gfp((f, g), only ∘ unique, splat(merge))(x)
(a = f(x.a), b = f(x.b)) == gfp((a = f, b = g), identity}, identity)((; a, b))
Whethers it's pracical and useful is a different question, of course. Input combinators like splat(merge)
and splat(vcat)
would only work if the element functions in fs
have a known input space, like measures and probability density functions (usually) do.
This would add something like
So that
Would be good to support
fcoll(a = f1, b = f2, c = f3)
,fcoll(f1, f2, cf3)
andfcoll(f1)
as will, similar to howfchain
handles tuples of functions.