Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamic function calling. #6713

Open
wants to merge 24 commits into
base: feature/script-reflection
Choose a base branch
from

Conversation

Moderocky
Copy link
Member

@Moderocky Moderocky commented May 19, 2024

Additions

New types
executable
function
Obtain function references by name (& source)
[the|a] function [named] %string% [local:(in|from) %script%]
[the] functions [named] %strings% [local:(in|from) %script%]
[all] [the] functions (in|from) %script%
Run function references & get results
# effect
(run|execute) %executable% [with [arg[ument]s]] %-objects%]
# expression for return value
[the] result of %executable% [with [arg[ument]s]] %-objects%]
Little examples
set {_function} to the function named "myFunc"
run {_function} with arguments "hello", "there"
set {_thing} to the result of {_function} with arguments "hello", "there"

Infrastructure

Road-map of future plans

I'm setting down a bit of API here for future Skript features/addons to use,
especially ones that want to run tasks.

I have a few ideas about what I would like to do with this, but nothing finalised yet.

image

Target Minecraft Versions: any
Requirements: none
Related Issues: fixes #1265, fixes #5838, requires #6702

@Moderocky Moderocky added the enhancement Feature request, an issue about something that could be improved, or a PR improving something. label May 19, 2024
@Fusezion
Copy link
Contributor

What about #6254?

@Pikachu920
Copy link
Member

also see #6254 (comment). i have to be honest - i'm not really a fan of reflective capabilities in vanilla skript

@sovdeeth
Copy link
Member

I, too, am not a big fan.
Will update this message with links to my previously expressed opinions on this tomorrow.

@Asleeepp
Copy link
Contributor

Sorry if im blanking right now, but why would you even need this in the first place? you could just... run the function? i dont see any point for this atleast in my eyes

@APickledWalrus
Copy link
Member

APickledWalrus commented May 19, 2024

I am not against the idea in general mainly for the fact that it provides a way to execute functions in a more natural way

I haven't yet reviewed the implementation so I can't really comment on that yet

@Phill310
Copy link
Contributor

Sorry if im blanking right now, but why would you even need this in the first place? you could just... run the function? i dont see any point for this atleast in my eyes

I would guess it would be used to do something like

command /kit <number>:
  trigger:
    run function named "giveKit%arg-1%"

But i dont see it as that useful personally

@Fusezion
Copy link
Contributor

I would guess it would be used to do something like

command /kit <number>:
  trigger:
    run function named "giveKit%arg-1%"

But i dont see it as that useful personally

Personally hate this example a more fexible one would be custom enchantments and

function enchantment_NAME(level:number,player:player)

replace name with enchantment name each time and call it from there, so you have basically a wrapper method now

@EquipableMC
Copy link
Contributor

I’m going to be flat out honest here, and this is just how I view things.
Skript should be kept simple. This is way to advanced for the average Skript user.
Skript-reflect is for advanced Skript users who have some knowledge of Java & a lot of knowledge of Skript. This seems like something that should be put into skript-reflect rather than Skript itself. I just don’t see a point in this being in Skript.

@Pikachu920
Copy link
Member

Pikachu920 commented May 20, 2024

i agree that dynamic function calling is a useful feature, but i think it should be a simple run function "x" effect and an expression to get the return value (see this). primarily, i don't want functions to be treated as objects. skript-reflect has sections for that purpose if people are really itching for that pattern in skript.

@cheeezburga
Copy link

I’m going to be flat out honest here, and this is just how I view things. Skript should be kept simple. This is way to advanced for the average Skript user. Skript-reflect is for advanced Skript users who have some knowledge of Java & a lot of knowledge of Skript. This seems like something that should be put into skript-reflect rather than Skript itself. I just don’t see a point in this being in Skript.

I really don't think this, as a concept, is that hard to grasp for an average user. Whether or not it belongs in Skript or reflect is another debate, but I really see no problem with the complexity side of things. In my eyes its the same kind of thing as the math functions - if they don't understand, then it's probably not for them, and they can just call the function normally.

@Moderocky
Copy link
Member Author

Moderocky commented May 20, 2024

I thought I would take the opportunity to clean up a few questions about functions.

What does dynamic calling do?

