www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - D Programming: How to Define and Use Custom Attributes

reply Soham Mukherjee <sohammm1087 gmail.com> writes:
I'm exploring the D programming language and I'm curious about 
custom attributes. How can I define and use custom attributes in 
D to add metadata or behavior to my code?

For example, let's say I want to create a custom attribute 
 MyCustomAttribute that I can apply to functions or types. How do 
I define this attribute, and how can I use it to influence the 
behavior of my D code?

It would be helpful if you could provide code examples and 
explain how custom attributes can be leveraged in practical 
scenarios within D programming. Thank you for your insights!
Sep 06 2023
next sibling parent bachmeier <no spam.net> writes:
On Wednesday, 6 September 2023 at 18:54:26 UTC, Soham Mukherjee 
wrote:
 I'm exploring the D programming language and I'm curious about 
 custom attributes. How can I define and use custom attributes 
 in D to add metadata or behavior to my code?

 For example, let's say I want to create a custom attribute 
  MyCustomAttribute that I can apply to functions or types. How 
 do I define this attribute, and how can I use it to influence 
 the behavior of my D code?

 It would be helpful if you could provide code examples and 
 explain how custom attributes can be leveraged in practical 
 scenarios within D programming. Thank you for your insights!
Ali's book has a chapter on UDAs: http://ddili.org/ders/d.en/uda.html
Sep 06 2023
prev sibling next sibling parent "H. S. Teoh" <hsteoh qfbox.info> writes:
On Wed, Sep 06, 2023 at 06:54:26PM +0000, Soham Mukherjee via Digitalmars-d
wrote:
 I'm exploring the D programming language and I'm curious about custom
 attributes. How can I define and use custom attributes in D to add
 metadata or behavior to my code?
 
 For example, let's say I want to create a custom attribute
  MyCustomAttribute that I can apply to functions or types. How do I
 define this attribute, and how can I use it to influence the behavior
 of my D code?
 
 It would be helpful if you could provide code examples and explain how
 custom attributes can be leveraged in practical scenarios within D
 programming. Thank you for your insights!
