[OT] What are D's values?

H. S. Teoh hsteoh at quickfur.ath.cx
Tue Oct 5 01:11:22 UTC 2021


On Mon, Oct 04, 2021 at 11:10:27PM +0000, Tejas via Digitalmars-d wrote:
[...]
> Can you please list any resources on DbI?
> 
> I've been finding it pretty hard to grasp, and I only managed to find
> a few videos by Andrei on the subject.
[...]

The basic idea is very simple (but the results are powerful indeed).

Basically, in traditional code, when a function takes a parameter, say,
it specifies exactly what the type T of the parameter must be (int,
float, some struct, etc.).  In the function body, it makes use of the
implementation details of this type in order to perform operations on
the data.

	auto myFunc(T data) { ... }

Experience shows, however, that often the function doesn't actually
depend on *all* the details of the type T.  Perhaps it uses only a small
subset of its fields/methods.  For example, the function may only need
to use .front and .empty.  So there's no reason for the function to
demand that incoming arguments must be of a fixed type T; T can really
be *any* type that happens to have a .front and .empty with the expected
semantics.  So we move from a normal (non-template) function to a
template function:

	auto myFunc(T)(T data) {
		... // make use of .front, .empty here
	}

So far so good, we all know what template functions are.

But DbI takes this one step further: suppose myFunc doesn't actually
need to use .empty. Maybe there's a different way to do what it wants
without using .empty, but it uses .empty because of efficiency (or
whatever other reason).  Still, since it's not strictly necessary, why
make the function depend on it?  It would be nice to be able to pass
types to myFunc that don't have .empty, so that it can perform its magic
(albeit somewhat less efficiently/whatever).  So what we do instead is
to use `static if` to inspect the incoming type T, and adapt ourselves
accordingly:

	auto myFunc(T)(T data) {
		static if (... /* T has .empty) {
			... // make use of .front, .empty here
		} else {
			... // alternative implementation that doesn't use .empty
		}
	}

Voila! Now myFunc can take both types that have .front and .empty, and
types that only have .front.

(Note also that this doesn't have to be limited to the presence/absence
of a field.  It can be any attribute of T inspectable at compile-time.
You can have static if blocks to switch between implementations based on
the size of a type, for example to make decisions about how to best
allocate storage for a large number of its instances, say. Or, the
favorite among D users, make decisions based on some UDA that you attach
to the type in order to change how the function processes it.)

This may seem trivial, but it has powerful consequences.  By making
myFunc independent of the existence of .empty, we've increased its scope
of applicability to a wider set of types.  Each static if we introduce
more-or-less doubles the set of types for which the function is
applicable.  Instead of writing one function for types that have .front
and .empty and another function for types that have only .front, we can
process both according to the same logic.  Instead of demanding the
caller of the function to supply us with a type fulfilling requirements
A, B, C, we adapt ourselves to whatever type the caller may throw at us,
making use of compile-time introspection to discover what the type can
do and adapting ourselves accordingly.

//

An actual example I wrote myself: a serialization module I wrote for
writing out objects into text form and reading it back later to
reconstruct said objects.  Instead of the traditional method of
inserting a .serialize method into every struct and class (wayyyy too
tedious, repetitious, and highly prone to bugs caused by human error),
my code instead does this:

	void save(S,T)(S storage, T data) {
		static if (is(T == struct)) {
			... // serialize structs here
		} else static if (is(T == class)) {
			... // serialize structs here
		} else if (is(T == U[], U)) {
			... // serialize arrays here
		} else if (is(T : int)) {
			... // serialize int-like types here
		}
		... // etc
		else static assert(0, "Can't serialize type " ~ T.stringof);
	}

So, when I want to serialize a struct S, what do I do?

	S data;
	storage.save(data);

When I want to serialize an int?

	int data;
	storage.save(data);

When I want to serialize an array?

	S[] data;
	storage.save(data);

Super-easy.  Notice that I deliberately named my data `data`: that means
if I ever decide to change the type of my data, I don't even need to
edit the .save line; I just change the type of `data`, and .save Just
Works(tm).  It automatically adapts itself to whatever type is passed to
it.  No need to call a different function, pass a ton of flags, none of
that fuss and muss.  Just hand it a different type, and it Just
Works(tm).

But wait, there's more!  What if I want to treat strings differently
from other arrays?  Easy, just add a static if block to .save that
handles strings before it handles general arrays:

	void save(S,T)(S storage, T data) {
		static if ...
		... else if (is(T == string)) {
			... // special handling for strings here
		} else if (is(T == U[], U)) {
			... // handle general arrays here
		} else ...
	}

But what if I want a different serialization scheme for certain data
types? E.g., if I have a struct MyStruct, and I don't want to serialize
*all* fields, but want to exclude, say, fields that only store cached
data that can be recomputed next time?  Here's where UDAs come into
play: I mark fields I don't want to save with a special UDA:

	struct DontSerialize {}

	struct MyStruct {
		int i;		// save this
		float f;	// save this too
		@DontSerialize string s; // don't save this
	}

	void save(S,T)(S storage, T data) {
		...
		static if (is(T == struct)) {
			foreach (field; allMembers!T) {
				// skip fields tagged with DontSerialize
				static if (!hasUDA!(mixin("data."~field), DontSerialize)) {
					... // serialize here
				}
			}
		}
		...
	}

Notice how clean this is.  In a traditional approach, I'd have to pass
some kind of blacklist to .save, perhaps an AA of stuff I don't want to
save, or some boolean flags, etc..  But with DbI, the API of .save
remains exactly the same as before:

	MyStruct[] data;
	storage.save(data);	// <--- exactly the same as before

All I need to do is to tag the field in the definition of MyStruct -- no
need to change code in all the places that pass MyStruct to .save, no
need to change the API of .save (or, God forbid, add a new overload /
different function just to handle this case), just add a new static if
block to check for the UDA, and it Just Works.

In other words, adding a new static if block to .save "teaches" it how
to adapt to structs that contain fields that shouldn't be saved. Once
thus taught, you can just hand it any type appropriately tagged with the
UDA, and it automatically does the Right Thing(tm).  No fuss, no muss,
no extra parameters, no messy option flags, no massive refactoring of
every callsite of .save, just a couple of lines of code change and we're
done.

There's so much more you could do to it. For example, another UDA for
tagging types that should use a different serialization scheme (e.g.,
highly-compressible data that should be filtered through a compression
algorithm). Another UDA that filters an array to exclude elements that
don't need to be saved.  Etc..  With every addition, .save becomes more
and more powerful, yet its API remains exactly the same as before:

	storage.save(data);	// <--- exactly the same as before

Code that calls .save don't need to be touched just because some data
type now needs different serialization behaviour. No massive
refactorings needed, no proliferation of flags and other extraneous
parameters to .save.  Just tag the type definition, and we're good to
go.

This, my friend, is the power of DbI.  This example, of course, only
scratches the surface of what you can do with DbI, but I'll leave it up
to you to explore the rest. :-D


T

-- 
Nearly all men can stand adversity, but if you want to test a man's character, give him power. -- Abraham Lincoln


More information about the Digitalmars-d mailing list