ID:1972495
 
Applies to:DM Language
Status: Open

Issue hasn't been assigned a status value.
Fundamentally, DM as a language could use delegates (C#) or another formalized function pointer. I am aware of hascall(), call(), and the : operator, but the places in which this is not formalized show through.

Briefly:
/delegate
var/datum/object
var/function
proc/Run()
/hook
var/list/delegates
var/return_condition = RETURN_NONZERO
proc/Run()


The idea being, most cleanly:

/mob/var/hook/on_death
/mob/proc/die()
on_death.Run()

/mob/poet/New()
on_death += delegate(src,/mob/poet/proc/dramatic_quote)
Although if I were to get everything I wanted for Christmas, it would look more like:
/mob/hook/on_death(var/timeofday, var/death_reason)
// Code before hooks are executed
. = ..()
// Code after hooks are executed
/mob/poet/proc/dramatic_quote(var/timeofday, var/death_reason)
say("Alas, [timeofday] I died, and all because of [death_reason]!")
mymob.on_death += delegate(mymob,/mob/poet/proc/dramatic_quote)
mymob/on_death("today","blood loss")

Where hook is a keyword like proc and verb, and the entire code block for the hook being optional, defaulting to .=..()

The return condition mentioned for hooks is a bitfield that specifies how to handle execution and return values. For example, when running ten procs, the third and seventh procs return a nonzero number while the rest return null. Depending on the actual circumstances, you may want execution to halt with the first zero/null or nonzero number (eg Cross()). You may want to run all of the procs, and return the largest return value (this also works if you don't care about the return, eg, Crossed()).

You may want to run all of the procs, and return a list of all return values. And you may only want one sentry value to cancel execution (in particular, null) while any other value is recorded in the list. You might also want a return result that is an associative array of (delegate=result), in case you need to locate the object that had a particular return value.

At minimum, any event hook will need to have the option of dying under certain (non-error) circumstances.

Delegates and/or Event hooks can also function as Topic() replacements. For example,
/mob/hook/click_me(var/why, var/how)
html_link = "<a href='?\ref[src.click_me]why=fun&how=html'>Click me!</a>


The facilities for this already exist and the above could in principle simply be done with library code:
/mob/var/hook/clickme_proc
/hook/Topic(href,href_list)
src.Run(arglist(href_list))

Still, it would be much better if this was formalized for a number of reasons.

It's far from an urgent feature request but I already mostly-wrote a library version of this, and it sort of... lacks. It isn't part of standard byond code, I have to workaround areas where I don't control the language. For example, compare:
/mob/hook/test(var/argument = null)
if(argument != null)
return ..()
else
src << "Argument required"
mob.test()
mob.test("a")

to:
/mob/var/hook/non_empty/test
/hook/non_empty // Run() calls RunList() with the args list
RunList(var/list/arguments)
if(!len(arguments) || arguments[0]==null)
usr << "Argument required"
return
..()
/mob/New()
test = new /hook/non_empty()
usr = src
test.Run()
test.Run("a")

...but that's not really your problem or anything, it's just that it could be a lot better.

Mostly I just think that delegates/function pointers and event hooks are a worthy addition to the toolkit. I haven't even run the well dry on things you could do. For example, the library version of hooks calls all procs defined on it, eg:
/hook/game_started/proc/notify_players()
for(var/client/C)
C << "The game has started!"

(This is implemented as a for(typesof("[type]/proc")) if you're curious.)

By adding that proc in code to the hook you guaranteed that it would be in the list of delegates. If hook is a keyword like proc, it would look really weird, sort of like
/proc/game_started/proc/notify_players()
...
game_started()

but I think it could work.

I think I am in danger of rambling on endlessly so I will wait for questions, feedback, or whatever.
I like the idea of built in fast hooks.
Honestly I'm a little lost here. I'm not quite understanding what's needed here that can't be handled more flexibly in soft code.
The cost of using soft code to do it is high. Function pointers are inherently a language construct, and libraries that try to add language features rarely work right.

For example, I suggested that nice syntax where hooks look like a function when they're run (because they are) while still acting in some ways like an object, in particular a special sort of list (which they are). A library-based hook is just an object, and whatever name you give the hook's Run proc is almost certain to be ambiguous or misleading--Run() for example is a proc that can be put on all sorts of objects, especially library code, to start some process. Call() is not a lot better. Longer, more descriptive proc names work but are a bad choice for library code.

Also, the code underlying named arguments is only slightly exposed by arglist(), and it's consequential to the function of the library. Delegate/Run() by necessity must take variable arguments (using the args list). If you want to specify only the third argument and keep the others as default, which is totally a thing you may want to do, you can't, because variable arguments cannot support named arguments, because the arguments aren't named. You can specify the first two arguments are null, but there is a difference between null values and non-supplied values. A library can get around this using lists, as long as you are okay with:
//implicit to this call are default arguments 1 and 2
myDelegate.RunList(list("arg3"=someval))

Again, it gets fairly close, I'm not gonna lie. In my implementaion Run() just calls RunList(args), which works in a number of cases.

Inherent type-safety is also nice. Libraries can do it, but it would be nicer formalized.

... I dunno.

Look, I'm probably not gonna be able to put up a strong defense of my point right now, I have a major headache. I think it would be great, even if it's just essentially a builtin library and not used in any major way by native code. It could also be a lot more depending on the work that's put in.

And again, delegates/function pointers are most useful in event-driven programming, which is a lot of Byondcode. I'm not saying you should replace all the event procs with hooks, in fact I'm not sure I'd recommend it, but you could. Enter()/Entered() are really just polling subscribers, where the subscribers are their contents list. In that case, it's exceptionally straightforward, but similar events could use more support.

It's not a huge deal either way.
In response to SuperSayu
SuperSayu wrote:
You can specify the first two arguments are null, but there is a difference between null values and non-supplied values.

DM doesn't really recognize such a difference. Pretty much the only way to tell if an arg was non-supplied is by checking args.len. I'm not fully clear on whether something like myproc(arg3=something), where the proc is defined as myproc(arg1,arg2,arg3), will send a 3-element list with the first two elements being null or not, but the behavior is undefined.

Inherent type-safety is also nice. Libraries can do it, but it would be nicer formalized.

The only function pointer concept existing in DM is the use of call(). It's less different from a strictly hard-coded call than you might think.

I don't think type safety could be extended at all unless the compiler had a notion of a typed proc pointer that included arg type info. But probably a lot depends on your hook implementation. I'm not really understanding how the syntax comes together here.

One of the places I'm tripping up is this:

/mob/hook/click_me(var/why, var/how)

DM syntax is clear that this would be interpreted as a redefinition of a click_me() proc, the original being declared in this type or one of its ancestors, of a mob type called /mob/hook. So that would not be valid syntax in any way, and is too radical a departure to be considered as a change. Something like this might fit:

atom/proc/hook/click_me(why, how)

A keyword like hook might be insertable after proc, much like tmp is after var.
In response to Lummox JR
Lummox JR wrote:
DM doesn't really recognize such a difference. Pretty much the only way to tell if an arg was non-supplied is by checking args.len. I'm not fully clear on whether something like myproc(arg3=something), where the proc is defined as myproc(arg1,arg2,arg3), will send a 3-element list with the first two elements being null or not, but the behavior is undefined.

Last time I checked, when defining a function with arguments, the arglist will always contain the defined arguments. So yes, in this case, it will send a 3-element list with the first two elements being null.
In response to Kamuna
The difference with non-supplied arguments is when they have defaults:
proc/myproc(var/a="a",var/b="b",var/c="c")
myproc(c="d")
myproc(null,null,"d")

EDIT: upon testing I find this is inaccurate. I was sure you could pass null to invalidate supplied arguments. Whatever

As for the keyword syntax for hook, I am mostly just guessing in the dark. The point of making a "hook" reserved word like that was to parallel the distinction between procs and verbs (understanding that there isn't much of a one but verbs are treated slightly differently); again, this being the more extreme end of the "DM Language change" spectrum.

I suppose that what I am suggesting for hooks is essentially a list of procs, which can be treated as either a list or a proc under different circumstances. In pseudocode it's fairly simple:

When called:
Call default handler aka run() with the supplied arguments. If a handler was specified at compile time, the default handler gets run with ..(). When the default handler is run, all linked delegates execute with a copy of the supplied arguments.

Operator +=/-=/add/remove:
Add or subtract delegates to the stored list

While we're on the subject of things that simple in principle, creating a delegate with a list of objects could return a list of delegates, but I'm ahead of myself by a great deal. Still, it would be nice to this in one line:
new /hook(delegate(contents,/obj/proc/test))()

(which would call /obj/proc/test on all objects in contents)
(you know a one line command to filter lists that just returns a new list filtered with that mask, eg, "contents as obj" returning only /obj contents of src... never mind, I think I am going crazy)

I apologize for the delay in responding but I am not feeling well.