I have a package where I want to create an extensible group of objects. I'm currently doing this via type parameters + dispatch, i.e. downstream packages defining methods for Thing{:specific_symbol}
. This seems to be working rather well, but from what I can tell is technically type piracy.
I'd be interested to hear thoughts on this.
[ Cross-post from Slack: https://julialang.slack.com/archives/C6A044SQH/p1682264929271579 ]
That looks like type piracy to me. If two users both define Thing{:foo}
they'll collide. Instead if they define their own namespaced value Thing{ModuleA.Foo}
/Thing{ModuleB.Foo}
then they won't.
Dispatch on type parameters is a bit of a grey area, but generally speaking it is considered type piracy
Honestly though without context it's really hard to say if it's a bad thing or not. Sometimes type piracy is a really useful tool
Sometimes it's a horrible idea
What's a scenario where Base.maximum(Vector{MyType}) =
causes a problem?
If someone in Base
wrote code that relied on Base.maximum(::Vector)
behaving in a specific way, and you pirated a new Vector{MyType}
method in there that changed it's behaviour, then that could cause problems
E.g. imagine if you wrote a method Base.maximum(::Vector{MyType})
that calculated the maximum by comparing the absolute values of the MyType
s rather than the regular values, then maximum
wouldn't be the same as doing a reduce(max, ::Vector{MyType})
which might be relied upon somewhere else
You chose an example where one could easily say "well there's a clearly right or wrong way to implement maximum
" but that's more or less besides the point.
Instead of overloading maximum
which operates on containers here, you should be overloading the scalar functions max
, min
, <
, etc.
Thing{:foo}
with a symbol seems a bit like environment variables, where ENV["package_var"] = "true"
is OK so long as everyone sticks to some convention like having their unique package name there.
In my case, I have a TOML file with entries of different types, and each entry specifies "I am an X
". The way I'm handling this is by using that to construct Entry{:X}
structs and then relying on dothing(::Entry{:X})
methods for a few crucial functions, and for more generic dogenericthing(::Entry)
methods for general functionality. This way, to add support for a new entry type Y
, a package just needs to implement a few dothing(::Entry{:Y})
entries, it is possible that one package could override another's methods. Singletons from the packages as parameters would be better in this respect, but I need to construct the Entry{:X}
instances based on the string in the TOML, which seems a fair bit less straightforward in the singleton type parameter approach.
I have a feeling in practice this should be ok, but I appreciate the second opinions.
If this is supposed to scale to many users, I think explicit namespacing should be used. eg using a tuple of (packagename, symbol)
or similar.
The entry types (which in this case are for data storage formats, e.g. csv
, json
, ...) are defined by packages, not users. I suppose the question here is whether it's worth worrying the creation or loading of two packages that say define a :json
type. If one hopes that all potential packages that might build on my project will nicely cooperate and not fight over a certain name, then I suppose we'll be all right. If not, then I'll need to add a way of being able to specify the package and switch to singletons, or similar.
Mason Protter said:
then
maximum
wouldn't be the same as doing areduce(max, ::Vector{MyType})
which might be relied upon somewhere else
This can happen without any kind of near-piracy at all. Define your own AbstractVector
type, define maximum
for it in an arbitrary way - now you get this inconsistency.
The issue here isn't piracy, it's non-formalizeable "different methods of the same function should have the same general behavior".
It's absolutely formalizeable, we just don't have any way to enforce these interfaces/connections at the moment
@Alexander the thing though is that code typically is anle to rely much more heavily on implementation details when they’re operating on concrete types they own. When operating on an AbstractVector
the writer needs to be careful to write highly generic code that hews to accepted norms and documented interfaces. That isnt really the case with bare Vector
Last updated: Dec 28 2024 at 04:38 UTC