Comments (10)
@jw3126 I didn't think about how or whether this should work with keyword arguments. Do you have any thoughts?
from fixargs.jl.
Perhaps something like
julia> @fix NamedTuple{(:a, :b)}((:a_call, _))
ERROR: syntax: all-underscore identifier used as rvalue
or
julia> @fix NamedTuple{(:a, :b)}(tuple(:a_call, _))
ERROR: syntax: all-underscore identifier used as rvalue
should work directly. And it would be different from
@fix NamedTuple{(:a, :b)}(@fix tuple(:a_call, _))
because the arguments would not have to be nested.
Whatever happens @fix f(g(1))
should evaluate g(1)
eagerly. If g
should be evaluated lazily, then either @fix f(@fix g(1))
or define a new macro (e.g. @deepfix
).
from fixargs.jl.
It is nice, to see that there is a lot of activity in this repo lately. About supporting nesting it is a tough design choice.
I thought about it a bit and I cannot really come up with a syntax that:
- Is intuitive + easy to read
- More ergonomic then lambda
Personally, I think I would just use anonymous functions for nested cases.
f = x -> (1 + x)/3
f(2)
I think the proposed syntax will add extra complexity and encourage code that is hard to read for those without a degree in FixArgs.jl
.
Of course, there might be some benefits, that I am just not seeing. Can you give some examples, where this shines?
from fixargs.jl.
Thanks for commenting, @jw3126 !
Personally, I think I would just use anonymous functions for nested cases.
Of course, there might be some benefits, that I am just not seeing. Can you give some examples, where this shines?
I will do my best to explain. Sorry in advance if this isn't clear. I'm afraid that what I write below may validate your concern about requiring a degree in FixArgs.jl
:-X.
At a high level, the benefit is the same as having an alternative for anonymous functions in the first place. Anonymous functions are given a "gensym" name in the type system, and FixArgs.jl
gives functions a systematic name in the type system. The systematic name may be cumbersome, and so it may be worth giving a better name as an alias, but that's outside the type system, so to speak.
Because of the gensym'd name, the type of an anonymous function is just a name, as opposed to a "signature" of the function (which incidentally poses a challenging for serialization. It reminds me of "content addressing" (e.g. with a hash of the content) vs location addressing.
Still at a high level, if there are cases where it's beneficial to replace an anonymous function with a Fix
, and if there are cases where it's beneficial to have nested anonymous functions, then I think it follows naturally that Fix
within Fix
is beneficial.
More concretely, here is one motivating example for nested fix (and the static argument feature):
Lines 253 to 280 in d6b7af8
It is admittedly a bit contrived. Less contrived is to represent something like
struct Ring{P, R1, R2}
center::P
radius_inner::R1
radius_outer::R2
end
Ring(c, r1, r2)
in terms of setdiff
and
struct Disk{P, R}
center::P
radius::R
end
One attempt with no nesting is:
@fix setdiff(Disk(c, r1), Disk(c, r2))
that type represents the center twice, and independently. As an illustration:
julia> sizeof(Ring((0,0), 1, 2))
32
julia> sizeof(@fix setdiff(Disk((0,0), 1), Disk((0,0), 2)))
48
And even if memory is not a concern, you still lose out of performance because you might have to assert that the two centers are equal instead of guaranteeing that by construction. If performance is not a concern, maybe static type safety is (though this is a handwavy argument because I don't know what may come of tooling for static analysis in Julia).
Nested Fix
wasn't used above, since it wasn't necessary. Let me try to facilitate what's coming by showing it would be used unnecessarily:
RingTwoCenters = @fix setdiff((@fix Disk(_, _)), (@fix Disk(_, _)))
@fix RingTwoCenters(((0, 0), 1), ((0,0), 2)) # note, this is not an example of nesting `Fix`
Back to the goal of sharing the center
. What we want instead is something like
RingAnon = (c, r1, r2) -> @fix setdiff(Disk(c, r1), Disk(c, r2))
@fix RingAnon(c, r1, r2)
Continuing with the illustration:
julia> sizeof(@fix RingAnon((0,0), 1, 2))
32
So now we have the right representation, where there is only one value for center
that is shared by both Disk
s.
However,
julia> typeof(@fix RingAnon((0,0), 1, 2))
Fix{var"#23#24",Tuple{Some{Tuple{Int64,Int64}},Some{Int64},Some{Int64}},NamedTuple{(),Tuple{}}}
I'll elaborate on why the anonymous function is a problem shortly, but I hope it's clear that recursive Fix
is part of the solution. For example, if instead of _
we could name argument positions with _1
, and _2
I would define something like:
Ring = @fix setdiff(@fix Disk(_1, _2), @fix Disk(_1, _3))
@fix Ring((0,0), 1, 2)) # note, this is not an example of nesting `Fix`
so the inner Fix
is needed because Disk(_1, _2)
cannot be evaluated. Another possibility is to try to allow something like
Ring = @fix setdiff(@fix Disk(_.center, _.r1), @fix Disk(_.center, _r2))
@fix Ring((;center=(0,0), r1=1, r2=2)) # note, this is not an example of nesting `Fix`
Back to what's wrong with RingAnon
: it has all the problems of anonymous functions. If I had a special way to plot ring shapes, I might want to dispatch on that type. I could use ::type(RingAnon)
, but then why have Base.Fix1
and Base.Fix2
? Here's an example of accomplishing (through use of internal behavior, so not reliable) what Fix2
does by using anonymous functions:
# compare to what is in Base:
# `isequal(x) = Fix2(isequal, x)``
# and
# `(f::Fix2)(y) = f.f(y, f.x)`
Fix2_isequal(x) = y -> isequal(y, x)
# I am not sure if this anonymous closure is guaranteed to be a single type parameterized by `typeof(x)`
# but rely on that behavior for the sake of example.
Type_Fix2_isequal(T) = only(Base.return_types(Fix2_isequal, (T,)))
# e.g. `Type_Fix2_isequal(Int64)` is `var"#119#120"{Int64}`
# and `Type_Fix2_isequal(Int32)` is `var"#119#120"{Int32}`
@assert Type_Fix2_isequal(Int64).name === Type_Fix2_isequal(Int32).name
# alternatively:
Type_Fix2_isequal_Int = let arbitrary = 0
typeof(Fix2_isequal(arbitrary::Int))
end
@assert Type_Fix2_isequal_Int === Type_Fix2_isequal(Int)
# compare to what is in Base:
# ```
# findfirst(p::Fix2{typeof(isequal),Int}, r::OneTo{Int}) =
# 1 <= p.x <= r.stop ? p.x : nothing
# ```
# Accessing `p.x` is internal behavior (https://github.com/JuliaLang/julia/issues/34051#issuecomment-563297778)
# but rely on that behavior for the sake of example.
Base.findfirst(p::Type_Fix2_isequal(Int), r::Base.OneTo{Int}) =
1 <= p.x <= r.stop ? p.x : nothing
findfirst(Fix2_isequal(3), Base.OneTo(10)) # will dispatch to method defined above
Even if we wanted to do something like that and keep RingAnon
, there is another motivation to avoid it to do with packages interoperating. Suppose that there are two packages, Package1.jl
defines the setdiff
function (really it's in Base
, but pretend), and Package2.jl
defines the type Disk
. Let's also take for granted that we don't want any dependency relationship between Package1
and Package2
.
Furthermore suppose I have Package3
and Package4
that both need to use ring-like objects. They have the following option:
- They could each define
struct Ring{P, R1, R2}
as above. But the point throughout is to avoid that and define "ring" in terms of existing names in the type system. - So instead they could each depend on both
Package1
andPackage2
and they could each define their ownPackage3.RingAnon
, andPackage4.RingAnon
as above.
In both of those cases, the two packages cannot interoperate with "ring" objects. Both cases can be helped by defining another package that defines "ring". For the second case, you can define Package12
that depends on Package1
and Package2
and defines RingAnon
as above. So now instead of two RingAnon
s, there is only Package12.RingAnon
.
But you can avoid making a Package12
if there is just a systematic name you can give to the "ring" based on Package1.setdiff
and Package2.Disk
. FixArgs.jl
lets you glue these two names together. And so finally Package3
and Package4
each depend on Package1
, Package2
, and FixArgs.jl
, and they can interoperate with "ring" types.
It is possible that this whole endeavor is too complicated (and worse, unbearably taxing on the Julia compiler), and that there should just be a package that defines struct Ring
... but I think it's worth an experiment.
from fixargs.jl.
Thanks a lot for the detailed post. I did not consider something like the Ring
example at all. Do I get the following right:
- You are mainly after "structural" lambdas as opposed to nominal ones.
- This is not mainly about alternate syntax for lamdas
- You want to be able to dispatch on nested fix
If so I think syntax wise the most intuitive thing might be just to reuse lambda syntax
@structural (c, r1, r2) -> setdiff(Disk(c,r1), Disk(c, r2))
from fixargs.jl.
You are mainly after "structural" lambdas as opposed to nominal ones.
@structural (c, r1, r2) -> setdiff(Disk(c,r1), Disk(c, r2))
That is an interesting idea. I want to clarify that these "structural lambdas" do not need to be executable. For example, perhaps there is no method setdiff(::Disk, ::Disk)
. That means that
Ring = @structural (c, r1, r2) -> setdiff(Disk(c,r1), Disk(c, r2))
Ring((0, 0), 1, 2)
does not need to work, but to represent a ring you would still do
@fix Ring((0, 0), 1, 2)
Whether the default is "eager" or "lazy", there probably needs to be a way to mark the opposite of the default. Is @structural
"deep" lazy? Or is it only lazy with unbound arguments? More clearly, what does
@structural (c, r1) -> setdiff(Disk(c,r1), Disk((0, 0), 2))
do?
- it does not evaluate
Disk((0, 0), 2)
because the macro never evaluates anything - it does evaluate
Disk((0, 0), 2)
because there are no free parameters
And whatever the answer is, there should be a syntax for achieving the opposite.
from fixargs.jl.
I had deep lazyness in mind. If you want eager evaluation, I would just to it explicitly:
disk = Disk((0, 0), 2)
@structural (c, r1) -> setdiff(Disk(c,r1), disk)
from fixargs.jl.
I think that's a good default, and I think that it's perfect to think of the idea as "structural lambdas". That brings up a question of how to implement e.g.
@structural x -> (y -> f(x, y))
(where both lambdas are structural).
What doesn't work is e.g.
Fix(Fix, (Some(*), Some((Some(nothing), nothing)), Some(NamedTuple())), NamedTuple())
It's a bit funny, because that definition itself is a curried version of f
.
from fixargs.jl.
That brings up a question of how to implement e.g.
@structural x -> (y -> f(x, y))
Here's one way:
Lines 319 to 332 in 3572230
it uses "positional argument holes", so to speak. It is in the direction of having typed expressions. Roughly:
julia> ==(2)
(::Base.Fix2{typeof(==),Int64}) (generic function with 1 method)
is a special case of the following untyped expression:
julia> using MacroTools: striplines, flatten
julia> dump(flatten(striplines(:(_1 -> $(==)(_1, 2)))))
Expr
head: Symbol ->
args: Array{Any}((2,))
1: Symbol _1
2: Expr
head: Symbol call
args: Array{Any}((3,))
1: == (function of type typeof(==))
2: Symbol _1
3: Int64 2
Imagine there is type information throughout:
- Instead of
Expr
, the type reflects thehead
and the type ofargs
- Instead of
args::Array{Any}
, it is a Tuple. - Instead of
:_1
, it isArgPos{1}()
It might be fine to bake in :call
. But there needs to be a way to distinguish between
@structural (c, r1, r2) -> setdiff(Disk(c, r1), Disk(c, r2))
and
@structural (c, r1, r2) -> setdiff(() -> Disk(c, r1), () -> Disk(c, r2))
I think we can keep the Fix
type, and nest it as I showed above, and introduce a new type to distinguish between those two possibilities.
from fixargs.jl.
I had deep lazyness in mind. If you want eager evaluation, I would just to it explicitly
I went with this, and allow eager evaluation with $(...)
within an @xquote
.
Now that Fix
is split into Lambda
and Call
, it's fairly straightforward to think about nesting.
from fixargs.jl.
Related Issues (19)
- simplify type parameter corresponding to keyword arguments HOT 1
- positional placeholder ("holes") HOT 1
- reify function arguments on their own, e.g. `FrankenTuples` HOT 1
- TagBot trigger issue HOT 3
- Error message to suggest rewriting some syntax as a function call
- Do not use nothing for holes? HOT 29
- Mention related packages in README HOT 5
- `LazySets.jl` as case study HOT 1
- Connection to structural typing?
- Support or not for closures
- Related patterns HOT 4
- Broadcasting considerations HOT 2
- inference with nested lambdas
- Rename+export bind? HOT 4
- Version released to Pkg is old HOT 4
- Broadcast Functions not working
- Rename and register? HOT 4
- Macros to facilitate expressing types in methods HOT 6
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from fixargs.jl.