Stream: helpdesk (published)

Topic: forward EVERYTHING except


view this post on Zulip Filippos Christou (Jan 27 2023 at 20:13):

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 ?

view this post on Zulip Rik Huijzer (Jan 27 2023 at 22:21):

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.

view this post on Zulip Brian Chen (Jan 27 2023 at 23:15):

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.

view this post on Zulip Brian Chen (Jan 27 2023 at 23:18):

If something like method_missing could be made to work well in Julia, that would be quite interesting

view this post on Zulip Filippos Christou (Jan 30 2023 at 07:06):

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.

view this post on Zulip Filippos Christou (Jan 30 2023 at 07:08):

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

view this post on Zulip Filippos Christou (Jan 30 2023 at 07:12):

@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 ?

view this post on Zulip Filippos Christou (Jan 30 2023 at 07:17):

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.

view this post on Zulip Filippos Christou (Jan 30 2023 at 07:18):

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.

view this post on Zulip Filippos Christou (Jan 30 2023 at 07:24):

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-catchblock, but it will be slow and bulky.

view this post on Zulip Filippos Christou (Jan 30 2023 at 07:27):

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.

view this post on Zulip Rik Huijzer (Jan 30 2023 at 08:18):

If you want Julia to be OOP, then that’s asking for a painful experience. Julia is not OOP.

view this post on Zulip Filippos Christou (Jan 30 2023 at 08:21):

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.

view this post on Zulip DrChainsaw (Jan 30 2023 at 13:08):

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.

view this post on Zulip Sukera (Jan 30 2023 at 13:09):

if you're accessing object properties directly, you're setting yourself up for a world of pain imo

view this post on Zulip Sukera (Jan 30 2023 at 13:10):

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

view this post on Zulip Sukera (Jan 30 2023 at 13:11):

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

view this post on Zulip Sukera (Jan 30 2023 at 13:12):

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

view this post on Zulip DrChainsaw (Jan 30 2023 at 13:14):

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.

view this post on Zulip Sukera (Jan 30 2023 at 13:14):

yeah, I dislike dot-access with a passion

view this post on Zulip Sukera (Jan 30 2023 at 13:15):

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

view this post on Zulip Sukera (Jan 30 2023 at 13:15):

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

view this post on Zulip Filippos Christou (Jan 30 2023 at 17:16):

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.

view this post on Zulip Brian Chen (Jan 30 2023 at 17:18):

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.

view this post on Zulip Brian Chen (Jan 30 2023 at 17:18):

Maybe an issue on Base would help carry the discussion forward

view this post on Zulip Cédric Belmant (Jan 30 2023 at 17:44):

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.

view this post on Zulip Cédric Belmant (Jan 30 2023 at 17:48):

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

view this post on Zulip Sukera (Jan 31 2023 at 07:23):

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

view this post on Zulip Filippos Christou (Feb 03 2023 at 13:23):

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)

view this post on Zulip Filippos Christou (Feb 03 2023 at 13:23):

Probably not always... Verdict: you need a brain to code.

view this post on Zulip Sukera (Feb 03 2023 at 13:26):

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!

view this post on Zulip Sukera (Feb 03 2023 at 13:27):

The only way out of that mess is to have a proper outside-extensible trait system, which we don't have :)

view this post on Zulip Filippos Christou (Feb 03 2023 at 13:36):

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: Oct 02 2023 at 04:34 UTC