User-defined attributes (UDAs) in D are primarily used for compile-time introspection. It lets you tag declarations with compile-time metadata that you can then inspect from other code at compile-time. One example is command-line option parsing code that prints out automatically-generated help text when the program is passed a `--help` option. The idea is to encapsulate your command-line options in a struct, for example: struct Options { double xmin; double xmax; double ymin; double ymax; int xres; int yres; } You could use compile-time introspection to iterate over struct members and use field names as option names (e.g., so you could run the program with `--xmin=1.0 --ymax=10.0`, etc.). You can do this without any UDAs, but suppose you want to go one step further, and have `--help` automatically print out all available options. You could manually write a showHelp() function that prints this information as a big string, of course, but that has the disadvantage that the help text is detached from the definition of the Options struct. If you subsequently modify the Options struct, your help text would become out-of-date and contain possiubly misleading or wrong information. It would be better if the help text could be embedded directly in the definition of Options, so that `--help` will *always* be up-to-date. The way you do this is to add UDAs to it: struct Desc { string text; } struct Options { Desc("Minimum X-coordinate") double xmin; Desc("Maximum X-coordinate") double xmax; Desc("Minimum Y-coordinate") double ymin; Desc("Maximum Y-coordinate") double ymax; Desc("Output horizontal resolution") int xres; Desc("Output vertical resolution") int yres; } By themselves, these UDAs don't do anything. But now you can inspect them in your showHelp() function to print out the UDA strings as part of its output: void showHelp() { Options opts; writefln("Usage: myprogram [options]"); writeln("Options:"); foreach (field; __traits(allMembers(Options))) { alias desc = getUDAs!(mixin("opts."~field), Desc); writefln("--%s", field); if (desc.length > 0) writefln("\t%s", desc); } } Now showHelp() will print out the description for each field in Options, and it will always display the correct information, as long as you update the UDA in Options whenever you change the definition of a field. You don't have to stop here. You can go even further, and have automatic option range enforcement. For example, if Options.xres must be between 10 and 5000, you could write code explicitly to check that, but again, the check will be detached from the definition of Options so it's liable to go out-of-sync. But with UDAs, we can add this information to the definition of Options, and have the option-parsing code automatically enforce this: struct Desc { string text; } struct Range { int minVal, maxVal; } struct Options { Desc("Minimum X-coordinate") double xmin; Desc("Maximum X-coordinate") double xmax; Desc("Minimum Y-coordinate") double ymin; Desc("Maximum Y-coordinate") double ymax; Range(10, 5000) Desc("Output horizontal resolution") int xres; Range(8, 4000) Desc("Output vertical resolution") int yres; } Then in your option-parsing code: void parseOption(ref Options opts, string name, string value) { foreach (field; __traits(allMembers(Options))) { if (name == field) { import std.conv : to; alias T = typeof(mixin("opts."~field)); T val = value.to!T; // parse option value // Automatic enforcement of option value // range alias range = getUDAs!(mixin("opts."~field), Range); if (range.length > 0) { enforce(val >= range.minVal, "value of "~field~" is below minimum"); enforce(val <= range.maxVal, "value of "~field~" is above maximum"); } // Range check passed (or no Range UDA // was found), assign parsed value to // options struct. mixin("opts."~field) = val; } } } Now all you have to do is to update the Range UDA in Options, and parseOption will automatically enforce the new range. A code change in one place, and all other code that inspects the UDA will update themselves automatically. You can also use UDAs to mark options that require special processing. For example, say int fields in Options are normally just converted from base 10, but one particular field, say `octalvalue`, expects user input in octal instead. Instead of writing if-then-else spaghetti in parseOption for each such exception, create a new UDA, say Octal, that you attach to the definition of `octalvalue`. Then in parseOption(), check the field for this UDA, and if it's present, parse the value using base 8 instead of base 10. This may seem like a lot of work for something you could do with just a single if-statement, but what if in the future you need another field that's in octal? With the UDA, you can just tack it on in the definition of Options, and everything will Just Work(tm). Glancing at the definition of Options will immediately tell you that this field is parsed in Octal; no need to dig through the code of parseOption() to find out about this. The UDA serves as self-documentation, which is a good thing. There are many other examples of UDA usage that I'm sure others will be able to provide. T -- The irony is that Bill Gates claims to be making a stable operating system and Linus Torvalds claims to be trying to take over the world. -- Anonymous
Sep 06 2023
prev sibling next sibling parent Nicholas Wilson <iamthewilsonator hotmail.com> writes:
On Wednesday, 6 September 2023 at 18:54:26 UTC, Soham Mukherjee 
wrote:
 I'm exploring the D programming language and I'm curious about 
 custom attributes. How can I define and use custom attributes 
 in D to add metadata or behavior to my code?

 For example, let's say I want to create a custom attribute 
  MyCustomAttribute that I can apply to functions or types. How 
 do I define this attribute, and how can I use it to influence 
 the behavior of my D code?

 It would be helpful if you could provide code examples and 
 explain how custom attributes can be leveraged in practical 
 scenarios within D programming. Thank you for your insights!
for an example of what the can be used for, I use them for attaching metadata to structs describing the values used to query them from an API and the use that in templates to generate code that to return those values https://github.com/libmir/dcompute/blob/master/source/dcompute/driver/ocl/context.d#L20 https://github.com/libmir/dcompute/blob/master/source/dcompute/driver/ocl/util.d#L117
Sep 08 2023
prev sibling next sibling parent cc <cc nevernet.com> writes:
On Wednesday, 6 September 2023 at 18:54:26 UTC, Soham Mukherjee 
wrote:
 I'm exploring the D programming language and I'm curious about 
 custom attributes. How can I define and use custom attributes 
 in D to add metadata or behavior to my code?
UDAs can be extremely useful for iterating through select members of a type when reading data from an input source, such as XML or JSON data. A very basic setup might look something like this (very stripped example written from memory, lot of blanks to be filled in): ```d struct XMLField {} // We'll use an empty struct for our UDA rather than enum since it's // guaranteed to be a globally unique symbol class Person { XMLField { // Everything in this block gets the XMLField UDA string name; int age; } } auto data = ` <People> <Person> <name>Bob</name> <age>34</age> </Person> <Person> <name>Joe</name> <age>20</age> </Person> </People> `; auto xml = MyXMLParser(data); // supply your own xml parser foreach (node; xml.find("Person")) { auto p = new Person; readFromXML(p, node); } void readFromXML(T)(T t, XMLNode node) { foreach (idx, field; T.tupleof) { // iterate through all member variables on class static if (hasUDA!(T.tupleof[idx], XMLField)) { // If it has the XMLField UDA... enum NAME = field.stringof; // the name of the member (name, age, etc) alias TYPE = typeof(field); if (auto text = node.find(NAME).text) { // find child XML node that matches member try { t.tupleof[idx] = text.to!TYPE; // std.conv.to to convert text to the appropriate type } catch (ConvException e) { ... } } } } } ``` An ultimate goal is often to use introspection to minimize the number of places, in code, that need to be "touched" every time a data structure is modified or new fields added. In an older, more straightforward program, if I added a new field to `Person`, I would also need to remember to change the files responsible for reading that field from the input data, validating it, etc. Here, I only need to add the new field with the UDA to the class, and the rest is taken care of (the UDA, itself, is not strictly necessary here, but it helps to let the system know which fields to process and which to ignore!). Of course, in a real-world program, significantly more care would need to be taken to validate the data, handle errors, conversions, missing entries, duplicate entries, and so on. But once your system is solid, it minimizes the pain of upgrading the important stuff. Some other richer ideas include: * Attributing classes to specify how to dynamically bind to LUA * Configuration settings that restrict allowable values to be stored from a text command console * Marking functions to be available via an RPC library and how to mangle them
Sep 10 2023
prev sibling next sibling parent cc <cc nevernet.com> writes:
On Wednesday, 6 September 2023 at 18:54:26 UTC, Soham Mukherjee 
wrote:
 It would be helpful if you could provide code examples and 
 explain how custom attributes can be leveraged in practical 
 scenarios within D programming. Thank you for your insights!
