-
-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Description
Just wondering what is the best approach to encapsulate structs, or protect the internal state of structs, in zig code. An example could be a clock struct, where the fields are constrained to certain ranges and where they may impact each other when changed. For example, adding 160 minutes to the clock time will change both minute and hour fields.
const Clock = struct{
hour: u8,, // in range 0..23
minute: u8, // in range 0..59
second: u8, // in range 0..59
millisecond: u16, // in range 0..999
fn addHours(hours: u8){
// update internal state
}
fn addMinutes(minutes: u8){
// update internal state
}
//other functions
}In zig 0.4 you can't make fields private, so you can't protect state the normal OOP way by using setters and getters. Currently, the only way to achieve some kind of encapsulation is by using naming convention like _second instead of second to imply that this field is internal and not meant to be modified from outside the struct.
I see four possible future alternatives for protection of state if something more elaborate than naming convention is desirable:
std.debug.asserta customisValidmethod each time before using a struct instance is used, or each time after modification- Add possibility to mark fields as
privateor readonly when accessed from outside its own struct. - Make the whole struct instance
immutable,and ensure that when initialized the instance is guaranteed to be valid. This would need some kind of protected "constructor" though. If it's possible to doconst myclock = Clock{.hour= 999999, ...}then it doesn't help that the instance is immutable after. - Encapsulate with Java style interface.
var myclock: IClock = Clock.init()This way, the fields are hidden when accessing themyclockinstance while all necessary getters and setters are available through the interface. It could even be possible to enforce that no instance will have the type of the implementing struct so that only the interface will be exposed. Example:
const Clock = implementation struct{
// members
}
const myclock : Clock = Clock.init()
// compile error:
// Instance of type Clock can only be instantiated as an interface implementation
const myclock : IClock = Clock.init() // OK.Personally, I highly prefer alternative 4, as interfaces can provide other things in addition to just encapsulation, such as polymorphism, clarify package/module APIs, and help IDEs autogenerate code. It can also provide the same benefits as inheritance or struct embedding (#1214), at least if you factor in IDE tools. For example, imagine a generate-code dialog that asks: "implement Interface1 by delegating to a Interface1Impl instance?" which if you accept, creates all that "boilerplate" code for you in the fraction of a second, with no added language complexity to worry about.