v icon indicating copy to clipboard operation
v copied to clipboard

Optional explicit interface implementations

Open mindplay-dk opened this issue 6 months ago • 4 comments

Describe the feature

Just to be clear, I'm aware this has been requested before:

  • https://github.com/vlang/v/issues/183
  • https://github.com/vlang/v/issues/1314
  • https://github.com/vlang/v/issues/13526
  • https://github.com/vlang/v/issues/14953
  • https://github.com/vlang/v/issues/15681

But it doesn't seem like this was ever discussed at length - and no in-depth reasoning was presented, explaining why this feature should be considered, so I will try to do that.

To be clear, what I'm proposing is an optional explicit implements keyword, which would apply to struct declarations.

What I'm proposing is this:

struct MyStruct implements IFirst, ISecond {
    first string
    second string
}

interface IFirst {
    firts string
}

interface ISecond {
    second string
}

(Alternatively, implements could be abbreviated as : if the terser syntax feels more like V.)

The implements declaration would be checked locally - that is, this declaration will first validate that MyStruct actually implements all the fields and methods in each of the IFirst and ISecond interfaces, before validating or reporting errors about invalid arguments in call sites, or invalid assignments, etc.

Of course, we would prefer not to write explicit implementations everywhere, because it's convenient, and because it does work well most of the time - to be clear, I very much like the approach taken interfaces by Go and TypeScript, it's definitely preferable most of the time, just not all of the time. TypeScript doesn't strictly need this feature either, but it has this feature, because there are times when being clear and explicit is objectively better for clarity, as well as for error reporting.

Use Case

There are two reasons why this should be given proper consideration.

1. Code readability

This example from the documentation demonstrates a code readability problem:

struct PathError {
    Error
    path string
}

fn (err PathError) msg() string {
    return 'Failed to open path: ${err.path}'
}

fn try_open(path string) ! {
    // V automatically casts this to IError 👈🤔
    return PathError{
        path: path
    }
}

fn main() {
    try_open('/tmp') or { panic(err) }
}

The example actually highlights the problem with the comment "V automatically casts this to IError" - I understand this comment was added to the manual to explain what's happening, but it's not unthinkable someone would actually put a comment like this in their code, since it is not clear from reading the code.

There is nothing in the code explains:

  1. Why or how PathError is converted to IError.
  2. Why the fn (err PathError) msg() string method is actually present.

With an explicit implementation, the example is clear:

struct PathError implements IError { 👈
    Error
    path string
}

fn (err PathError) msg() string {
    return 'Failed to open path: ${err.path}'
}

fn try_open(path string) ! {
    return PathError{
        path: path
    }
}

fn main() {
    try_open('/tmp') or { panic(err) }
}

It's now clear that the PathError declaration is intended to implement IError - we don't need the comment in try_open to explain what's going on, and there is no mystery as to why the msg method exists, which previously looked like it might have been an unused method.

2. Error Reporting

From the example above, also consider the fact that error reporting can now report the error where the error is - for example, if you were to omit the msg method, you would get an error saying the msg method is required to correctly implement the IError interface. In other words, implements IError explains our intent to implement that interface, which the reader (and compiler) could otherwise only assume based on usage.

Let's consider the interfaces example from the documentation:

struct Dog implements Speaker {
    breed string
}

fn (d Dog) speak() string {
    return 'woof'
}

struct Cat implements Speaker {
    breed string
}

fn (c Cat) speak() string {
    return 'meow'
}

interface Speaker {
    breed string
    speak() string
}

Here, the addition of implements makes it clear that there is an intended relationship between Cat and Dog and the Speaker interface - you know this in advance when you see the first declaration. Contrast this with the example in the manual, where no relationship is established unless you read through the calling code, which I've intentionally omitted from the updated example here - there is no saying the calling code for an interface is located in the same file.

In terms of error reporting, we can now point to Dog or Cat as having a problem, if the required methods and fields aren't present, or have the wrong type, etc. - rather than pointing at many call sites having potential problems, we can now point at a single declaration as definitely having a problem, e.g. "missing field" or "missing method".

One, clear error versus many potential errors.

Proposed Solution

No response

Other Information

This feature could help with modularity and reuse as well.

For example, consider the example presented here, where you need to implement a Context type:

pub struct Context {
    veb.Context
pub mut:
    // In the context struct we store data that could be different
    // for each request. Like a User struct or a session id
    user       User
    session_id string
}

There is no problem with this example per se, but consider the case where I'd like to create a module that provides a reusable middleware, which will establish a session_id - in order to work with your custom context, this middleware would need you to add the session_id string to your Context type, and this would need to be part of an interface.

If you see the declaration above, you would have no idea why the session_id is there, and a pub struct Context implements ISessionContext declaration would help make that relationship clear and explicit - it explains why you would expect this Context implementation to work with the session middleware, and ensures we point to the source of that problem.

If some day we have an ecosystem of reusable middleware, it would be helpful to understand from someone's Context declaration how they intended for it to work with the integrated middleware components.

Acknowledgements

  • [ ] I may be able to implement this feature request
  • [ ] This feature might incur a breaking change

Version used

0.4.7

Environment details (OS name and version, etc.)

[!NOTE] You can use the 👍 reaction to increase the issue's priority for developers.

Please note that only the 👍 reaction to the issue itself counts as a vote. Other reactions and those to comments will not be taken into account.

mindplay-dk avatar Aug 10 '24 11:08 mindplay-dk