Sorry if im blanking right now, but why would you even need this in the first place? you could just... run the function? i dont see any point for this atleast in my eyes

Dynamic (or indirect) calling means running something when you don't explicitly know [what/where] it is. This doesn't need to be linked to reflection (in fact C doesn't really have a concept of reflection, but still has call-by-pointer functions).

It means you can prepare a function call in advance, store it (or even pass it somewhere else) and then invoke it when you need to.

function completeQuest(user: player, onSuccess: function):
    # check quest completion logic
    send "You've finished the quest!" to {_user}
    if {_onSuccess} exists:
        run {_onSuccess} with arguments {_user}

In this case, it also allows you to call a function by its name (e.g. run function "startQuest_%{_name}%")

It's been a staple of programming since the COBOL and FORTRAN days.

Even non-object oriented languages almost always use some kind of member reference (with the exception of R).
Click here for some examples :)

Go

object := // ...
m := object.MethodByName("myFunction")
if m.IsValid() {
    m.Call(nil)
}

Java

You've got executable interfaces (e.g. Runnable, Function, Supplier, Consumer)

Runnable object = // ...
runnable.run();

// method reference!
Function<String, Integer> func = String::length;
int length = func.accept("hello");

And reflection, of course

var object = // ...
Method method = object.getClass().getMethod("myFunction");
method.invoke(object);

JavaScript

const object = // ...
const myFunction = Reflect.get(object, 'myFunction')
Reflect.apply(myFunction, object, [])

Pascal

begin
  RttiType := RttiContext.FindType('Unit1.MyType') as TRttiInstanceType;
  Object := // ...
  try
    RttiType.GetMethod('myFunction').Invoke(Object, []);
  finally
    Foo.Free;
  end;
end;

Lisp

(let* ((my-class (find-class (read-from-string "myclass")))
       (myfunction-method (find-method (symbol-function (read-from-string "myfunction"))
                                        nil (list my-class))))
  (funcall (sb-mop:method-generic-function myfunction-method)
           (make-instance my-class)))

C

Even languages that don't have any concept of reflection like C still have first-order functions:

bool (*myfunc) ();
myfunc = // ...
if (myfunc())
    // ...

Skript doesn't really have anything like abstraction or polymorphism (or even overloading functions) so there is a big gap in the language when it comes to reusing code.

Why add a function handle object?

i agree that dynamic function calling is a useful feature, but i think it should be a simple run function "x" effect and an expression to get the return value (see this). primarily, i don't want functions to be treated as objects. skript-reflect has sections for that purpose if people are really itching for that pattern in skript.

It's true that we could pass the name of a function around and call that, but a first-order object has significant advantages in a number of areas.

It's much more efficient to obtain a binding once and then reuse it.

Obtaining a function from its namespace, creating its reference and verifying it is actually a pretty slow task (comparatively speaking), which isn't a problem when it's done at parse time with myFunction().

If we have a first-order object, we can resolve the function & its bindings, check security, validate it once when we create the object:

set {_func} to the function named "hello"

At the making of _func we can already resolve the function from its namespace, check it exists, do some* of its validation (sign, return type, etc.)

Until we discard _func this never needs to be repeated, for the same reason you're advised to keep your Method object for as long as you can in Java, rather than re-acquiring it each time.

If you're resolving your function every time a task is run (maybe every game tick) then this is adding a huge amount of inefficiency.

We want to do our safety checks as early as possible.

We don't really have the option of run-time errors or feedback in Skript.
Ideally, if our dynamic function isn't a real function (or if the user is misusing it) we want to discover that as soon as possible, so the user can obtain that feedback.

If we have a handle then we can use that to report when something goes wrong.

Of course, we can do that all by name:

if a function named {name} exists:
    run function {name}

but this is pretty inefficient, we're resolving the entire namespace & function twice within two lines, and our initial check isn't even comprehensive (we haven't, for example, made sure that the function can accept the arguments).

set {func} to the function named {name}
# we perform our lookup once
if {func} exists: # something was matched?
We can pass them by reference to other places that use them.

Here's part of Tud's chess game, made using his(?) addon that provides dynamic function access.
Edit: apparently the addon is called unsafe-skript.

image

In this example, Tud registers movement functions for each chess piece. When a chess piece needs to be moved, it can just invoke the function it was given, rather than having to resolve anything.

Of course, this could be emulated by storing a function name, but that would mean resolving everything on each move, and we wouldn't even have a function name to go off at parse time so we couldn't do any ahead-of-time safety checks.

We can create a dynamic bootstrap in the place they're executed. This saves us having to verify parameter types multiple times.

Function parameters should be verified before being executed.
This is a very intensive operation with more than a hundred lines of checks.

If we have a function object, we don't need to reverify it if it's being used in the same place.

Regular static function references verify once at parse time using the argument expressions. We don't have that luxury since we can't know for sure what the function is at parse time, but we can still do it before resolution:

run {function} with arguments "hello" and 52

The first time this line runs, we'll have to verify function can accept a String and a Number. This verification can be stored in the dynamic function bootstrap, so when we encounter the same line again, {function} already knows it can handle those expression types and doesn't need to run the verifier again.

This provides exactly the same level of verification that a static function call gets, and it avoids having to repeat it every time the function is called.

Tud's Example

Thanks to @UnderscoreTud for providing me his chess game code to use as an example.

Tud's chess game
chess.mp4

Excluding the chat-rendering, the entire game comes in at barely 200 lines of code because it's written so efficiently.

What about the other dynamic functions attempts?

What about #6254?

Good question! There are at least three pull requests for dynamic functions and a testing branch. My implementation has a slightly different focus, and I'm building off my work in #6702.

I liked #6254 but I had a few safety concerns, particularly around dead references.

Tud registers a Function directly as the object. I was worried about exposing this to scripts or addons, because it's not actually safe to call by itself (you have to do a bunch of things around this, like resetting the return value storage).

I'm also concerned about storing direct references to functions, because they could change if a script reloads or is disabled. Storing the Function in a variable will prevent it being garbage-collected, and I'm not even sure what would (or should) happen if you tried to invoke a function after its script unloaded. This could create a significant memory leak.

My implementation creates a dynamic bootstrap for a function and its source script, which doesn't hang on to the function when it needs to be garbage collected and makes sure the source script is still valid (not having been reloaded) when you call it. I think I can also get a performance advantage here by only performing the validation once.

I wanted to use some of the tools I set down in #6702 and I thought it would be a bit impolite to start dismantling Tud's work on his branch 😬

I disagree with the approach taken in feature/dynamic-functions-testing.

I am strongly against things like last return type. This is unsafe by design (any other effect can overwrite this) and I think they are clumsy to use.
I was really glad when we finally got an alternative for last spawned entity.

Of course, you can make it safer with ThreadLocals and mapping event <-> return value (like we do with loop-value and such) but this becomes a real mess for something that didn't need to be so difficult.

This is entirely personal preference, but I hate having to reference stuff by ID each time. It feels really clumsy to write.
I hated addons doing things like create boss bar by id %string% and send embed with id %string%, I would always much rather have an actual variable for it.

@TheBentoBox
Copy link
Member

To throw in my two cents, I’d generally caution against becoming overprotective of Skript’s simplicity to the point that we reject powerful and useful features. I agree that this isn’t even very complex, but even if it was, I don’t see how a language with a goal of being beginner friendly necessitates literally every aspect of it being beginner friendly. The language can both be beginner friendly and have complex elements as long as those complexities aren’t required of beginners. I’d even argue including an intentional amount of extra opt-in complexity is a great way to help beginners deepen their skills in a low-commitment way.

I’d also argue this only comes off as reflection by necessity. Half of the huge gain here is the ability to pass callbacks, which essentially every other language supports and is in itself not reflexive in many languages. A JavaScript example can also be done as follows:

class Foo {
  bar() {}
}

let v = new Foo();
someOtherFunctionAcceptingACallback(v.bar);

The ability to pass callbacks is a key developer tool that we’ve always been missing. Shove it in skript-reflect if you want, but it feels like a key part of a language so I’d argue Skript core makes more sense as its home.

@sovdeeth
Copy link
Member

I, too, am not a big fan. Will update this message with links to my previously expressed opinions on this tomorrow.

My opinion has been swayed by @Moderocky's comments on the benefits of first-order functions, I think this is a good idea.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement Feature request, an issue about something that could be improved, or a PR improving something.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

10 participants