Go to file
Chris Morgan 27eca55182 Replace the raw module with just hash_map
When I implemented Extend<Box<A>> for Map<A>, I thought:
can I implement Extend<(TypeId, Box<A>)> for RawMap<A>,
as HashMap<K, V> implements Extend<(K, V)>?
No, I responded, for insert is unsafe,
and a trait implementation cannot be marked unsafe.
Then said I, hang on, why is insert unsafe?
For by analogy with pointers, creating a dangling pointer is safe,
it’s only dereferencing it that’s unsafe;
so too here could insertion be safe and retrieval unsafe.
Then I realised: RawMap is actually completely safe, of itself;
the reason the unsafety is needed is AsMut<RawMap<A>> for Map<A>:
that retrieval is defined as safe, so insertion need be done delicately.

And so I consulted with myself and wondered:
Would it not be better to drop AsMut,
exposing rather an `unsafe fn as_raw_mut`?
For `AsRef<RawMap<A>>` and `Into<RawMap<A>>` may yet be safe,
yet this would take RawMap towards parity with HashMap.

And yet further went I,
descending into depths unplumbed these five years,
saying unto myself:
Wherefore RawMap<A> at all?
Why not rather HashMap<TypeId, Box<A>, BuildHasherDefault<TypeIdHasher>>,
accessed freely and safely by reference or Into,
and unsafely as discussed in the previous stanza
(if I may call it such)?
Striving to understand the matter,
it was a wearisome effort,
and I could not.
I consulted with 143ee06268,
yet with the passage of nigh seven years it was not able to tell me
just why I had thought this might be a good idea.
For lo, those were the benighted ages before Rust 1.0.0,
though the glimmer of that bright dawn danced on the horizon
like the latter part of an arctic winter.

And so, casting away the trammels of history
I forged a new path.
For lo! had I not even then declared it
“not necessarily the final form”?
Casting RawMap away from me and clasping HashMap to my bosom,
I found the required diff in lib.rs
such a delicate thing,
so slight.
It was pleasing in my eyes, and so forthwith I decided:
hew down the unwanted abstraction,
and bind it with a band of iron and bronze,
that it may grow no more.
So need I not add more features to it,
mere shadows of the true HashMap underneath.

Oh fortunate day!
Three-hundred-odd lines removed,
(though more detailed comments offset this,
so that the end result is more like 223,)
and simplicity restored.
Well, except for this fly in the ointment:
std versus hashbrown.
Woe unto the person who calls a raw map std::collections::HashMap,
for when another comes and enables hashbrown,
the first shall crumble into nothingness and errors most distressing.
The mitigation of this is `pub type RawMap<A>`,
augmented by the very truth that,
few using this feature,
few may stumble!
Yet there are difference betwixt the twain,
seen in my cfg branching on VacantEntry and OccupiedEntry,
and this *is* a very mild violation of the principle of strictly additive features.

There ’tis: the tale of an abstraction unravelled.
Unravelled? Feels more like “not ravelled” rather than undoing ravelling.
Deravelled? Disravelled?
I shall but brand the abstraction a dead princess, as it were,
and this my pavane pour un infante défunte.

And if you, dear reader—
if reader there be of this screed that has grown rather longer than originally anticipated but also probably more entertaining if you don’t mind this sort of thing or share a similar sense of humour to me—
have aught to opine on the matter,
You know my email address.
2022-01-26 00:16:15 +11:00
benches Remove the bench Cargo feature as superfluous 2017-07-07 10:55:35 +10:00
src Replace the raw module with just hash_map 2022-01-26 00:16:15 +11:00
.gitignore Remove obsolete items from .gitignore 2022-01-26 00:16:15 +11:00
.travis.yml no_std support 2022-01-26 00:16:15 +11:00
Cargo.toml no_std support 2022-01-26 00:16:15 +11:00
CHANGELOG.md Replace the raw module with just hash_map 2022-01-26 00:16:15 +11:00
COPYING Add the BlueOak-1.0.0 license 2022-01-26 00:16:15 +11:00
README.md no_std support 2022-01-26 00:16:15 +11:00
test no_std support 2022-01-26 00:16:15 +11:00
test-oldest-Cargo.lock no_std support 2022-01-26 00:16:15 +11:00

