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

named return values and reference-assignment operators #286

Closed
andrewrk opened this issue Mar 27, 2017 · 6 comments
Closed

named return values and reference-assignment operators #286

andrewrk opened this issue Mar 27, 2017 · 6 comments
Labels
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

@andrewrk
Copy link
Member

andrewrk commented Mar 27, 2017

Proposal:

  • There are 2 forms of return values. Status quo, and named return values.
  • Copyable types must use normal return syntax.
  • Non-copyable types must use named return syntax.
const Point = struct {
    x: i32,
    y: i32,
};

const Vec3 = struct {
    x: i32,
    y: i32,
    z: i32,
};

const Value = enum {
    One: Point,
    Two: Vec3,
};

fn foo() -> (result: %Value) {
    // here we want to assign to x,y,z fields but the error and enum are
    // "in the way"
    // we could overwrite the whole thing:
    result = Value.One { Point { .x = 1, .y = 2, }};
    // actually, is this even legal? if Value is non-copyable then this would be a copy right here.

    // but what if we want to assign the fields piecemeal?
    const payload: &Value = &.%result;
    // now result is set to non-null and result payload is undefined.
    const vec: &Vec3 = &.Value.Two payload;
    // now result is set to non-null with enum tag Value.Two
    // and enum payload undefined
    vec.x = 1;
    vec.y = 2;
    vec.z = 3;

    // combining them to 1 line:
    const vec = &.Value.Two &.%result;
    vec.x = 1;
    vec.y = 2;
    vec.z = 3;
}

Covers part of #83.

The syntax looks pretty scary and is certainly not intuitive.

But the semantics make sense. The point of this is so that the same init code can work at compile time and at runtime. Consider an init method. Here's status quo std.rand.Rand:

pub const Rand = struct {
    rng: Rng,

    /// Initialize random state with the given seed.
    pub fn init(r: &Rand, seed: usize) {
        r.rng.init(seed);
    }
};

This is problematic because you cannot initialize Rand at compile time. Also the usage looks like this:

var rand: Rand = undefined;
rand.init(seed);

Better usage would look like:

var rand = Rand.init(seed);

And this would work the same at run-time or compile-time.

Although I guess technically you can still get it to work at compile-time status quo:

var rand = {
    var tmp: Rand = undefined;
    tmp.init(seed);
    tmp
};

This is still less than ideal. We could have the better syntax with this definition:

pub const Rand = struct {
    rng: Rng,

    /// Initialize random state with the given seed.
    pub fn init(seed: usize) -> (r: Rand) {
        r.rng.init(seed);
        // or if rng uses the same form,
        r.rng = Rng.init(seed); // not actually a copy since Rng.init also using named return value
    }
};
@andrewrk andrewrk added the enhancement Solving this issue will likely involve adding new logic or components to the codebase. label Mar 27, 2017
@andrewrk andrewrk added this to the 0.1.0 milestone Mar 27, 2017
@raulgrell
Copy link
Contributor

raulgrell commented Mar 27, 2017

Proposal: a reference assignment statement

I think this idea is syntactically sound, but "set" is not be the best word. It doesn't really describe what the statement is doing, the block after it is where things are actually set.

It does essentially what the reference-assignment operator proposed above does.


fn foo() -> (result: %Value) {
    set(result) | payload | {                      // @typeOf(payload) == &Value
        // now result is set to non-null and result payload is undefined.
        set(payload) | vec: Value.Two | {   // @typeOf(vec) == &Vec3
            // result is set to non-null with enum tag Value.Two
            // and enum payload undefined
            vec.x = 1;
            vec.y = 2;
            vec.z = 3;
        }
    }
}

fn bar() -> (result: %Vec3) {
    set(result) | vec | {
        // now result is set to non-null Vec3 undefined
        vec.x = 1;
        vec.y = 2;
        vec.z = 3;
    }
}

const Point = struct {
    x: i32,
    y: i32,
};

const Vec3 = struct {
    x: i32,
    y: i32,
    z: i32,
};

const Value = enum {
    One: Point,
    Two: Vec3,
};

Other names to consider:

  • refer/reference/assign(result) | payload | {}
    Since it would be like reference-assigning result to a var payload.

  • resolve/unwrap(result) | payload | {}
    Since it's resolving/unwrapping a nullable/error union/enum(type-union) into a payload. These seem like the most generally applicable names.

  • view/with(result) | payload | { }
    Since it's creating a view into the result through the payload, or "with result's payload"

  • return(result) | payload() {}
    Variation of a result statement, you're defining the block of code that's dealing with the assignments to the payload involved in returning.

