Proposal: user defined attributes

Adam D. Ruppe destructionator at gmail.com
Tue Mar 20 09:31:45 PDT 2012


On Tuesday, 20 March 2012 at 15:17:04 UTC, Andrei Alexandrescu 
wrote:
> Also, as I mentioned, the availability of the easy escape hatch 
> of adding a language feature thwarts creativity.

I agree entirely. Usually, when I see threads like this,
my response is "challenge accepted".

But, on this problem, my answer is, unfortunately,
"challenge failed".

I want to annotate declarations for my web.d, where
you can write a class and have D do a bunch of work
for you in creating all kinds of stuff.

Let me do up a quick example:

===
struct MyType {
    int num;
    string name;
    string desc;
    Element makeHtmlElement() {
       auto e = Element.make("div", desc).addClass("my-type");
       return e;
    }
}

class MySite : ApiProvider {
     MyType getSomething(int someNumber, string name, Text 
description) {
          return MyType(someNumber, name, description);
     }

     void savePhoneNumber(string name, string phoneNumber) {
         _checkLoggedIn();
         ensureGoodPost();
         _doSave();
     }

     void _doSave() { yada yada yada }
}
===


Now, when you go to mysite.com/get-something, it will
generate a form like this:

       <form method="GET">
   Some Number: ____________
          Name: ____________
   Description: ____________
                ____________
                ____________

               [Get Something]
        </form>



It pulls this info from the D code - it uses an alias
template param to the method, then parses .stringof
to get the names and default params. (It then mixes
in the default to get a D type, which doesn't work
on custom types since they aren't in the right scope,
but hey, it works in a lot of cases.)



When possible, it uses the type of the parameter
to specialize the form. This comes from ParameterTypeTuple.

Here, it recognizes struct Text{} as being a textarea.
tbh, I'd prefer to use @note(Textarea(rows)), but this
works, and alias this to string makes it pretty natural
to work with too.



Moreover, I wrote method="GET". It pulls this from the
method name. Since it starts with get, it doesn't require
http post for it.



So far, so good. Let's look at the next method, savePhoneNumber.
This !startsWith("get"), so it generates a POST form, with
the same kind of thing:

         Name: _________
Phone Number: _________
      [Save Phone Number]


Hey, it works. But, I'd like to do even better. Could
we do validation?

Well, yeah, I think we could.

struct PhoneNumber {
      enum _validationRegex = "[0-9]+";
}

Boom.

What about an example text, though? You could use
that same enum method, but you would need a different
type for each example.

If you have two "string name" and in one case, you want
it to be "e.g. Bob Smith" and in the other, you want
"e.g. Downtown Office", you'd have to make separate
structs for them, despite it just being strings in
every other way.


That's one big case where the compile time annotation
comes through:

void savePhoneNumber
     @note(ExampleText("e.g. Downtown Office")
         string name,
     PhoneNumber phoneNumber)
{ }


To do that with a mixin, you might:

mixin(ExampleText!(savePhoneNumber, "name", "downtown"));
void savePhoneNumber(string name, ...) {...}

and have that yield:

enum _attribute_DsavePhoneNumberMangle_param_name_FILE_LINE_type 
= ExampleText("downtown");



ok, this is a bit of a pain in the ass, but I think it'd
work... (the file_line_type stuff is so you can have multiple
things of the same type. Moderately fragile! But, it'd work.)


The pain gets bad down the line though. Renaming things means
repetition. Separate declarations might get lost as you
shuffle the order around. But, not too awful...


Until you get into more functions. Enter 
arsd.web.prepareReflection
and makeJavascriptApi:

foreach(member; __traits(allMembers, yourclass))
     // build a runtime map and corresponding bindings for other 
languages



Should our enum be included here? Should this include code
to detect the annotations and translate them to the
native target language? How much code is this going to take?

What if I use a third party lib? Will it have to be hacked to
ignore these extra names too?


Well, we can work around this too. web.d has a convention
where static if(name[0] == '_') { don't process }. (This
is actually a workaround for a lack of __traits(getProtection)
though. We already have D keywords that do this: public
and/or export. But, there's currently no way to check for these,
so I settled for a naming convention.

This is what _doSave() demonstrates in the example.

I'm hoping to do a dmd pull request to add getProtection at
some point though.)

We could all agree on a naming convention, in theory. In
practice, I don't know. If I could check export || public,
I might drop the underscore.



Another problem is how do we get this data? We have
an alias of the method... but not it's parent. Well,
there's __traits(getParent). I think we could actually
do that.

But the idea of a parent reminds me of another problem.
This is a class, the method is virtual.

What if a subclass overrides it and wants to tweak
the annotation?


How will we handle annotation inheritance? Overloading
is kinda solved via mangleof (I think, I haven't tried
this yet, but in principle it would work), but inheritance
is a whole other bag.


Perhaps we do ClassName_methodName and in the search go
up the base class tree?


I don't know... I think we *could* make it work at this
point, but it depends on an agreed naming convention,
where we (essentially) embed little DSLs in member names,
packing all kinds of info in there to work correctly.

Piles upon piles of string processing code that we'd
all have to agree on for reflection work, with
complications around almost every corner. If we
solve overloading and inheritance, what will be
next?



Embedding info directly in names is the easiest,
but a bit problematic too:

getSomething()


What if I want it to be http GET, but be named
say, "something-viewer"?

Currently, I implement that as two functions:

getSomething() for machines to access and
alias getSomething somethingViewer;
for human users.


It isn't ideal, but again, it works.
(The ensureGoodPost() function does a runtime
check against CSRF. That's a potential candidate
for an annotation too, but the exception works
perfectly fine too, so meh.)




Another thing I'd like, in the same vein as
the ExampleText, is Hideable. Again, not really
a type, though it could be with enough special
case code added throughout to handle it.

But, this would be a parameter that, if given,
is hidden instead of showed:

void changeName(@note(Hide) int id, string name) {}

link to change-name?id=1, and then you don't have
to show the ID in the automatically generated form.

Don't want to always do this though:

send-message?message=example+message

we *want* that to be editable...


(In practice, right now, I make the form another
function and modify it myself. That's several
lines for something that could be done generically
though, which is pain.)







Well, I was going to go on longer, but I think we
have enough here to draw a conclusion.

Can we do it? Well, I kinda think we can. Hell,
I already do like 3/4 of what I want in web.d

With the power of D, we could make all kinds of
weird stuff encoded as strings, stored as variable
names.


But, it is hard to use at most levels. Writing the
annotations is a long, repetitive process. Processing
them takes a lot of fragile code.

Even /ignoring/ them takes a lot code, and that's
what, to me, pushes it over into compiler territory.

Compiler provided annotations are opt-in and backward
compatible. If you want them, they're there, and if
you don't, they aren't in the way like magic enums
would be.


More information about the Digitalmars-d mailing list