AnyMap, a safe and convenient store for one value of each type

If youre familiar with Go and Go web frameworks, you may have come across the common “environment” pattern for storing data related to the request. Its typically something like map[string]interface{} and is accessed with arbitrary strings which may clash and type assertions which are a little unwieldy and must be used very carefully. (Personally I would consider that it is just asking for things to blow up in your face.) In a language like Go, lacking in generics, this is the best that can be done; such a thing cannot possibly be made safe without generics.

As another example of such an interface, JavaScript objects are exactly the same—a mapping of string keys to arbitrary values. (There it is actually more dangerous, because methods and fields/attributes/properties are on the same plane.)

Fortunately, we can do better than these things in Rust. Our type system is quite equal to easy, robust expression of such problems.

The AnyMap type is a friendly wrapper around a HashMap<TypeId, Box<dyn Any>>, exposing a nice, easy typed interface, perfectly safe and absolutely robust.

What this means is that in an AnyMap you may store zero or one values for every type.

Cargo features/dependencies/usage

Typical Cargo.toml usage:

[dependencies]
anymap = "1.0.0-beta.1"

No-std usage, using alloc and the hashbrown crate instead of std::collections::HashMap:

[dependencies]
anymap = { version = "1.0.0-beta.1", default-features = false, features = ["hashbrown"] }

The std feature is enabled by default. The hashbrown feature overrides it. At least one of the two must be enabled.

On stability: hashbrown is still pre-1.0.0 and experiencing breaking changes. Because its useful for a small fraction of users, I am retaining it, but with different compatibility guarantees to the typical SemVer ones. Where possible, I will just widen the range for new releases of hashbrown (e.g. change 0.12 to >=0.12, <0.14 when they release 0.13.0), but if an incompatible change occurs, I may drop support for older versions of hashbrown with a bump to the minor part of the anymap version number (e.g. 1.1.0, 1.2.0). Iff youre using this feature, this is cause to consider using a tilde requirement like "~1.0" (or spell it out as >=1, <1.1).

Unsafe code in this library

This library uses a fair bit of unsafe code for several reasons:

  • To support CloneAny, unsafe code is required (because the downcast methods are defined on dyn Any rather than being trait methods); if you wanted to ditch Clone support this unsafety could be removed.

  • In the interests of performance, skipping various checks that are unnecessary because of the invariants of the data structure (no need to check the type ID when its been statically ensured by being used as the hash map key).

  • In the Clone implementation of dyn CloneAny with Send and/or Sync auto traits added, an unsafe block is used where safe code used to be used in order to avoid a spurious future-compatibility lint.

Its not possible to remove all unsafety from this library without also removing some of the functionality. Still, at the cost of the CloneAny functionality and the raw interface, you can definitely remove all unsafe code. Heres how you could do it:

  • Remove the genericness of it all (choose dyn Any, dyn Any + Send or dyn Any + Send + Sync and stick with it);
  • Merge anymap::raw into the normal interface, flattening it;
  • Change things like .map(|any| unsafe { any.downcast_unchecked() }) to .and_then(|any| any.downcast()) (performance cost: one extra superfluous type ID comparison, indirect).

Yeah, the performance costs of going safe are quite small. The more serious matter is the loss of Clone.

But frankly, if you wanted to do all this itd be easier and faster to write it from scratch. The core of the library is actually really simple and perfectly safe, as can be seen in src/lib.rs in the first commit (note that that code wont run without a few syntactic alterations; it was from well before Rust 1.0 and has things like Any:'static where now we have Any + 'static).

Colophon

Authorship: Chris Morgan is the author and maintainer of this library.

Licensing: this library is distributed under the terms of the Blue Oak Model License 1.0.0, the MIT License and the Apache License, Version 2.0, at your choice. See COPYING for details.