v
v copied to clipboard
Optional explicit interface implementations
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:
- Why or how
PathError
is converted toIError
. - 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.