Giter Site home page Giter Site logo

nesting about fixargs.jl HOT 10 CLOSED

goretkin avatar goretkin commented on May 12, 2024
nesting

from fixargs.jl.

Comments (10)

goretkin avatar goretkin commented on May 12, 2024

@jw3126 I didn't think about how or whether this should work with keyword arguments. Do you have any thoughts?

from fixargs.jl.

goretkin avatar goretkin commented on May 12, 2024

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.

jw3126 avatar jw3126 commented on May 12, 2024

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.

goretkin avatar goretkin commented on May 12, 2024

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):

FixArgs.jl/test/runtests.jl

Lines 253 to 280 in d6b7af8

@testset "imaginary unit as lazy and static *, and `Complex` as lazy +, using `reinterpret`" begin
# This example is a bit circular, since `im` is already `Complex`. But suppose there were some `ImaginaryUnit()` singleton.
# the "besides the point" evaluations would fail, but the overall point stands that:
# 1. imaginary numbers can be representeded with lazy multiplication with `ImaginaryUnit()`
# 2. complex numbers can be represented with lazy addition of real numbers and imaginary numbers
typed_data = [i + 10*i*im for i = 1:10]
untyped_data = reinterpret(Int, typed_data)
MyComplex_instance = (@fix (@fix _ + @fix(_ * Val{im}()))(1, (2, )))
# that this works is kind of besides the point
@test MyComplex_instance() === 1 + 2im
# the point is that `MyComplex` is a memory representation of a complex number made from two `Int`s
# `MyComplex` is just an alias for the structure of the type
MyComplex = typeof(MyComplex_instance)
new_typed_data = reinterpret(MyComplex, untyped_data)
@test map(x->x(), new_typed_data) == typed_data # again, kind of besides the point
# note that there are other structures possible which would give the same results
# for example `Val{im}() * _` instead of `_ * Val{im}()` gives a different structure with the same result
# swapping the arguments to `+` would give a different memory layout.
# the point is also that you could define `*(::MyComplex, ::MyComplex)::MyComplex`
# and that you can do so without introducing any new type names
end

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 Disks.

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 and Package2 and they could each define their own Package3.RingAnon, and Package4.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 RingAnons, 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.

jw3126 avatar jw3126 commented on May 12, 2024

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.

goretkin avatar goretkin commented on May 12, 2024

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.

jw3126 avatar jw3126 commented on May 12, 2024

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.

goretkin avatar goretkin commented on May 12, 2024

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.

goretkin avatar goretkin commented on May 12, 2024

That brings up a question of how to implement e.g.

@structural x -> (y -> f(x, y))

Here's one way:

FixArgs.jl/test/runtests.jl

Lines 319 to 332 in 3572230

foo = x -> (y -> *(x, y))
foo1 = Fix(
Fix,
Template((
Some(*),
Template((
ArgPos{1}(),
Some(ArgPos{1}())
))
))
)
@test foo("a")("b") == foo1("a")("b")

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:

  1. Instead of Expr, the type reflects the head and the type of args
  2. Instead of args::Array{Any}, it is a Tuple.
  3. Instead of :_1, it is ArgPos{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.

goretkin avatar goretkin commented on May 12, 2024

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)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo 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.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.