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

Unified Call Syntax #148

Closed
jmi2k opened this issue May 5, 2016 · 10 comments
Closed

Unified Call Syntax #148

jmi2k opened this issue May 5, 2016 · 10 comments
Labels
breaking Implementing this issue could cause existing code to no longer compile or have different behavior. enhancement Solving this issue will likely involve adding new logic or components to the codebase. proposal This issue suggests modifications. If it also has the "accepted" label then it is planned.
Milestone

Comments

@jmi2k
Copy link

jmi2k commented May 5, 2016

You can see it here. I've seen it in some new languages, and I think it fits perfectly into Zig. Also, it allows adding functions that operate on any type after declaring all their members: just put it as the first argument.

glfwSetKeyCallback(window, key_callback);    // Meh
window.glfwSetKeyCallback(key_callback);     // Nice!

struct TwoNumbers {
    x: i32,
    y: i32,

    pub fn add() -> i32 {
        return x + y;
    }
}

// Simple and clean!
pub fn sub(self: TwoNumbers) -> i32 {
    return self.x - self.y;
}
@andrewrk
Copy link
Member

andrewrk commented May 5, 2016

It's not clear how this would fit into Zig.

Zig does not have overloading (and almost certainly won't in the future), so a function named sub would need to be in an appropriate namespace to not clash with other functions with the same name.

In another file, to import this, assuming the file was called "math.zig" you would do:

const math = @import("math.zig");
fn f() {
    const v = TwoNumbers {.x = 1, .y = 2};
    math.sub(v);
}

With your UFCS proposal you could perhaps do:

const math = @import("math.zig");
const sub = math.sub;
fn f() {
    const v = TwoNumbers {.x = 1, .y = 2};
    v.sub();
}

Or maybe using the use keyword as currently exists:

use @import("math.zig");
// now `sub` is in the current namespace

But then you wouldn't be able to import sub from another import:

use @import("vector.zig");
// might cause error because `sub` is defined twice.

Versus if you had this:

struct Vector {
    x: i32,
    y: i32,
    pub fn sub(self: Vector, other: Vector) -> Vector {
        Vector {.x = self.x - other.x, .y = self.y - other.y}
    }
}
struct TwoNumbers {
    x: i32,
    y: i32,
    pub fn sub(tn: TwoNumbers) -> i32 {
        tn.x - tn.y
    }
}

Here we have two implementations of sub and it's fine because they're namespaced to the structs they reside in.

Further, putting the function in the struct works conveniently for generic programming:

struct Vector(T: type) {
    x: T,
    y: T,
    pub fn sub(self: Vector(T), other: Vector(T)) -> Vector(T) {
        Vector(T) {.x = self.x - other.x, .y = self.y - other.y}
    }
}

It's not clear how this would work without being in the scope of the struct.

Side note about the use declaration, I'm considering removing it in favor of having to be explicit about all imported things so that when reading code you can always know where imports come from.

@thejoshwolfe
Copy link
Contributor

I'm not convinced that extension methods are worthwhile.

There are two main arguments I can find in favor of extension methods: chaining extension methods like a.foo().bar().baz() instead of baz(bar(foo(a))), and IDE dot completion.

I'm not sure how useful chaining extension methods will be in Zig for a number of reasons like: no garbage collector, and no OOP (at least yet). (Remember that Zig already has method syntax for methods declared inside the struct; this proposal allows method syntax for "third-party" methods.) I feel like where extension methods really shine in C# is when you extend IEnumerable<T> or some other interface with functions that naturally follow from the interface's definition, like TakeFirst. Since Zig doesn't have interfaces, there's a big chunk of the benefit lost right there.

Regarding IDE dot completion, I'm generally in favor of catering to IDE's (even if they don't exist yet), but I'm not sure this is really the right approach to that problem. Extension method syntax will enable you to discover a certain class of method, which is cool I guess. But the cost seems pretty high.

I can't get past the idea that UFC would allow you to write u32(60).syscall1(0), which just makes no sense at all. syscall1 is certainly not a method of the type u32, and the ability to write code like this would make Zig a little bit worse of a language.

So is this feature worth this cost? I'm not convinced.

@marler8997
Copy link
Contributor

Responding to @thejoshwolfe here instead of #427 since it's more related to UFCS. For reference I'm providing an example where "chaining extension methods" helps readability:

print(raiseToThePowerOf( plus( filterIfGreaterThan(someList, 3), 10 ), 2), stdout)

With UFCS you could write this as:

someList.filterIfGreaterThan(3).plus(3).raiseToThePowerOf(2).print(stdout)

I'm including the proposed prefix annotation which is another solution to this problem. Note that this proposal is just a concept, the chosen syntax is just to demonstrate the idea.

// note the "prefix" annotation on the first argument of these function definitions
pub fn filterIfGreaterThan(prefix r : Range, limit : i32) { ... }
pub fn plus               (prefix r : Range, valueToAdd : i32) { ... }
pub fn raiseToThePowerOf  (prefix r : Range, exponent : i32) { ... }
pub fn print              (prefix r : Range) { ... }

// Possible syntax for providing prefix arguments to a function call: prefixArg > func(normalArgs...)
someList > filterIfGreaterThan(3)

Note that when an argument is marked as prefix, it cannot be passed in like a normal parameter, i.e.

filterIfGreaterThan(someList, 3)  // ERROR: filterIfGreaterThan requires a prefix argument

Since this is an error, it means there is only one way to call the function, so josh's example u32(60).syscall1(0) would result in an error. With this feature chaining methods would look like this:

someList > filterIfGreaterThan(3) > plus(3) raiseToThePowerOf(2)

@thejoshwolfe
Copy link
Contributor

How would memory management work for the Range class you're proposing? Can you show me an implementation of plus so I can get a better understanding of your example?

Note that therer's no "default allocator" in Zig, so no new or delete operators and no builtin garbage collector. See here for an example of a class that allocates memory: https://github.com/zig-lang/zig/blob/master/std/array_list.zig

I'm raising this point, because your code example that does map/filter operations on lists looks like it depends on a garbage collector, and Zig doesn't have that.

I think it's important to make big design decisions motivated by more concrete than theoretical usecases, so I'm trying to take your code example pretty seriously here.

@marler8997
Copy link
Contributor

Ranges have nothing to do with memory allocation, it's just a set of methods you implement that allows code to know how to work together on a set of data. There are different kinds of ranges but the most basic is the input range which requires the empty, front and popFront methods. These are the methods you would call in a for loop like this:

for(; !range.empty(); range.popFront())
{
    auto front = range.front();
    // use front
}

You could use memory allocation in your range but typically all the data is computed lazily and kept on the stack.

I think an example implementation will be better than trying to explain. I don't have a working zig compiler right now so I've included a C++ implementation that makes use of templates, you could also create one that uses virtual functions and pointer casting, but this one is more efficient since it can inline the entire thing and end up behaving as if you wrote for loops for everything. I've also included the equivalent in D, which showcases what "template type deduction" gives you and also what it would look like with UFCS.

#include <stdio.h>

template<typename Range>
void printNumbers(Range range)
{
    for(; !range.empty(); range.popFront())
    {
        printf("%d\n", range.front());
    }
}

template<typename T>
struct arrayRange
{
    T* next;
    T* limit;
    arrayRange(T* start, int count) : next(start), limit(start + count)
    {
    }
    bool empty()
    {
        return next >= limit;
    }
    T front()
    {
        return *next;
    }
    void popFront()
    {
        next++;
    }
};

template<typename Range>
struct plus
{
    Range inputRange;
    int valueToAdd;
    plus(Range inputRange, int valueToAdd) : inputRange(inputRange), valueToAdd(valueToAdd)
    {
    }
    bool empty()
    {
        return inputRange.empty();
    }
    int front()
    {
        return inputRange.front() + valueToAdd;
    }
    void popFront()
    {
        inputRange.popFront();
    }
};

template<typename Range>
struct filterIfGreaterThan
{
    Range inputRange;
    int limit;
    filterIfGreaterThan(Range inputRange, int limit) : inputRange(inputRange), limit(limit)
    {
    }
    bool empty()
    {
        for(; !inputRange.empty(); inputRange.popFront())
        {
            if(inputRange.front() <= limit)
            {
                return false;
            }
        }
        return true;
    }
    int front()
    {
        return inputRange.front();
    }
    void popFront()
    {
        inputRange.popFront();
    }
};


int main(int argc, char* argv[])
{
    int integerArray[] = {2, 1, 5, 8, 3};

    printf("Print Numbers:\n");
    printNumbers<arrayRange<int>>(
        arrayRange<int>(integerArray, 5));

    printf("Add 10 to each number and print:\n");
    printNumbers<plus<arrayRange<int>>>(
        plus<arrayRange<int>>(
        arrayRange<int>(integerArray, 5), 10));

    printf("Print original numbers to show they haven't changed:\n");
    printNumbers<arrayRange<int>>(
        arrayRange<int>(integerArray, 5));

    printf("Add 10 to each number then filter any that are greater than 13, then print them\n");
    printNumbers<filterIfGreaterThan<plus<arrayRange<int>>>>(
        filterIfGreaterThan<plus<arrayRange<int>>>(
        plus<arrayRange<int>>(
        arrayRange<int>(integerArray, 5), 10), 13));

    return 0;
}

And here's the D version

import std.stdio;

void printNumbers(Range)(Range range)
{
    // Note: D's foreach loop understand ranges
    foreach(value; range)
    {
        writeln(value);
    }
}

// D already has methods that treat arrays like ranges, but I'm including this to make
// it close to the C++ version.
auto arrayRange(T)(T[] array)
{
    struct _
    {
        T* next;
        T* limit;
        this(T* start, T* limit)
        {
            this.next = start;
            this.limit = limit;
        }
        bool empty()
        {
            return next >= limit;
        }
        T front()
        {
            return *next;
        }
        void popFront()
        {
            next++;
        }
    }
    return _(array.ptr, array.ptr + array.length);
};

auto plus(Range)(Range inputRange, int valueToAdd)
{
    struct _
    {
        Range inputRange;
        int valueToAdd;
        this(Range inputRange, int valueToAdd)
        {
            this.inputRange = inputRange;
            this.valueToAdd = valueToAdd;
        }
        bool empty()
        {
            return inputRange.empty();
        }
        int front()
        {
            return inputRange.front() + valueToAdd;
        }
        void popFront()
        {
            inputRange.popFront();
        }
    }
    return _(inputRange, valueToAdd);
};

auto filterIfGreaterThan(Range)(Range inputRange, int limit)
{
    struct _
    {
        Range inputRange;
        int limit;
        this(Range inputRange, int limit)
        {
            this.inputRange = inputRange;
            this.limit = limit;
        }
        bool empty()
        {
            for(; !inputRange.empty(); inputRange.popFront())
            {
                if(inputRange.front() <= limit)
                {
                    return false;
                }
            }
            return true;
        }
        int front()
        {
            return inputRange.front();
        }
        void popFront()
        {
            inputRange.popFront();
        }
    }
    return _(inputRange, limit);
};


int main(string[] args)
{
    auto integerArray = [2, 1, 5, 8, 3];

    //
    // No UFCS, but with template parameter deduction
    //
    writeln("Print Numbers:");
    printNumbers(arrayRange(integerArray));

    writeln("Add 10 to each number and print:");
    printNumbers(
        plus(
        arrayRange(integerArray), 10));

    writeln("Print original numbers to show they haven't changed:");
    printNumbers(
        arrayRange(integerArray));

    writeln("Add 10 to each number then filter any that are greater than 13, then print them");
    printNumbers(
        filterIfGreaterThan(
        plus(
        arrayRange(integerArray), 10), 13));

    //
    // Now use template parameter deduction AND UFCS
    //
    writeln("Print Numbers:");
    integerArray.arrayRange.printNumbers;

    writeln("Add 10 to each number and print:");
    integerArray.arrayRange.plus(10).printNumbers;

    writeln("Print original numbers to show they haven't changed:");
    integerArray.arrayRange.printNumbers;

    writeln("Add 10 to each number then filter any that are greater than 13, then print them");
    integerArray.arrayRange.plus(10).filterIfGreaterThan(13).printNumbers;

    return 0;
}

@thejoshwolfe
Copy link
Contributor

Great example! I'll convert all that to zig to see how it looks with status quo. Standby...

@andrewrk andrewrk reopened this Sep 13, 2017
@andrewrk andrewrk added this to the 0.2.0 milestone Sep 13, 2017
@andrewrk andrewrk added breaking Implementing this issue could cause existing code to no longer compile or have different behavior. enhancement Solving this issue will likely involve adding new logic or components to the codebase. labels Sep 13, 2017
@raulgrell
Copy link
Contributor

raulgrell commented Sep 13, 2017

This was in the context of the infix function proposal, which is why it is in terms of Vecs - they are not the best structures to demonstrate the concept.

I will expand on this later, meanwhile, here are my thoughts. Keep in mind some of this is already possible with the generic syntax.

I've been thinking of how I'd pass arbitrarily typed self parameters to a function,
or a way to signal that the implicit passing of a parameter was expected,
or communicate that a function is acting on a value, similar to what @marler8997 did with the prefix keyword.

They keyword bound was chosen for illustrative purposes, and I apologize for the # sigil =P

const Vec2 = struct {
    data: [2]f32,

    // Like a for loop, use a * to get a reference.
    // function bound to `this` ?
    bound fn add(other: &const Vec2) | *self | -> &Vec2 {
        self.data = sum(self, other).data;
        return self;
    }
    
    bound fn plus(other: &const Vec2)  | self | -> Vec2 {
        sum(self, other)
    }

    pub fn sum(self: &const Vec2, other: &const Vec2) -> Vec2 {
        vec2(self.data[0] + other.data[0], self.data[1] + other.data[1])
    }
}

const a = Vec2.init(1, 2);
const b = Vec2.init(4, 8);
// Namespace call
const c = Vec2.sum(Vec2.sum(a, b), a);
// UFCS
const u = a.sum(b).sum(c);
const m = a.add(b).add(c);
const d = a.plus(b).plus(c);

This led to a sort of trait/impl construct idea, that uses the compile-time duck typing concept to provide a generic way to access an abitrary struct.
If we provide the type of the self variable, we can compose structures at 'bind' time.
This is like explicitly extending the namespace of a struct, and being able to access the functions like methods on an instance thanks to UFCS

const Additive = bound struct {
    data: []f32, // Make sure struct being bound to has a field with same name?
                 // Struct field must be implicitly castable to this field type. 

    fn add(other: &const T) | *self, T | -> &T {
        // Provide implementation
        for (self.data) | d, i | {
            self.data[i] += other.data[i];
        }
        return self;
    }
    
    fn plus(other: &const T)  | self, T | -> T {
        // Or use existing
        // Duck-typing: works if T namespace has fn sum(&const T, &const T) -> T 
        T.sum(self, other)
    }
}

const Vec3 = struct {
    data: [3]f32,
    
    pub fn sum(self: &const Vec2, other: &const Vec2) -> Vec2 {
        vec3(
            self.data[0] + other.data[0],
            self.data[1] + other.data[1],
            self.data[2] + other.data[2]
        )
    }
}

const a = #Vec3(Additive).init(1, 2, 6);
const b = #Vec3(Additive).init(4, 8, 6);

// Namespace call, a and b are still Vec3's
const c = Vec3.sum(Vec3.sum(a, b), a);

// UFCS with namespace and extension functions.
const d = a.sum(b).sum(c);
const e = a.add(b).add(c);
const f = a.plus(b).plus(c);

And an idea about explicit interfaces, where you basically require that
a struct be extended at bind time with some sort of implementation, but this gets fiddly

const IAdd = struct {
    add: bound fn(other: &const T) | self, T | -> &T, 
    plus: bound fn(other: &const T) | self, T | -> T, 
}

const Vec4 = struct(IAdd) {
    data: [3]f32,
    
    pub fn sum(self: &const Vec2, other: &const Vec2) -> Vec2 {
        vec3(
            self.data[0] + other.data[0],
            self.data[1] + other.data[1],
            self.data[2] + other.data[2],
            self.data[3] + other.data[3],
        )
    }
}

// Composed structs
const Vec4T = #Vec4(Additive);

const _ = Vec3.init(1, 2, 6); // Fine
const _ = Vec4.init(1, 2, 6, 8); // Error: must bind an IAdd

const a = Vec4T.init(1, 2, 6, 8); // Works, Additive provides IAdd funcs
const b = #Vec4(Additive).init(1, 3, 5, 7)

// Namespace call, a and b are still Vec4's
const c = Vec4.sum(Vec4.sum(a, b), a);

// UFCS with namespace and extension functions.
const d = a.sum(b).sum(c);
const e = a.add(b).add(c);
const f = a.plus(b).plus(c);

const x = c.plus(a).plus(b); // c has not had Additive bound to it, so does not have plus?
const y = c.sum(a).plus(b); // c.sum(a) makes a @typeOf(c) which doesn't have Additive, so fails?

@tiehuis tiehuis added the proposal This issue suggests modifications. If it also has the "accepted" label then it is planned. label Sep 15, 2017
@thejoshwolfe
Copy link
Contributor

To follow up my earlier comment, I determined that the Range code example is what I would call filter iterators, which are definitely possible without unified call syntax. Here's an actual real example of me using filter iterators in C++, and UFCS is not an issue.

Can we come up with an actual real example where UFCS would be good? I see lots of discussion about vectors, but that's an example of where you can use status-quo method call syntax, so UFCS is not relevant there.

@raulgrell
Copy link
Contributor

raulgrell commented Sep 19, 2017

I was actually under the impression status quo was UFCS. I was using it as an example of how status quo was good. Perhaps I should annotate my comments to make that clear.

If someone could describe how status quo differs from UFCS, that might make things clearer

@thejoshwolfe
Copy link
Contributor

I didn't read the entire pdf in the OP, and the poster seems to have lost interest, so I'll state these definitions for the sake of discussion:

  • Method syntax (status quo): Declare methods in a struct, then you can call them with object.method() or Struct.method(object).
  • Extension methods: Declare methods outside of a struct definition with special notation that designates them as being usable as extension methods. Example above. Calling it still looks like object.method().
  • Unified Function Call Syntax: All functions are usable as extension methods. This would enable u32(60).syscall1(0).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
breaking Implementing this issue could cause existing code to no longer compile or have different behavior. enhancement Solving this issue will likely involve adding new logic or components to the codebase. proposal This issue suggests modifications. If it also has the "accepted" label then it is planned.
Projects
None yet
Development

No branches or pull requests

6 participants