If you'd like a more powerful example... here is a proof of concept for an RPC module that uses UDAs to allow easy definition of success or failure callbacks the called function can seamlessly reply with. ```d proxy.requestName().onSuccess((string str) { writefln("We got the name reply back from the server: %s", str); }).onFailure((string errMsg) { writefln("Something went wrong! Error: %s", errMsg); }); ``` This example omits the actual networking portion of the system, and instead simply calls functions on the target object directly, and replies immediately. In a real-world example, the function request and its parameters would be serialized, along with a unique identifier specifying which object to call, passed across a network or across threads, decoded and run on the destination process, and then likewise the reply serialized and returned. One could adapt the system to be either synchronous, where the caller waits until the reply or failure has been received, or asynchronous where reply callbacks are processed at regular intervals. For example, imagine inserting a simple `.async` into the call chain, specifying timeouts, or setting other options. But otherwise, this program should fully compile and give an idea of how such a system might work. ```d import std.stdio; import std.format; import std.traits; import std.exception; import util.extratraits; class Person { mixin ProxyReceiver; string name; int age; this(string name, int age) { this.name = name; this.age = age; } Proxy Reply!(string) void requestName() { reply(name); } Proxy Reply!(bool) void setName(string s) { this.name = s; reply(true); } Proxy Reply!(int) Failure!(string) void doubleEvenNumber(int x) { if (!(x % 2)) reply(x * 2, x); else failure("Supplied number is not even."); } } struct Reply(RA...) {} struct Failure(RA...) {} mixin template ProxyReceiver() { Proxy!(typeof(this)) _proxy; void reply(string funcstr = __FUNCTION__, RA...)(RA rargs) { _proxy.reply!(funcstr)(rargs); } void failure(string funcstr = __FUNCTION__, RA...)(RA rargs) { _proxy.failure!(funcstr)(rargs); } } class Proxy(Destination) { Destination dest; private static abstract class Callback { MsgNum msgNum; double startTime, timeout; } private static final class CallbackT(alias FUNC) : Callback { static if (hasUDA!(FUNC, Reply)) { alias SUCCESS = void delegate(TemplateArgsOf!(getUDAs!(FUNC, Reply)[0])); SUCCESS successDG; } static if (hasUDA!(FUNC, Failure)) { alias FAILURE = void delegate(TemplateArgsOf!(getUDAs!(FUNC, Failure)[0])); FAILURE failureDG; } } alias MsgNum = uint; MsgNum nextMsgNum = 1; Callback[MsgNum] callbacks; MsgNum lastMessageNum; alias FIRE = void delegate(); FIRE[MsgNum] pendingCalls; // use delegate here as simulation for networking delay to allow time to add callbacks this(Destination dest) { this.dest = dest; dest._proxy = this; } void reply(string funcstr = __FUNCTION__, RA...)(RA rargs) { mixin(`alias FUNC = `~funcstr~`;`); alias FQN = fullyQualifiedName!FUNC; static assert(hasUDA!(FUNC, Reply), "No reply allowed for func: "~FQN); alias UDA = getUDAs!(FUNC, Reply)[0]; alias RFUNC = void delegate(TemplateArgsOf!UDA); static assert(canCallFuncWithParameters!(RFUNC, RA), format("Invalid parameters for reply: %s (expected %s)", RA.stringof, Parameters!RFUNC.stringof)); auto msgNum = lastMessageNum; auto p = msgNum in callbacks; auto cbase = *p; callbacks.remove(msgNum); auto cb = cast(CallbackT!FUNC) cbase; enforce(cb, "Callback mismatch: could not cast to %s", CallbackT!FUNC.stringof); //writefln("Ready to call cb(%s) delegate with args: %s", cb, rargs); if (cb.successDG !is null) { cb.successDG(rargs); cb.successDG = null; } } void failure(string funcstr = __FUNCTION__, RA...)(RA rargs) { mixin(`alias FUNC = `~funcstr~`;`); alias FQN = fullyQualifiedName!FUNC; static assert(hasUDA!(FUNC, Failure), "No failure allowed for func: "~FQN); alias UDA = getUDAs!(FUNC, Failure)[0]; alias RFUNC = void delegate(TemplateArgsOf!UDA); static assert(canCallFuncWithParameters!(RFUNC, RA), format("Invalid parameters for failure: %s (expected %s)", RA.stringof, Parameters!RFUNC.stringof)); auto msgNum = lastMessageNum; auto p = msgNum in callbacks; auto cbase = *p; callbacks.remove(msgNum); auto cb = cast(CallbackT!FUNC) cbase; enforce(cb, "Callback mismatch: could not cast to %s", CallbackT!FUNC.stringof); if (cb.failureDG !is null) { cb.failureDG(rargs); cb.failureDG = null; } } struct Outbound(alias FUNC) { this() disable; this(this) disable; ~this() { //writefln("~Outbound!%s [%s:%s]", FUNCNAME!FUNC, msgNum, fired); if (!fired) go(); } alias FQN = fullyQualifiedName!FUNC; static if (hasUDA!(FUNC, Reply)) { alias SUCCESS = void delegate(TemplateArgsOf!(getUDAs!(FUNC, Reply)[0])); } static if (hasUDA!(FUNC, Failure)) { alias FAILURE = void delegate(TemplateArgsOf!(getUDAs!(FUNC, Failure)[0])); } private Proxy proxy; private MsgNum msgNum; private bool fired; private this(Proxy proxy, MsgNum n) { this.proxy = proxy; this.msgNum = n; } static if (is(SUCCESS)) auto onSuccess(SUCCESS dg) scope return { static assert(is(SUCCESS), ("Reply not allowed for %s", FQN)); CallbackT!FUNC cb; if (auto p = msgNum in proxy.callbacks) { cb = cast(CallbackT!FUNC) *p; assert(cb, "Callback type mismatch"); assert(cb.successDG is null, "Success callback already set"); } else { cb = new CallbackT!FUNC; cb.msgNum = msgNum; proxy.callbacks[cb.msgNum] = cb; } cb.successDG = dg; fired = true; // Returning new struct so this is now defunct return Outbound(proxy, msgNum); } static if (is(FAILURE)) auto onFailure(FAILURE dg) scope return { static assert(is(FAILURE), ("Failure not allowed for %s", FQN)); CallbackT!FUNC cb; if (auto p = msgNum in proxy.callbacks) { cb = cast(CallbackT!FUNC) *p; assert(cb, "Callback type mismatch"); assert(cb.failureDG is null, "Failure callback already set"); } else { cb = new CallbackT!FUNC; cb.msgNum = msgNum; proxy.callbacks[cb.msgNum] = cb; } cb.failureDG = dg; fired = true; // Returning new struct so this is now defunct return Outbound(proxy, msgNum); } void go() { if (fired) return; fired = true; if (auto fireDG = msgNum in proxy.pendingCalls) { proxy.pendingCalls.remove(msgNum); (*fireDG)(); } } } auto opDispatch(string s, SA...)(SA sargs) { //writefln("opDispatch: <%s>%s", s, SA.stringof); alias FUNC = __traits(getMember, dest, s); alias FN = FUNCNAME!FUNC; alias FQN = fullyQualifiedName!FUNC; static if (!hasTemplateUDA!(FUNC, Proxy)) { pragma(msg, format("Cannot call function %s without Proxy UDA", FQN)); // Difficult to get compilation error messages inside opDispatch static assert(false); } alias PARAMS = Parameters!FUNC; static if (!canCallFuncWithParameters!(FUNC, SA)) { pragma(msg, format("Invalid parameters for proxy %s: expected %s, got %s", FQN, PARAMS.stringof, SA.stringof)); static assert(false, "Invalid parameters"); } else { auto msgNum = nextMsgNum++; auto outbound = Outbound!FUNC(this, msgNum); pendingCalls[msgNum] = { // This delegate calls the receiver object directly. In reality, we would // split Proxy into a Transmitter/Receiver pair and serialize a message // across the network to be reconstructed and remotely call the receiver // on the destination object. lastMessageNum = msgNum; __traits(getMember, dest, s)(sargs); }; return outbound; } } } void main() { auto person = new Person("bob", 34); auto proxy = new Proxy!Person(person); proxy.requestName().onSuccess((string str) { writefln("Client| Received name: %s", str); }); proxy.doubleEvenNumber(4).onSuccess((int r, int orig) { writefln("Client| Received doubled number: %s (Original number was: %s)", r, orig); }).onFailure((string str) { writefln("Client| Error: %s", str); }); proxy.doubleEvenNumber(3).onSuccess((int r, int orig) { writefln("Client| Received doubled number: %s (Original number was: %s)", r, orig); }).onFailure((string str) { writefln("Client| Error: %s", str); }); assert(proxy.callbacks.length == 0, format("Unhandled callbacks: %s", proxy.callbacks)); } ``` . My `util/extratraits.d` for some additional helper templates: ```d module util.extratraits; import std.traits; template FUNCNAME(F...) if (F.length == 1) { import std.string; enum FUNCNAME = fullyQualifiedName!(F[0])[ fullyQualifiedName!(F[0]).lastIndexOf('.')+1 .. $ ]; } bool canCallFuncWithParameters(alias FUNC, SA...)() pure safe nothrow { static if (SA.length > Parameters!FUNC.length || SA.length < numRequiredArguments!FUNC) { return false; } static foreach (idx, P; Parameters!FUNC) { static if (idx < SA.length && !isImplicitlyConvertible!(SA[idx], P)) { return false; } } return true; } size_t numRequiredArguments(alias FUNC)() pure safe nothrow { size_t numReq = 0; static foreach (argi, PD; ParameterDefaults!FUNC) { static if (is(PD == void)) numReq++; else return numReq; } return numReq; } string shortname()(string str) { import std.string; auto excl = str.indexOf('!'); if (excl > 0) str = str[0 .. excl]; return str[str.lastIndexOf('.')+1 .. $]; } bool hasTemplateUDA(alias SYM, alias UDA)() { static foreach (idx, attr; __traits(getAttributes, SYM)) { static if (__traits(isTemplate, TemplateOf!UDA)) { static if (__traits(isSame, attr, TemplateOf!UDA)) return true; } else { static if (__traits(isSame, attr, UDA)) return true; } } return false; } template isMemberVariable(alias THIS, alias SYM) if (isAggregateType!THIS) { enum bool isMemberVariable = !(isFunction!SYM || isType!SYM || __traits(isTemplate, SYM) /*|| hasStaticMember!(THIS, SYM.stringof)*/); } ```
Sep 10 2023
prev sibling next sibling parent Gulshan Negi <negigulshan903 gmail.com> writes:
Well, In D we can define Custom Attributes in very simple ways, 
lets understand it with the help of below example.

module mycustomattribute.d;

public struct MyCustomAttribute {
     string data;

     this(string data) {
         this.data = data;
     }
}

Apart from this we can use Custom Attributes to add additional in 
out code.

Thanks
Sep 13 2023
prev sibling parent Basile B. <b2.temp gmx.com> writes:
On Wednesday, 6 September 2023 at 18:54:26 UTC, Soham Mukherjee 
wrote:
 I'm exploring the D programming language and I'm curious about 
 custom attributes. How can I define and use custom attributes 
 in D to add metadata or behavior to my code?

 For example, let's say I want to create a custom attribute 
  MyCustomAttribute that I can apply to functions or types. How 
 do I define this attribute, and how can I use it to influence 
 the behavior of my D code?

 It would be helpful if you could provide code examples and 
 explain how custom attributes can be leveraged in practical 
 scenarios within D programming. Thank you for your insights!
I used UDA to - statically check that classes fields [did not require the GC](https://gitlab.com/basile.b/iz/-/blob/master/import/iz/memory.d?ref_type=heads#L227). - handle [main arguments](https://gitlab.com/basile.b/iz/-/blob/master/import/iz/options.d?ref_type=heads#L658) - define [serializable aggregate members](https://gitlab.com/basile.b/iz/-/blob/master/import/iz/serializer.d?re _type=heads#L2179), absurdly complex tho. - [compose functions](https://gitlab.com/basile.b/iz/-/blob/master/import/iz/sugar.d?ref_type=heads#L1413) A very simple example written recently (previous are rather old): [class qualifiers](https://forum.dlang.org/thread/gfqnotxyttdiqwfyvl o forum.dlang.org), allowing to restrict the candidate in an overload set, in a fashion that ressemble a recent c++ feature.
Sep 13 2023