Git Product home page Git Product logo

Comments (7)

jleeothon avatar jleeothon commented on June 14, 2024

In requirement 2, to "wrap" around methods (global-scope functions and subroutines too?), does it mean you don't just use the equals operator (=) to point to an existing function? You could "wrap" it with additional functionality like either of

  • opening its value
delegate mydelegate: // signature goes here
    return onething() + another()
  • making some sort of anonymous function (is this even syntax?)
delegate mydelegate = :
    return onething() + another()

The above examples may have something to do with the last point you mentioned about function types and creating delegates by themselves. Correct me if I'm wrong.

In requirement 2, on the word instance... Do you mean just calling instance methods from any object instance? (somebody.dosomething())) Or do you mean that inside a class/struct/etc. we could call this's methods --and more broadly, the this is defined (e.g. mydelegate: this.dosomething())

Requirement 3 is a bit unreadable ("default" can be understood as an adjective instead of a verb). Maybe word it like "should default".

On overloading, do you mean that inside a class it is possible to have two delegate members with the same name but different signatures? That is an interesting (new?) concept to statically typed languages. I bet use-cases are limited, so we can push them farther in the release plan. Also, consideration of variable length argument lists and dictionaries could make fork Hyve design paradigms, with an impact in designing delegate overloading.

from corto.

SanderMertens avatar SanderMertens commented on June 14, 2024

With "wrapping" I actually mean just that - having the delegate point to either a lang::function, lang::method or maybe even a lang::observer. Preferably assignable by using the '=' operator. For example (bear in mind that the following code may be completely inconsistent with other examples due to a lack of design at this point):

uint32 timesTwo(uint32 i) = i * 2;
var delegate{uint32, {uint32}} myDelegate = timesTwo;
myDelegate(2); // returns 4

Another example where a delegate is used as a member

class HelloWorld::
   hello: delegate{string}; // Delegate that returns a string without parameters

string helloInDutch() = "Hallo Wereld";
string helloInEnglish() = "Hello World";

HelloWorld hw: helloInDutch;
hw.hello(); // Returns "Hallo Wereld"

Another example in which a delegate is used to wrap a method call

class SpaceShip::
    x, y: uint32;
    void move(uint32 x, uint32 y): 
       this.x = x; this.y = y;

SpaceShip rosetta: 0, 0;
var delegate{void, {uint32, uint32}} moveShip = rosetta.move;
moveShip(10, 20); // Changes position of rosetta

from corto.

SanderMertens avatar SanderMertens commented on June 14, 2024

The above design actually seems feasible since it hits all the requirements of the first comment. The defaulting-to-base behavior is illustrated in the following example:

class SpaceShip::
    damage: uint32;
    turret: delegate{void, {SpaceShip}}; // Fire at another spaceship

class XWing: SpaceShip::
    turret: delegate{void, {SpaceShip}};

void photonCannon(SpaceShip s): s.damage += 10;

XWing myXWing: 0, photonCannon, null;  // Assign SpaceShip::turret delegate
myXWing.turret(myXWing); // XWing::turret is null so SpaceShip::turret is called.
// Ship has self-destructed...

This behavior is very useful in the same way overridable methods are useful. It also serves a real purpose for the type-system, since it allows subclasses to automatically default to the constructor of their base class.

Overloading in this case seems to be off the table. If delegates are no longer procedures but types than overloading can't be done (overloading is not supported on members or variables). Given the obscurity of the feature, I suppose that's not such a big problem.

from corto.

SanderMertens avatar SanderMertens commented on June 14, 2024

Q: how to map delegates to the type system:

As an instance of struct (bad)

struct delegate::
    returnType : typedef;
    params : sequence{parameter};
    proc : function;
    instance : object;

Usage:

uint32 get10() = 10;
delegate d: uint32, { }, get10, null;
d(); // return 10

This approach is unfavorable. It would create strong coupling between the language (bindings) and the delegate struct, without a delegate being a fundamental building block of the type system.

As a subclass of struct

enum compositeKind:: ...., DELEGATE;
struct delegatedata::
    proc : function;
    instance : object;

class delegate : struct, private::
    returnType: type;
    params: sequence{parameter};
    int16 init(): this.base = delegatedata; // Implicitly inherits from delegatedata

uint32 get10() = 10;
delegate getN: uint32, { };
getN f = get10;
f(); // return 10
f.proc; // refers to get10

This approach is nice. It separates the things that are immutable (return type, parameters) and the things that are mutable (procedure, instance). Usability is also good enough - though the delegate declaration can use some syntactic sugar. This would be ok, since delegate would get its own type kind, therefore the language can define special syntax.

The following example shows assigning an instance method to the above delegate:

