Better support for wrapper types

I’m sure I cannot be the only one who sometimes feels that writing wrappers for types in Julia that I just want to either extend with some custom methods or change the functionality of just a few built-in functions can sometimes get a bit bothersim. With multiple dispatch being such an integral feature of Julia, it feels a bit silly to write functions like Base.getindex(ob::MyType, idx) = getindex(ob.vec, idx) so many times.

Wouldn’t it make sense to have some built in pattern (or macro) that allows users to automatically forward all function that are already defined for some type, to one of its fields?

2 Likes

See this discussion for further discussion and, in particular, its recommendation of ForwardMethods.jl.

While sometimes I think about forwarding literally all methods, I usually end up deciding that’s too broad a brush. It also tends to break down. For example, if you wrap a <:Number, you might want sqrt to unwrap the value, apply sqrt and re-wrap it. But you don’t want to do this for another function like >=(0), since you want that to return a Bool.

While doing this only for all functions explicitly defined for the wrapped type sounds more sensible, recall that many methods are duck-typed, so that list is very incomplete (for example, it would miss any of the many methods that take arbitrary arguments and promote them).

If you aren’t using a package’s macro but instead defining forwards manually for an explicit list, metaprogramming can be useful:

# unary functions
for f in [:abs, :abs2, :sqrt, :exp, :log, :sin, :cos, :tan] # and many more
	@eval Base.$f(x::MyType) = MyType(Base.$f(x.value))
end

# binary functions
for f in [:+, :-, :*, :/, :^] # and many more
	@eval Base.$f(x::MyType, y::Number) = MyType(Base.$f(x.value, y))
	@eval Base.$f(x::Number, y::MyType) = MyType(Base.$f(x, y.value))
	@eval Base.$f(x::MyType, y::Number) = MyType(Base.$f(x.value, y.value))
end
8 Likes

or not, if you have:

struct PositiveFloat
  # assert it's >= 0.0, I'm not sure how to do this in code, or if possible:
  num::Float32
end

then you want it to work on it directly (because of multiple dispatch), and NOT unwrap it first, then you would disable the assert, because it will provide faster code, i.e. should give you access to a single fast assembly instruction without any checks or possible error throwing.

From my experience, if the type is just a wrapper, then it often has different semantics from the data it contains, and forwarding all methods would be wrong. If it is not just a wrapper but also something else, e.g. an AbstractArray, the methods to be defined come from an informal interface spec. But in that case the data layout is an implementation detail and also the method implementations can be hard to automate.

1 Like

I agree, but in many of my own cases I only need to change the behavior of a set of functions, like push! for example. In my opinion it would be great to automatically forward the functions at a language level when a different definition is not given already. E.g. in many cases I just forward size, length, isempty, sizehint!, or even iterate, sort!, getindex/setindex! and others. Having the forwarding be opt out in the sense that any function is forwarded unless otherwise defined would reduce a lot of the boilerplate I write for wrapping types often.

Of course, a struct would need to be explicitly marked as a wrapper in this case. If the details of the wrapper don’t support this pattern it wouldn’t have to be used.

Would @forward from ReusePatterns.jl work for you? GitHub - gcalderone/ReusePatterns.jl: Implement composition and concrete subtyping in Julia.

It works, but in my case doesn’t actually reduce the amount of lines I have to write that much, and still requires me to explicitly think about every function I want to forward. Often I find out along the way I forgot to forward a function like iterate, let’s say.

Also just semantically I find it easier to think about some wrapper type as “basically the type it wraps except for a handful of functions that have been explicitly defined” instead of having to read every function that it implements out of all the functions the “informal interface spec”, but that may just be me.

I thought the point is that @forward from ReusePatterns.jl specifically does not require you to specify every function? Unlike the similar macros from ForwardMethods.jl. e.g. in the example in the documentation we have Edition wrapping PaperBook wrapping Book, and forwards like @forward((PaperBook, :b), Book), which automatically forwards all the methods on Book.

This sounds a lot like inheritance.

Oh wow, I will check that later. If it works like that I would be quite happy, but will this allow overwriting though? I was aware of how ForwardMethods does it I guess and maybe never really considered ReusePatterns (I had seen the package before).

I mean there’s a reason inheritance was invented. I agree it’s similar, and with Julia’s type system something like this would be quite nice for my productivity.

I haven’t tried it myself, but as far as I can tell it just uses methodswith to find all existing methods on the base type, and then defines a corresponding method on the new type. So overwriting should work without any issues.

Wouldn’t redefining functions at module scope cause a warning then? (I don’t know actually what the warning is warning about, because everything always seems to work fine in that case, but still I guess it’s better not to do it in that way then I assume)