@thejoshwolfe
Copy link
Sponsor Contributor

See #287 for my copy eliding proposal.

With that proposal the following is fine (in the context of this issue's OP):

fn foo() -> (result: %Value) {
    result = Value.One { Vec2 { .x = 1, .y = 2, }};
}
// or even without a named return value:
fn foo() -> %Value {
    Value.One { Vec2 { .x = 1, .y = 2, }}
}

So here's my proposal to throw into the bucket of ideas for this issue's reference-assignment usecase:

Introduce named return values for blocks. I'm not super excited about this idea, but here it is.

fn foo() -> (result: %Value) {
    // where a Vec2 value is expected, we provide a block that returns a Vec2.
    // the block's return value is named vec.
    result = Value.One { -> (vec: Vec2) {
      vec.x = 1;
      vec.y = 2;
    }};
}

This syntax doesn't introduce any new keywords. I'm not sure if this would introduce ambiguity with the inline assembly syntax, but it doesn't introduce any ambiguity elsewhere.

@raulgrell
Copy link
Contributor

@thejoshwolfe - I like where you're going, and think the sweet spot is actually somewhere between our suggestions... How about this?

fn foo() -> (result: %Value) {
    // Direct assignment (same as before, lines split for visual comparison with proposal)
    result = Value.One { Vec2 { 
         .x = 1,
         .y = 2,
    }}
    // No need to specify type of vec, Value.One already specifies Vec2
    result = Value.One | vec | {
        vec.x = 1;
        vec.y = 2;
    };
}

Which with your copy eliding rules makes it just like a regular assignment. It's still sort of a named return value for a block or a named assignment address, equivalent to something like

    result = {
         var vec =  Value.One { Vec2 { .x = undefined, .y = undefined } };
         vec.x = 1;
         vec.y = 2;
         vec
    };

I like the |...| syntax for this because of how it appears in the rest of zig:

  • for ( array ) | member, index | {}
    for() splits up the array, and with each of its members, executes a block with 'parameters' member and index

  • switch( enumvar ) { enum.Type => | payload | {} }
    Switch matches an enum var with it's type, creates the corresponding payload, executes the block with the payload

  • const a = func() %% |err| { return err }
    The %% checks to see if func returns an error, and if it does, execute a block with that error.

Basically, the |...| creates a view into something created by the statement it's in. The same syntax, for a %Vec2

fn bar() -> (result: %Vec2) {
    result = Vec2 | vec | {
        vec.x = 1;
        vec.y = 2;
    }
}

In this proposal, the statement the |...| is drawing from is equivalent to an assignment, "unsugared":
( result = Vec3{undefined} ) | ref to assignment | { }

The -> and the parentheses (vec: Vec2) makes it look like a function parameter declaration and think it might be more intuitive to leave that for closures/functions within functions. #229

I'm about to comment on the null/error unwrapping vs try/test blocks and some exploration of the |...| concept that dips into closures.

Would doing something like this make sense?

const Func = enum {
     A: fn(param:T) ->T,
}

fn selfFn(param:T) ->T {
     return param
}

fn baz() -> (result: %Func ) {
    // Should this work?
    result = selfFn;
    // Or this ?
    result = Func.A { selfFn }
    // If not, is this better? Same syntax/semantics as rest of proposal
    result = Funct.A | fn | {
        fn = selfFn;
    };
}

@thejoshwolfe
Copy link
Sponsor Contributor

i like where you're going @raulgrell. Consider the comment I just added in #287, which could allow this syntax:

fn foo() -> %Value {
    Value.One {
        var vec: Vec2 = undefined;
        vec.x = 1;
        vec.y = 2;
        vec
    }
}

This is a very small departure from existing Zig constructs, and solves this issue entirely with copy eliding rules. That being said, I'm not sure we want the copy eliding rules to be this sophisticated. See #287.

@thejoshwolfe
Copy link
Sponsor Contributor

Would doing something like this make sense?

fn baz() -> (result: %Func ) {
    // Should this work?
    result = selfFn; // <---- no. you need to reference Func.A
    // Or this ?
    result = Func.A { selfFn } // <---- that should work already
}

@andrewrk
Copy link
Member Author

Closing in favor of well defined copy eliding semantics (#287) which accomplishes all the goals of named return values.

@tiehuis tiehuis added proposal This issue suggests modifications. If it also has the "accepted" label then it is planned. rejected labels Sep 15, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
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

4 participants