Skip to content
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

Dynamic creation of lenses #73

Closed
singularitti opened this issue Aug 13, 2019 · 15 comments
Closed

Dynamic creation of lenses #73

singularitti opened this issue Aug 13, 2019 · 15 comments

Comments

@singularitti
Copy link

singularitti commented Aug 13, 2019

Could we have a syntax that supports dynamic creation of lenses? Currently, if I want to create some lens, I need to write

@lens _.prop1.prop2.prop3

But sometimes I may have properties that are stored in variables. So I want to request a syntax like

@lens :prop1 :prop2 :prop3
@lens var1 var2 var3
# equivalent to @lens _.prop1.prop2.prop3

where :prop1, :prop2 and :prop3 are just symbols, usually got from the fieldnames function. _ is omitted there. And var1 = :prop1, var2 = :prop2, var3 = :prop3, respectively.
Is this possible?

@singularitti singularitti changed the title Dynamic creation of lens Dynamic creation of lenses Aug 13, 2019
@tkf
Copy link
Collaborator

tkf commented Aug 13, 2019

Is this what you need?

julia> using Setfield

julia> a = Setfield.PropertyLens{:a}()
(@lens _.a)

julia> b = Setfield.PropertyLens{:b}()
(@lens _.b)

julia> a  b
(@lens _.a.b)

@singularitti
Copy link
Author

Thank @tkf, I am aware of this syntax. Just curious whether an easier way of doing this is possible.

@tkf
Copy link
Collaborator

tkf commented Aug 13, 2019

Hmm... I think writing PropertyLens{name}() etc. is not too bad if you are doing metaprogramming. The syntax you need is just one linear:

julia> mapfoldl(x -> Setfield.PropertyLens{x}(), , [:prop1, :prop2, :prop3])
(@lens _.prop1.prop2.prop3)

@singularitti
Copy link
Author

singularitti commented Aug 13, 2019

Ha, I like this one-liner! Thank you! Can this one-liner be added to public APIs and documentation?

@tkf
Copy link
Collaborator

tkf commented Aug 13, 2019

I don't know if it worth mentioning since it's just another way to write

l = @lens _
for name in [:prop1, :prop2, :prop3]
    l = l  Setfield.PropertyLens{name}()
end
l

I think an actionable item here is to expose Setfield.PropertyLens etc. as public APIs (i.e., add docstring and include in the documentation). Maybe wait for what @jw3126 says and you can make a PR?

@singularitti
Copy link
Author

singularitti commented Aug 13, 2019

Thank you @tkf. That's a good idea! I prefer writing

makelens(props::AbstractVector{Symbol}) = mapfoldl(x -> Setfield.PropertyLens{x}(), , props)

since the latter one is less readable. I can make a PR. The reason why I want an API to handle this is that I do not want to write something dealing with lenses in my package, which has nothing to do with lenses. That's what a package like this should provide.

@jw3126
Copy link
Owner

jw3126 commented Aug 13, 2019

I am okay with @tkf s suggestion to make PropertyLens part of the public API. A PR for that would be very welcome! As for the makelens I try hard to keep this package minimalistic. If in doubt, leave it out so I am against having this function here.

@tkf
Copy link
Collaborator

tkf commented Aug 13, 2019

I'm mild -1 to adding such function as it's just onelinear. Having said that, one justification would be that a function

propertylens(name::Symbol) = Propertylens{name}()
propertylens(names::Symbol...) = mapfoldl(propertylens, , names)

is somewhat similar to Base.string which could be implemented as

string(x) = String(x)
string(xs...) = mapfoldl(string, *, xs)

@jw3126
Copy link
Owner

jw3126 commented Aug 13, 2019

Building a string is a much more common operation then what we do here though. Also propertylens(names...) would not return a PropertyLens.

Still I really like the syntax much better then makelens and I don't like mandatory curly braces in public APIs like PropertyLens{name}(). So I can maybe imagine having this here...

@singularitti, @tkf Do you have some open source code where you would like to use this/already use this?

@singularitti
Copy link
Author

Hi @jw3126, I do not have an open-source code using this. But I may want to use this feature in the near future.

@tkf
Copy link
Collaborator

tkf commented Aug 13, 2019

Also propertylens(names...) would not return a PropertyLens.

That's the reason why I didn't call PropertyLens(names...). I expect CamelCase(...) :: CamelCase to hold (most of the time) but I thought it's more OK if it is a factory function.

But this signature does not match well for index lens, as maybe you'd want

indexlens(idx...) = IndexLens(idx)

so that indexlens(1) === @lens _[1] and indexlens(1, 2) === @lens _[1, 2]. So I'm not sure if it is a good idea.

I don't like mandatory curly braces in public APIs like PropertyLens{name}()

I agree. If we are to make the constructor an API, I think it's better to have PropertyLens(name::Symbol) = PropertyLens{name}(), like Val does.

Do you have some open source code where you would like to use this/already use this?

No, I don't have a usecase. I'm totally OK with writing @lens _.property.

When I touch PropertyLens, it's more about consuming as an argument; e.g., propname(::PropertyLens{name}) where name = name

@singularitti
Copy link
Author

singularitti commented Aug 13, 2019

My use case may be:
I have a nested type with many fields (so that my users would want to create lenses by code rather than by hand). And I want to help my users (who are not familiar with lenses and teaching them this may scare them away) so that they do not need to know how to create a lens, get/set a lens' value. All they need to know is what fields (nested) they want to modify, and I take care of the rest. For example,

function modify_fields(obj::MyNestedType, val, props...)
    lens = mapfoldl(x -> Setfield.PropertyLens{x}(), , [props...])
    set(obj, lens, val)
end

where props are a single/vector of fieldnames of each type nested in MyNestedType.
For example,

struct M
    c::N
end

struct N
    d
end

struct A
    a::M
    b
    e
end

x = A(M(N(1)), 2, 4)
modify_fields(x, 3, :a, :c:, :d)
# return y with y.a.c.d = 3

@tkf
Copy link
Collaborator

tkf commented Aug 13, 2019

who are not familiar with lenses and teaching them this may scare them away

Is it that the name "lens" feels out of place? Maybe @at _.a.b.c or something is better? Or is it macro that is scary?

@singularitti
Copy link
Author

I think it makes more senses to those who have seen Haskell's lens, so let's keep this name. And if a user uses @at but sees a Lens type, he/she would be confused.

@jw3126
Copy link
Owner

jw3126 commented Aug 15, 2019

Note that your modify_fields will introduce type instability. Also if your users are expected to programatically create accessors to many fields, it might be reasonable to expose them to some lens basics.
I agree that Haskell lens library takes time to learn (I only know a small part of it myself), but basic @lens _.a.b is pretty intuitive I think.

As for changes in this library I am ok with documenting+exporting

PropertyLens(name::Symbol) = PropertyLens{name}()

but not propertylens for know.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants