October 05, 2021

On Monday, 4 October 2021 at 23:10:27 UTC, Tejas 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.

Even the d-idioms website doesn't have much.

Any text sources would be really appreciated.

Thank you for reading!

Design by introspection is when you have templated code "customize" itself based on the specific type(s) it's instantiated with.

One place DbI is used a lot is in std.range. For example, here's how std.range's documentation describes the return value of retro:

>

Type: auto

A bidirectional range with length if r also provides a length. Or, if r is a random access range, then the return value will be random access as well.

Source: https://phobos.dpldocs.info/std.range.retro.html#returns

Notice those "if" clauses. The return value has a .length property if the original range, r, has a .length property. That's DbI.

How is it done? Internally, retro uses introspection features like __traits and is() expressions to check what properties r has, and uses static if to customize its own implementation based on the result of those checks.

October 04, 2021
On 10/4/21 3:15 PM, Walter Bright wrote:

> This is plasticity, the opposite of brittleness.
>
> What are your experiences with this?

That is exactly the case! (I wrote my response to H. S. Teoh before reading your post.)

"Plasticity" seems to be a common theme about D.

Ali

October 04, 2021
On Mon, Oct 04, 2021 at 11:39:05PM +0000, Paul Backus via Digitalmars-d wrote:
> On Monday, 4 October 2021 at 22:15:33 UTC, Walter Bright wrote:
> > Interestingly, left off is one thing that D does very well at: plasticity. What do I mean by that?
[...]
> > I discovered a key component of this is D's use of . instead of ->. One can easily test drive with classes, structs, pointers, and values, interchanging them as one would try on a shirt. It's such a frackin' nuisance to do that in C and C++, one just doesn't bother.
> > 
> > This is plasticity, the opposite of brittleness.
> > 
> > What are your experiences with this?
> 
> I've experienced this too.
> 
> In D, with a little ingenuity, it is possible to make almost any surface-level syntax "desugar" to almost anything you want. Which means that when you want to change how some part of your program is implemented, you can almost always keep the original syntactic "interface" stable while doing so.

I've totally experienced this.  Some of my personal projects started out as one-off, throwaway scripts, ugly, full of hacks, crazy one-liners, etc..  But as time went on, it got refactored, cleaned up, sprouted modules, packages, and gradually transformed itself into a set of well-encapsulated components interacting with each other through nice APIs, as though it were something designed that way from the beginning.

The use of '.' for what C/C++ use '.' and '->' for is one of the factors in making this possible.  I'd even go further and say that the coincidence of '.' (as in member access) with '.' (as in FQN separator) allows certain refactorings that involve substituting a package/module with a nested variable. (Actual example: to make I/O code more unittestable, sometimes I insert an `alias stdio = std.stdio` template parameter into my function; that way, the unittest can substitute std.stdio with a mock-up struct that emulates std.stdio without performing actual I/O. Had the FQN separator been different from the member access operator, this would've been impossible.)

Optional parentheses is another - it's easy to turn a public mutable field into a properly-encapsulated access method without needing to rewrite a whole bunch of code.  Conversely, a method used like a field can be substituted with an actual field, should that become necessary for whatever reason.

Type inference and Voldemort types (much as people criticize it) makes it possible to change the return value of some function without suddenly making a whole ton of code uncompilable. Indeed, I've come to adopt the practice of using `auto` whenever the code I'm writing technically doesn't need to care what the concrete type is (which is quite a lot of the time -- it applies even if you access some field in the type, because all that requires is that it be any type for which .xyz returns a value; it can be a public field, an access method, a UFCS function on an int, whatever).

UFCS + IFTI also lets you abstract away the implementation details of entire chains of processing, so you can change any number of intermediate types in between without needing to touch the chain itself at all.


[...]
> I think this "plasticity" is probably closely related to "expressiveness", because the language features that contribute to one tend to also contribute to the other.
[...]

IMO, the plasticity comes from what I call "symmetry", by which I mean the mathematical (not aesthetic) sense of symmetry: some object X remains unchanged under some set of operations Y.  More specifically, in D there's a lot of "syntactic symmetry": some given piece of syntax such as a line of code (the "object") remains unchanged under many forms of refactorings (the "operations").  Such swapping out the types involved in that line of code, changing something from class to struct, etc..

The greater the syntactic symmetry of a piece of code under the set of refactorings, the easier it is to refactor the program while making only minimal changes to the surface level code.

For example, UFCS chains.  By constraining the API of each component of the chain to the range API, the chain becomes symmetric under many refactoring operations like inserting/removing a component, substituting one component for another, changing the concrete element types of a component, reordering components (to some extent -- if the relevant types are compatible), etc..  This symmetry makes it easy to change the code: the syntax of the chain remains unchanged under the operation of changing a return type, for example, so you only need to change the return type, you don't have to change the syntax of the chain.  The range API serves as the invariant amid these refactoring operations, and this gives range-based code a high degree of symmetry under said refactorings.

DbI is a another powerful example: by using static if to discover the properties of a template argument instead of making assumptions about the existence of, e.g., some property .xyz, you make that template symmetric under the operation of swapping that argument for another type that may not have a .xyz property. The template adapts itself to the new argument, instead of no longer compiling because the .xyz that was present in the old type doesn't exist in the new.  The body of the template remains unchanged in the face of the change in behaviour of the incoming type.  Symmetry.


On the converse side, the lack of symmetry in certain aspects of the language makes certain refactorings more difficult.

One example is the struct/class by-value/by-ref dichotomy.  While certain refactorings let you get away with simply writing `struct` in the place of `class`, if there's code that depends on the by-ref nature of the class, then more effort will be required to maintain the correctness of the code, e.g., inserting `ref` into function parameters in order to emulate the by-ref behaviour of the class.

Another example is D's integer promotion rules. If for whatever reason you wish to change `int` to `short`, for example, you'll quickly run into trouble with arithmetic expressions that now require a cast in order to be assignable back to a short.

I'm sure you can find many other examples of asymmetry in D, but IMO D does have a high degree of symmetry in many of the right places, and that makes D code highly refactorable and plastic.  In other languages, there tends to be a lot of asymmetry under many refactoring operations, which makes it difficult to perform certain kinds of refactorings. This effectively de-incentivises said refactorings, making the language less plastic and programs less refactorable.


T

-- 
Spaghetti code may be tangly, but lasagna code is just cheesy.
October 05, 2021

On Tuesday, 5 October 2021 at 00:11:29 UTC, Paul Backus wrote:

>

On Monday, 4 October 2021 at 23:10:27 UTC, Tejas 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.

Even the d-idioms website doesn't have much.

Any text sources would be really appreciated.

Thank you for reading!

Design by introspection is when you have templated code "customize" itself based on the specific type(s) it's instantiated with.

One place DbI is used a lot is in std.range. For example, here's how std.range's documentation describes the return value of retro:

>

Type: auto

A bidirectional range with length if r also provides a length. Or, if r is a random access range, then the return value will be random access as well.

Source: https://phobos.dpldocs.info/std.range.retro.html#returns

Notice those "if" clauses. The return value has a .length property if the original range, r, has a .length property. That's DbI.

How is it done? Internally, retro uses introspection features like __traits and is() expressions to check what properties r has, and uses static if to customize its own implementation based on the result of those checks.

Is that seriously all there is to it? I see Andrei saying in his talks that static if doubles the design space covered, that DbI can tackle combinatorial explosion.

Can DbI really help with software that gets complex exponentially?

I guess a couple case studies will be nice.

I've been putting it off, but maybe it's time I ignore the "experimental" smell of checkedint and allocator and actually study the underlying code.

I'm genuinely having trouble comprehending how a bunch of compile time queries help reduce the complexity of software constructs exponentially... (maybe because I'm conflating it with lines of code metric, thinking that DbI will somehow make a 100_000 line program writable in a couple thousand lines)

