There are some functions and packages here and there
And there also some packages offering an OOP experience like
However I don't want to handpick which functions to forward neither care about how many variables there are or in what position is the field I want to forward.
I would like to forward everything everywhere all at once. :eye:
And just handpick which functions I don't want to forward so that I can implement them.
e.g. let's say I want to define my own SimpleGraph
variance that just adds vertices in my own funny way.
I would like to do something like (pseudocode)
using Graphs
# same subtyping and parameters as SimpleGraph
struct MyFunnySimpleGraph{T <: Integer} <: AbstractSimpleGraph{T}
sg::SimpleGraph{T}
end
@forwardeverythingeverywhereallatonce MyFunnySimpleGraph.sg except
Graphs.add_vertex!
end
function Graphs.add_vertex!(mfsg::MyFunnySimpleGraph)
# hilarious code
# ...
# end hilarious code
add_vertex!(mfsg.sg)
end
so I would expect even a random function defined like
function random_function_from_random_package(x::T, y::R) where {T,R}
# code
end
when called with random_function_from_random_package(10, mfsg)
(without use of macro)
to actually expand to
random_function_from_random_package(10, mfsg.sg)
is this even possible ?
So to understand the question, it’s a bit like the pub
idea in OOP where methods in a class are made public and available to the consumer when the object holding the methods is in scope?
Maybe adding an export myfun
below the function would solve your issue? Or write a macro to allow @export function myfun() … end
, but that doesn’t sound like the best idea. Best would probably to bite the bullet and manually define exports so that the code is easiest to understand.
Filippos Christou said:
is this even possible ?
No, though I wish it was. One good reason to not have it is that the semantics get messy when you consider multiple dispatch and being able to dynamically add methods.
If something like method_missing
could be made to work well in Julia, that would be quite interesting
I just realized this post actually wishes for of a solution to a bigger gripe, expressed also here https://viralinstruction.com/posts/badjulia/#the_type_system_works_poorly.
@Rik Huijzer
If I were to put it in OOP terms, I would just say subtyping structs. However knowing that this is impossible in julia I had some hopes for compositions and forwarding mechanisms, since partial solutions are already available.
@Brian Chen
interesting to see how Ruby approaches this. I think there are 2 separate discussions here:
i) should there be such functionality ?
ii) how would this be implemented or what should be added in the language in order for an implementation to be possible ?
arguing about i)
I agree that it might lead into shady territories, complicated patterns and hard-to-diagnose errors. However, programming is complicated and some times this is needed. Also, favoring code repetition and e.g. copying all SimpleGraph
methods and substituting the type with MyFunnnySimpleGraph
is a bigger programmatic sin.
So, I don't know what the exact repercussions would be, but I am sure it solves a big problem and a gripe for the language. At the same time it holds onto its principle of composition over inheritance.
Regarding ii)
, I had the impression metaprogramming was strong enough to get us wherever we wanted. I mean, using this, packages like OnjectOriented.jl
were built that provide a OOP experience. But I can see the challenge in this one. Ruby's method_missing
could be implemented right away with a try-catch
block, but it will be slow and bulky.
Then, I though maybe a traits mentality could get us out of this problem. After all (never used, from what I hear) Rust is only using traits to do its business. However I failed to find a trait pattern that implements such functionality.
If you want Julia to be OOP, then that’s asking for a painful experience. Julia is not OOP.
I don't want julia to be OOP. I just want the composition over inheritance experience to be easier. Merely an extension of the well known and widely used Lazy.@forward
.
I also often wish for some 'substitute struct X for struct Y except when I say so' type of magic.
A workaround I have used a couple of times is to have and abstract type DecoratingX
which forwards all method involving an x::DecoratingX
to wrapped(x)
. Only practically useful when there is a small number of methods but I believe this is more of a fundamental limitation to composability/reuse (e.g. it becomes more difficult to keep consistent behaviour the more you violate SRP or something). Obviously doesn't work on types where you can't rearrange the inheritance hierarchy.
if you're accessing object properties directly, you're setting yourself up for a world of pain imo
field access should always happen through functions and you want to have an abstract supertype in almost all cases, least of which to make testing easier
what you can do when you do have an abstract super type and use functions is that you can quite easily define a "do nothing" subtype for testing, that can return whatever you need, while still dispatching just like the "true" object
of course none of that matters when it's not your package, at which point you're at the mercy of the package dev to know about software engineering practices.. which we sorely lack, as a mostly scientific community
Sukera said:
if you're accessing object properties directly, you're setting yourself up for a world of pain imo
Yup, accessing through functions is pretty much a prerequisite for the pattern to work.
yeah, I dislike dot-access with a passion
you can sort-of fix it by writing custom getproperty
for all your testing subtypes, but that too is a world of unnecessary pain when the true interface (just write a damn function) is already there
not to mention that you may be bound to some other interface due to that field being concretely typed at definition, so you can't replace internals...
yeah. if all the codebase belongs to me, then all problems are easily solved. The hard part starts when I want to use a different source. I wish this was easy to solve.
DrChainsaw said:
I also often wish for some 'substitute struct X for struct Y except when I say so' type of magic.
I've asked about how to get these "phantom" types working on Slack before, but didn't get any bites. They would really useful for array wrappers.
Maybe an issue on Base would help carry the discussion forward
One issue I see with such a forwarding is that it is not clear oftentimes whether you want to return the wrapped field, or the original structure. Say if you have an array wrapper, how would you define empty!
on it? Typically, empty!
returns the emptied collection, in this case it should be the wrapper, but with forwarding it will return the result of empty!
called on the wrapped array, and so, the wrapped array will be returned. Same story for algebraic operations, where e.g. adding two array wrappers should return an array wrapper, not the addition of the wrapped arrays which are a different type. But there, you don't want to return one of the original arguments (as you would with empty!
), but actually rewrap the result into a new array wrapper. Then you have also problems with method ambiguities, where if you declare f(t::MyType, args...; kwargs...) = f(t.inner, args...; kwargs...)
, you may run into dispatch issues depending on what methods are defined for f
and specifically what was the type signature on args
: say f(::Any, ::Integer, ::Integer)
was defined, you'd need to actually define f(::MyType, ::Integer, ::Integer)
. I believe the list of issues and corner cases may go on for a while, the conclusion being that such a generic forward wrapping is either not well defined or faces significant implementation challenges.
So I think explicitly forwarding methods (and not implicitly as suggested by the title of this thread) makes more sense because then you can actually be clear about the expected behavior (and even define a few @forward
macro variants or options to rewrap the result at the end, or return the original argument, that kind of stuff).
The real fun starts when you realize that re-wrapping the result of an operation may break the semantics/invariants imposed by the wrapper :) Sometimes you really do need to have the same object be mutated
yeah. that's a very good point.
I guess you shouldn't forward as a non-brainer.
Could this kind of wrapping though be used as a non-brainer ?
struct MyWrapperType
wrapped::AnotherType
end
wrapper(mwt::MyWrapperType) = mwt.wrapped
everyfunctionoutthere(mwr::MyWrapperType)
result = everyfunctionoutthere(wrapped(mwr))
return identityorwrapped(result)
identityorwrapped(x) = x
identityorwrapped(x::AnotherType) = MyWrapperType(x)
Probably not always... Verdict: you need a brain to code.
As a trivial counterexample, you could have a type that checks the backtrace in every function implemented for it for whether there is a direct chain of dispatch from the first dispatch down to the current call, using exactly that type. If there are two such blocks in the backtrace, the type knows that it's been wrapped and throws. Et voila, you have a type that is unwrappable!
The only way out of that mess is to have a proper outside-extensible trait system, which we don't have :)
I played a bit with SimpleTraits.jl and WhereTraits.jl, but indeed they both seem to require modification of original code to do what this post desires...and that's a deal breaker.
Last updated: Dec 28 2024 at 04:38 UTC