class MyNum::
    num : uint32;
    uint32 getMyNum() = num;

MyNum n: 30;
f = n.getMyNum;
f(); // returns 30

For improved usability, delegates should define syntax that goes beyond regular object instantiation. The following snippet shows the above example with a possible delegate syntax:

uint32 get10() = 10;
uint32 getN() delegate; // reuses regular procedure syntax
getN f = get10;
f(); // return 10

from corto.

SanderMertens avatar SanderMertens commented on June 14, 2024

Using delegates with objects

This comment assumes the 2nd approach described in the previous comment. With a "small" extension, delegates can become a much more powerful mechanism for creating abstractions. Consider the following code:

uint32 getN() delegate;
uint32 i: 10;
getN f = i;
f(); // return 10

In addition to being able to refer to (and call) procedures, delegates could also point to objects. This notion is derived from the idea that accessing the value of an object of type X is not meaningfully different than calling a function without parameters with returntype X.

In this sense, delegates become a generic mechanism for specifying an interface to data, regardless of how this data computed or stored. Similarly, there is no reason why delegates shouldn't be able to refer to pure values:

uint32 getN() delegate;
getN f = 10;
f(); // return 10

To enable such behavior, the above definition of delegatedata would have to be changed to:

struct delegatedata::
    proc : function;
    instance : any;

Note that when allowing for values (and thus variables) to be assigned to delegates, there is a need for disambiguation between referring to an object and referring to its value for non-reference types:

uint32 getN() delegate;
getN f = 10;
f(); // returns 10
uint32 i = 20;
f = i;
f(); // returns 20
i = 30;
f(); // still returns 20, assignment was by value
f = &i;
f(); // returns 30
i = 40;
f(); // returns 40

The above example suggests a bit of intelligence on the delegates part, it is able to abstract from objects and values, without exposing a reference to its external interface.

However, Delegates, like functions, should also be able to expose references:

uint32& getN() delegate;
uint32 i: 10;
getN f = i;
f() = 20;
f(); // returns 20
i == 20; // is true 

from corto.

SanderMertens avatar SanderMertens commented on June 14, 2024

A deeper reality of types, values and functions

It seems that when doing some thought experiments, the type system might expose a deeper "truth" that is underpinning all types and values. Consider the following concept:

uint32 getElem(uint32 i) delegate;
array{uint32, 3} ints: 10, 20, 30;
getElem f = ints;
f(1); // returns 20

Rather than creating new semantics between delegates and collection types, suppose that there is a generic mechanism that allows values to be "callable". In other words, ints() would return {10, 20, 30}. ints(1) would return 20. For now, I will refer to this mechanism as a "callable".

In code:

uint32 i: 10;
i; // 10
i(); // 10
10(); // 10

The above would imply that arrays (or more generically, collections) have defined two callables - of accessing its value. Currently the language cannot express such callables, but the following conceptual syntax makes an attempt:

<@> () = this; // @ refers to the type of this
<@.elementType> (uint32 elem) = this[elem];

Or perhaps, an interface that converts the value to a string:

<string> () = this.toString();

A function then is reduced to an ordinary object that happens to be callable in a specific way:

uint32 multiply(uint32 a, uint32 b) = a * b;

would be equivalent to (bear with me on the syntax):

void multiply <= <uint32> (uint32 a, uint32 b) = a * b;

Overloading now also would become part of the type system - which is a good thing:

void multiple <= 
    <uint32> (uint32 a, uint32 b) = a * b;
    <float32> (float32 a, float32 b) = a * b;
    <string> (string s, uint32 n) = s * n; 

With the above notion, a delegate "just" resolves the appropriate callable of an object that it is assigned to. Upon invoking the delegate, the callable is invoked. All types would implement a default callable without arguments, which just returns the value.

This concept of "everything is callable" is quite powerful. It would unify the following concepts in a single abstraction:

  • values (variables, objects, literals)
  • procedures
  • overloading
  • closures (in the above example, multiply doesn't have to be a void!)
  • operator overloading (yes - I'll provide further comments on this)

from corto.

SanderMertens avatar SanderMertens commented on June 14, 2024

Operator overloading and callables

It seems that callables are an excellent candidate for implementing operator overloading - if this will ever be implemented. Consider the following code (ignore the recursion for now):

void @+ <=
    <uint32> (uint32 a, uint32 b) = a + b;
    <float32> (float32 a, float32 b) = a + b;
    // etc..

Now if I want to define a "+" operator for my own type, I could do this:

class Point::
    x, y: uint32;

void @+ <=
    <Point> (Point a, Point b) = {a.x + b.x, a.y + b.y};

var Point p, q = {10, 20};
var Point r = p + q; // r will be {20, 40}

from corto.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.