October 05, 2021

On Monday, 4 October 2021 at 23:56:41 UTC, SealabJaster wrote:

>

On Monday, 4 October 2021 at 23:10:27 UTC, Tejas wrote:

>

...

I'm not actually too sure about anything specifically on DbI.

Anything to do with using UDAs would be pretty close:

  1. http://ddili.org/ders/d.en/uda.html
  2. https://bradley.chatha.dev/blog/dlang-json-serialiser-5 (is part of a multi-part blog series, so might be hard to follow by itself)

You've motivated me to add a bit about DbI onto my upcoming DConf talk (assuming it's not too late for me to make changes)! So hopefully that'll be a bit of help as well.

Generally a pairing of static if, static foreach, and anything to do with __traits is what I'd define as DbI.

That's okay, parts 1-4 a literally a click away :D

Thank you very much for sharing a real code-base that is also explained in a somewhat tutorial manner!

October 04, 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
October 05, 2021

On Tuesday, 5 October 2021 at 00:51:26 UTC, Tejas wrote:

>

On Tuesday, 5 October 2021 at 00:11:29 UTC, Paul Backus wrote:

>

Notice those "if" clauses. The return value has a .length property if the original range, r, has a .length property. That's DbI.

How is it done? Internally, retro uses introspection features like __traits and is() expressions to check what properties r has, and uses static if to customize its own implementation based on the result of those checks.

Is that seriously all there is to it? I see Andrei saying in his talks that static if doubles the design space covered, that DbI can tackle combinatorial explosion.

Can DbI really help with software that gets complex exponentially?

Whenever you write a static if statement, you end up with two possible versions of the code: one where the condition is true, and one where it's false. It follows that if you have N static if statements in your program, then you have 2^N possible versions of the code--so the number of versions increases exponentially with respect to the number of static if statements.

If you assume that "number of static if statements" is proportional to "number of lines of code", then you can make the claim that DbI allows you to write programs in O(N) lines that would otherwise take O(2^N) lines.

Of course, this is not really true in practice. In languages that don't support DbI, what actually happens is that you do not write all 2^N customized versions of the code. Instead, you give up on having individual customized versions for each use-case and write a single version based on some lowest-common-denominator abstraction. What you really lose here are the benefits to performance and expressiveness that come from having individually-customized versions of your code.

October 05, 2021

On Tuesday, 5 October 2021 at 01:11:22 UTC, H. S. Teoh wrote:

>

On Mon, Oct 04, 2021 at 11:10:27PM +0000, Tejas via Digitalmars-d wrote: [...]

>

[...]
[...]

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

[...]

...
...
...

You're the absolute best, you know that?

Thank you so much for taking the time and effort to write this, I'll be bookmarking this and reading it every single time I doubt my understanding of DbI.

In fact, I think this even deserves a place in in p0nce's d-idioms page

October 05, 2021
On Monday, 4 October 2021 at 23:16:00 UTC, H. S. Teoh wrote:
> +1, D is so versatile that it easily lends itself to all these diverse use cases

And it can meet you where you are

i wrote about this a while ago:
http://dpldocs.info/this-week-in-d/Blog.Posted_2021_02_08.html#adam's-thoughts-on-mental-friction


(fun fact: that topic was actually what I was originally thinking about doing as the DConf 2014 talk.)
October 05, 2021

On Tuesday, 5 October 2021 at 01:33:45 UTC, Paul Backus wrote:

>

On Tuesday, 5 October 2021 at 00:51:26 UTC, Tejas wrote:

>

[...]

Whenever you write a static if statement, you end up with two possible versions of the code: one where the condition is true, and one where it's false. It follows that if you have N static if statements in your program, then you have 2^N possible versions of the code--so the number of versions increases exponentially with respect to the number of static if statements.

[...]

So the key is getting expressiveness and performance while maintaining a degree of conciseness.

This thread has been very useful for me, thank you, and everyone who has answered me :D