New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
nesting #16
Comments
@jw3126 I didn't think about how or whether this should work with keyword arguments. Do you have any thoughts? |
Perhaps something like
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 |
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.
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 Of course, there might be some benefits, that I am just not seeing. Can you give some examples, where this shines? |
Thanks for commenting, @jw3126 !
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 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 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 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 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 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 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 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 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 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 # 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 Furthermore suppose I have
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 But you can avoid making a 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 |
Thanks a lot for the detailed post. I did not consider something like the
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)) |
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 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 (c, r1) -> setdiff(Disk(c,r1), Disk((0, 0), 2)) do?
And whatever the answer is, there should be a syntax for achieving the opposite. |
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) |
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.
(where both lambdas are structural). What doesn't work is e.g.
It's a bit funny, because that definition itself is a curried version of |
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:
It might be fine to bake in @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 |
I went with this, and allow eager evaluation with Now that |
There are some cases where I want something roughly like
and
to work.
There needs to be a mechanism to prevent this nesting (that is, to maintain the current behavior). Currently
and so the encoding for the nesting behavior should probably just not wrap the
Fix{typeof(+),...}
inSome
. That is, I desire the following:and
The text was updated successfully, but these errors were encountered: