slint-ui / slint

Slint is a declarative GUI toolkit to build native user interfaces for Rust, C++, or JavaScript apps.
https://slint.dev
Other
17.54k stars 600 forks source link

Convenience syntax for helper functions #174

Closed tronical closed 1 year ago

tronical commented 3 years ago

At moment it's possible to use callback handlers to define what can function as helper functions:

  callback helper(int) -> logical_length;
    helper(idx) => {
        idx * 30px
    }

    Rectangle {
        background: green;
        y: helper(1);
    }

This works well in the sense that the callback helper declaration acts like it is declaring a property and the handler declaration assigns the right body to it. It would be great if the parser would support a more convenient syntax to achieve the same semantical result:

function blah(idx: int) -> logical_length {
    idx * 30
}
ogoffart commented 3 years ago

We could also try to have a way to "initialize" inline

// no arguments
callback foo => { /*...*/ }
// with arguments
callback foo(abc: int) -> int => { /* ... */ }

Or is this => too much?


We also need to consider the side effects an threading issues of these function.

I think we have the following dimention for different kind of function or callback:

  1. Side effect: property binding should in theory be "pure" so they should not be allowed to call event or things that may result. This can only be enforced if we know what has side effect.
  2. synchronous: anything that returns a value for example need to be synchronous. But we could also imaging events which would have effect asynchronously because it is run in a thread for example.
  3. Can it be replaced by native code?
  4. Can we connect several handler to it, or only one handler.

Maybe we can use these keywords

No side effect side effect allowed
synchronous, 1 handler, can return value function callback
possibly async, possibly several handler N/A event
tronical commented 3 years ago

Or is this => too much?

IMO yes. I'd prefer function because that's what it is. If that comes with additional restrictions then I think that's fine, such as a check for side-effects.

I like the proposed table though, that's nice.

ogoffart commented 2 years ago

@tronical and I discussed this today.

We realized that there is many dimension to the problem of declaring a callback:

  1. Function that can be override (set) by native code or when instantiating (callback) or not (helper functions)
  2. Whether a function has a side effect or is pure. (The goal being to check, at compile time, that property bindings only use pure things)
  3. internal or public
  4. Synchrinous or queues (this could actually be specified when connecting to the callback. eg: clicked => queued { debug("foo") }

We haven't agreed on a good way to declare purity and if it should be the default and enforced. (@pure attribute or public pure function, or pure by default and public impure function or side-effect)

But we agreed that there is a need for non over-writable function such as

component CheckBox {
   public function toggle() {   // private by default
      self.checked = !self.checked;
      self.toggled(self.checked);
   }
   callback toggled(checked : bool); // callbacks are always public
}

For callback, we decided to

Be-ing commented 1 year ago

But we agreed that there is a need for non over-writable function such as

To clarify what you mean, in that example toggle would be callable from outside CheckBox but outside code could not assign toggle to some other function?

tronical commented 1 year ago

Our current thinking is this:

If you want to call a function from a binding or pure function, you then need to declare it pure. Later, if you want to add a side-effect to the pure function, you end up having to remove the "pure" declaration from the public function and chances are that you might see that such a change will affect your users.

ogoffart commented 1 year ago

PR that implements it: https://github.com/slint-ui/slint/pull/1989

ahayzen-kdab commented 1 year ago

Elsewhere in Rust the default is the "safe" or "better" option and you have to opt-in to the other option, eg by using unsafe or mut for safety and mutability. This makes me wonder if where possible things should be pure by default and you should have to opt-in to impure :thinking: But maybe I'm overthinking things and this would be too much boilerplate to mark most functions as impure :-)

ogoffart commented 1 year ago

We indeed are wondering if the default should be pure or impure. I think for callback it should be impure as most callbacks are meant to be impure. (Unless they return a value, maybe)

For function it is a bit less clear what the default should be.

Making it a different default based on the return value doesn't sound like a good idea.

That said, it is not clear what in pure or impure is the "safer" or "better". The equivalent of pure in rust would be const fn which is not the default

ahayzen-kdab commented 1 year ago

Right, so maybe having a side affect isn't strong enough reason to choose between pure/impure by default. And as you say for functions Rust doesn't use const fn anyway :thinking:

Reading your blog there is the example of calling a function that has a side affect during property evaluation.

  height: { 
      toggle(); // BAD: this changes another property during evaluation.
      return compute_size(42px);
  }

Once there are pure/impure, are you thinking here that the binding would need to have something tagged onto or an unsafe block to indicate that the developer really does want to call an impure function ? such as unsafe { toggle(); } or tag the whole property binding or something ? eg what would this look like with the pure/impure stuff ?

ogoffart commented 1 year ago

Based on the experiment in #1989 , I noticed that, after porting the examples to annotate the callback with pure, only two testcases do suspicious things by making use of side effects in properties. These tests are arguably not nice code and i don't know if their use case should be supported. I'm pretty sure however that people might rely in such hack like the ones in https://github.com/slint-ui/slint/issues/112#issuecomment-1065866865 However, if it involve native code, they can still mark the callback as pure and have the native code violate that. We don't protect against it So i don't know if we need a way to tell .slint that we want to do side effect in a pure function. If so, unsafe { } is not the right keyword. Maybe side-effect { toggle() } or toggle!() or 😷toggle()

ogoffart commented 1 year ago

Done