and I already explained that Union
is a thing.
o11c
That still doesn't explain why duck typing is ever a thing beyond "I'm too lazy to write extends BaseClass
". There's simply no reason to want it.
Write-up is highly Windows-centric (though not irrelevant elsewhere).
One thing that is regretfully ignored in discussions of async, tasks, green threads, etc. is that there is no support/consideration for native (reliable/efficient) thread-local variables. If you're lucky you'll get a warning about "don't use them".
Then - ignoring dunders that have weird rules - what, pray tell, is the point of protocols, other than backward compatibility with historical fragile ducks (at the cost of future backwards compatibility)? Why are people afraid of using real base classes?
The fact that it is possible to subclass a Protocol
is useless since you can't enforce subclassing, which is necessary for maintainable software refactoring, unless it's a purely internal interface (in which case the Union
approach is probably still better).
That PEP link includes broken examples so it's really not worth much as a reference.
(for that matter, the Sequence
interface is also broken in Python, in case you need another historical example of why protocols are a bad idea).
chunks: [AtomicPtr>; 64],
appears before the explanation of why 64 works, and was confusing at first glance since this is completely different than the previous use of 64
, which was arbitrary. I was expecting a variable-size array of fixed-size arrays at first (using something like an rwlock you can copy/grow the internal vector without blocking - if there was a writer, the last reader of the old allocation frees it).
Instead of separate flags, what about a single (fixed-size, if chunks are) atomic bitset? This would increase contention slightly but that only happens briefly during growth, not accesses. Many architectures actually have dedicated atomic bit operations though sadly it's hard to get compilers to generate them.
The obvious API addition is for a single thread to push several elements at once, which can be done more efficiently.
Aside: Note that requests
is sloppy there, it should use either raise ... from e
to make the cause explicit, or from None
to hide it. Default propagation is supposed to imply that the second exception was unexpected.
For an extension like this - unlike most prior extensions - you're best off with essentially an entirely separately compiled copy of the program/library. So IFUNC
is a poor fit, even with peer optimization.
In practice, Protocols are a way to make "superclasses" that you can never add features to (for example, readinto
despite being critical for performance is utterly broken in Python). This should normally be avoided at almost all costs, but for some reason people hate real base classes?
If you really want to do something like the original article, where there's a C-implemented class that you can't change, you're best off using a (named) Union
of two similar types, not a Protocol
.
I suppose they are useful for operator overloading but that's about it. But I'm not sure if type checkers actually implement that properly anyway; overloading is really nasty in a dynamically-typed language.
Let's introduce terms here: primarily, we're plotting "combat power" as a function of "progress level". Both of these are explained below.
I assume we're speaking about a level system that scales indefinitely. If there is a very small level cap it's not important that all this math actually be done in full (though it doesn't); balancing of the constants is more important in that case.
The main choice to be made is whether this function is polynomial or exponential; the second choice is the exponent or base, respectively (in either case, call it N
). Note that subexponential (but superpolynomial) functions exist, but are hard to directly reason about; one purpose of the "progress level" abstraction is to make that easier.
Often there are some irregularities at level 1, but if we ignore those:
- in a polynomial system, if N level-1 characters can fight equally to 1 level-2 character, then N level-10 characters can fight equally to 1 level-20 character.
- in an exponential system, if N level-1 characters can fight equally to 1 level-2 character, then N level-19 characters can fight equally to 1 level-20 character.
The third choice is whether to use absolute scale or relative scale. The former satisfies the human need for "number go up", but if your system is exponential it implies a low level cap and/or a low base, unless you give up all sanity and touch floats (please don't). The latter involves saying things like "attacks on somebody one level higher are only half as effective; on someone one level lower it's twice as effective", just be sure to saturate on overflow (or always work at the larger level's scale, or declare auto-fail or auto-pass with a sufficient level difference, or ...).
Note that relative scale is often similar to how XP works even if everything else uses absolute scale, but mostly I'm not talking about XP here (though it is relevant for "is it expected for people actually to hit the hard level cap, or are you relying on a soft cap?").
Progress level is purely a utility abstraction. If you have exponential tiers (e.g. if you make a big deal about displayed levels 10, 100, 1000 or 4, 16, 64, 256) you might choose to set it to the log of the displayed level (this often matches human intuition, which is bad at math), though not necessarily since it might unnecessarily complicate the math with the cancelling you might do.
If you want xianxia-style "punching up is harder the higher you go" (and you're actually), it might be some superlinear function of character level (quadratic? exponential?).
Otherwise it is just the displayed character level (in some contexts it is useful to consider this as a decimal, including the fraction of XP you've got to the next level. Or maybe not a fraction directly if you want to consider the increasing XP requirements, though for planning math it usually isn't that important).
Combat power is usually a product of up to 6 components, in rough order of importance:
- effective health (including shields and damage resistance, if applicable - the latter is often hyperbolic!)
- attack damage (including math for crits - though if they're a constant max multiple you can ignore them)
- attack chance (as modified by enemy dodge). Often this is bounded (e.g. min 5%, max 95%) so you get weirdos trying to use peasants to kill a
- number of targets that can (practically) be hit with a single attack. Usually this involves compromises so might be ignored.
- distance at which the attack can be performed. Usually this involves compromises so might be ignored.
- how long you can keep fighting effectively without a rest (regeneration is relevant for this, whether innate, from items, or from spells). This is only relevant for certain kinds of games.
So the net result is usually somewhere between a quadratic and a cubic system relative to the scaling of the individual components. If the individual scaling is exponential it's common to just ignore the polynomial here though.
Things like "stats" and "skills" are only relevant insomuch as they apply to the individual components. One other thing to think about is "how effective is a buff applied by a high-level character to a low-level character, and vice versa", which is similar to the motivation of level requirements for gear.
All of these can be done with raw strings just fine.
For the first pathlib
bug case, PATH
-like lookup is common, not just for binaries but also data and conf files. If users explicitly request ./foo
they will be very upset if your program instead looks at /defaultpath/foo
. Also, God forbid you dare pass a Path("./--help")
to some program. If you're using os.path.dirname
this works just fine.
For the second pathlib
bug case, dir/
is often written so that you'll cause explicit errors if there's a file by that name. Also there are programs like rsync
where the trailing slash outright changes the meaning of the command. Again, os.path
APIs give you the correct result.
For the article mistake, backslash is a perfectly legal character in non-Windows filenames and should not be treated as a directory component separator. Thankfully, pathlib
doesn't make this mistake at least. OTOH, /
is reasonable to treat as a directory component separator on Windows (and some native APIs already handle it, though normalization is always a problem).
I also just found that the pathlib.Path
constructor ignores extra kwargs. But Python has never bothered much with safety anyway, and this minor compared to the outright bugs the other issues cause.
One problem is that Rust doesn't support tagged unions. enum
is regrettably solving a different problem, but since it's the only hammer we have, it's abused for this kind of thing. This often leads to having to write match error ... unreachable
.
The problem is that C++ compilers still haven't fixed a trivial several-decades-old limitation: you still have to pass the named arguments in order.
They use the excuse of "what's the evaluation order", but ordinary constructors have the exact same problem and they deal with that fine.